SpringBatch-高级教程-全-
SpringBatch 高级教程(全)
一、Spring Batch
2001 年,当我从北伊利诺伊大学毕业,花了两年时间研究 COBOL、大型机汇编程序和作业控制语言(JCL)之后,我找到了一份学习 Java 的顾问工作。我特别选择了那个职位,因为在 Java 还是热门新事物的时候,我有机会学习它。我做梦也没想到我会回来写批处理。我相信大多数 Java 开发人员也不会考虑批处理。他们考虑最新的 web 框架或 JVM 语言。他们考虑面向服务的架构,以及 REST 与 SOAP 之类的东西,或者当时流行的任何字母汤。
但事实是,商业世界是批量运行的。您的银行和 401k 报表都是通过批处理生成的。你从最喜欢的商店收到的带有优惠券的电子邮件?可能通过批处理发送。就连修理工来你家修你洗衣机的顺序都是批量处理决定的。在一个我们从 Twitter 获取新闻的时代,谷歌认为等待页面刷新需要太长时间来提供搜索结果,而 YouTube 可以让某人一夜之间家喻户晓,为什么我们需要批处理呢?
有几个很好的理由:
- 您并不总是能立即获得所有需要的信息。批处理允许您在开始所需的处理之前收集给定处理所需的信息。以你每月的银行对账单为例。在每次交易后为打印的报表生成文件格式有意义吗?更有意义的做法是等到月底,回头看看经过审核的交易清单,以此来构建报表。
- 有时候这是很好的商业意识。虽然大多数人喜欢在他们点击购买的第二秒就把他们在网上购买的东西放在送货卡车上,但这可能不是零售商的最佳行动方案。如果客户改变主意,想要取消订单,如果订单还没有发货,取消会便宜很多。给顾客几个小时的额外时间,一起分批发货,可以为零售商节省大量资金
- 它可以更好地利用资源。闲置大量处理能力的成本很高。让一组预定的进程以一个恒定的、可预测的速率一个接一个地运行,充分发挥机器的潜力,这样更划算。
这本书是关于用 Spring Batch 框架进行批处理的。本章回顾了批处理的历史,指出了开发批处理作业的挑战,提出了使用 Java 和 Spring Batch 开发批处理的案例,最后提供了框架及其特性的高级概述。
批量处理的历史
要看批处理的历史,你真的需要看计算本身的历史。
时间是 1951 年。UNIVAC 成为第一台商业化生产的计算机。在此之前,计算机都是为特定功能设计的独特的定制机器(例如,在 1946 年,军方委托一台计算机来计算炮弹的轨迹)。UNIVAC 由 5200 个真空管组成,重量超过 14 吨,速度高达 2.25MHz(相比之下,iPhone 4 的处理器为 1GHz),运行从磁带驱动器加载的程序。在当时,UNIVAC 被认为是第一个商业化的批处理机。
在深入历史之前,我应该定义一下批处理到底是什么。你开发的大多数应用都有用户交互的一面,无论是用户点击 web 应用中的链接,在胖客户端的表单中输入信息,还是在手机和平板电脑应用上点击。批处理与那些类型的应用完全相反。批处理,在本书中,被定义为没有交互或中断的数据处理。一旦开始,批处理运行到某种形式的完成,没有任何干预。
在计算机和数据处理的发展中,下一个大的变化:高级语言,已经过去了四年。它们最初是在 IBM 704 上与 Lisp 和 Fortran 一起引入的,但后来成为批处理世界中 800 磅重的大猩猩的是通用面向业务语言(COBOL)。COBOL 于 1959 年开发,并在 1968 年、1974 年和 1985 年进行了修订,它仍然在现代商业中运行批处理。Gartner 的一项研究 1 估计,全球 60%的代码和 85%的商业数据都是用这种语言编写的。换个角度来看,如果你把所有的代码都打印出来,然后把打印出来的东西堆起来,你会得到一堆 227 英里高的东西。但这正是创新停滞的地方。
COBOL 在四分之一个世纪里没有经历过重大的修改。教授 COBOL 及其相关技术的学校数量已经明显下降,取而代之的是像 Java 和. NET 这样的新技术。硬件很昂贵,资源也越来越稀缺。
大型计算机不是批处理发生的唯一地方。我之前提到的那些电子邮件是通过批处理发送的,而这些批处理可能不会在大型机上运行。从你最喜欢的快餐连锁店的销售点终端下载数据也是批量的。但是,您在大型机上发现的批处理进程和那些通常为其他环境编写的批处理进程(例如,C++和 UNIX)之间有一个显著的区别。这些批处理过程都是定制开发的,它们几乎没有共同点。自从被 COBOL 接管后,新的工具和技术就很少了。是的,cron 作业已经在 UNIX 服务器上启动了定制开发的进程,在 Microsoft Windows 服务器上启动了计划任务,但是还没有新的行业认可的工具来执行批处理。
直到现在。2007 年,埃森哲宣布它正与 interface 21(Spring 框架的最初写入器,现在的 SpringSource)合作开发一个开源框架,用于创建企业批处理流程。作为埃森哲首次正式进军开源世界,它选择将其在批处理方面的专业知识与 Spring 的流行和功能集结合起来,创建一个健壮、易用的框架。2008 年 3 月底,Spring Batch 1.0.0 发布版向公众开放;它代表了 Java 世界中第一个基于标准的批处理方法。一年多以后,在 2009 年 4 月,Spring Batch 升级到了 2.0.0,增加了一些特性,比如用 JDK 1.5+取代了对 JDK 1.4 的支持,基于块的处理,改进的配置选项,以及对框架内可伸缩性选项的显著增加。
1www . Gartner . com/web letter/merant/article 1/article 1 . html
在 COBOL 2002 和面向对象的 COBOL 中有一些修订,但是它们的采用比以前的版本少得多。
批量挑战
毫无疑问,您熟悉基于 GUI 编程的挑战(胖客户端和 web 应用都是如此)。安全问题。数据验证。用户友好的错误处理。不可预测的使用模式会导致资源利用率激增(让您的一篇博客文章出现在 Slashdot 的首页,以了解我在这里的意思)。所有这些都是同一件事的副产品:用户与你的软件交互的能力。
但是,批次不同。我在前面说过,批处理是一个不需要额外的交互就可以运行的过程。因此,GUI 应用的大多数问题都不再有效。是的,存在安全问题,并且需要数据验证,但是使用和友好的错误处理的峰值要么是可预测的,要么甚至可能不适用于您的批处理过程。您可以预测流程中的负载,并据此进行设计。如果只有可靠的日志记录和通知作为反馈,您可能会很快失败,因为技术资源可以解决任何问题。
所以批处理世界中的一切都是小菜一碟,没有挑战,对吗?很抱歉打破您的幻想,但是批处理在许多常见的软件开发挑战中呈现出它自己独特的变化。软件架构通常包括许多要素。可维护性。可用性。可扩展性。这些和其他能力都与批处理相关,只是方式不同。
前三个能力——可用性、可维护性和可扩展性——是相关的。有了 batch,你就不用担心用户界面了,所以可用性不是漂亮的图形用户界面和酷的动画。不,在批处理过程中,可用性是关于代码的:错误处理和可维护性。你能很容易地扩展通用组件来添加新特性吗?它在单元测试中被很好地覆盖了吗,这样当你改变一个已存在的组件时,你就可以知道对整个系统的影响了吗?当作业失败时,您是否知道何时、何地以及为什么失败,而无需花费很长时间进行调试?这些都是对批处理有影响的可用性的方面。
接下来是可扩展性。检查现实的时间到了:你最后一次在一个网站上工作是什么时候,这个网站每天有一百万的访问者?10 万怎么样?实话实说:大公司开发的大多数网站都不会被浏览很多次。然而,拥有一个需要在一个晚上处理 10 万到 50 万笔交易的批处理过程并不是一件难事。让我们把 4 秒钟加载一个网页看作一个稳定的平均值。如果通过批处理处理一个交易需要这么长时间,那么处理 100,000 个交易将需要四天以上的时间(对于 100 万个交易,则需要一个半月)。在当今的企业环境中,这对于任何系统都是不切实际的。底线是批处理需要能够处理的规模通常比您过去开发的 web 或胖客户端应用大一个或多个数量级。
第三是可用性。同样,这不同于您可能习惯的 web 或胖客户端应用。批处理通常不是全天候的。事实上,他们通常都有预约。大多数企业在知道所需的资源(硬件、数据等)可用时,会将作业安排在给定的时间运行。例如,需要为退休帐户建立报表。虽然您可以在一天中的任何时间运行该作业,但最好是在市场收盘后运行,这样您就可以使用收盘基金价格来计算余额。需要的时候能跑吗?你能在分配的时间内完成工作,不影响其他系统吗?这些问题和其他问题会影响批处理系统的可用性。
最后,您必须考虑安全性。通常,在批处理世界中,安全性并不围绕人们侵入系统和破坏东西。批处理在安全方面的作用是保护数据安全。敏感的数据库字段加密了吗?你是偶然记录个人信息的吗?对外部系统的访问如何?他们需要凭据吗?您是否以适当的方式保护这些凭据?数据验证也是安全性的一部分。通常,正在处理的数据已经过审查,但是您仍然应该确保遵守规则。
如您所见,在开发批处理过程中涉及到大量的技术挑战。从大多数系统的大规模到安全性,batch 都有。这是开发批处理过程的部分乐趣:你可以更专注于解决技术问题,而不是在 web 应用上将表单字段向右移动三个像素。问题是,在大型机现有基础设施和采用新平台的所有风险的情况下,为什么要用 Java 进行批处理呢?
为什么要用 Java 做批处理?
面对刚刚列出的所有挑战,为什么要选择 Java 和 Spring Batch 这样的开源工具来开发批处理呢?我能想到在你的批处理中使用 Java 和开源的六个原因:可维护性、灵活性、可伸缩性、开发资源、支持和成本。
可维护性第一。当您考虑批处理时,您必须考虑维护。这段代码通常比其他应用有更长的生命周期。这是有原因的:没有人看到批处理代码。与必须跟上当前趋势和风格的 web 或客户端应用不同,批处理是用来处理数字和构建静态输出的。只要它完成了它的工作,大多数人只是享受他们工作的成果。正因为如此,您需要以这样一种方式构建代码,使得它可以被容易地修改而不会招致大的风险。
进入 Spring 框架。Spring 是为你可以利用的一些东西而设计的:可测试性和抽象性。Spring 框架通过依赖注入和 Spring 提供的额外测试工具来鼓励对象的解耦,这允许您构建一个健壮的测试套件来最小化后续维护的风险。在没有深入探究 Spring 和 Spring 批处理工作方式的情况下,Spring 提供了以声明方式处理文件和数据库 I/O 的工具。你不必写 JDBC 代码或管理 Java 中文件 I/O API 的噩梦。像事务和提交计数之类的事情都由框架来处理,所以您不必管理您在流程中的位置以及当出现问题时该做什么。这些只是 Spring Batch 和 Java 为您提供的可维护性优势的一部分。
Java 和 Spring Batch 的灵活性是使用它们的另一个原因。在大型机领域,您有一个选择:在大型机上运行 COBOL。就这样。另一个常见的批处理平台是 UNIX 上的 C++。这最终成为一个非常定制的解决方案,因为没有业界接受的批处理框架。无论是大型机还是 C++/UNIX 方法都无法提供 JVM 部署的灵活性和 Spring Batch 的特性集。想要在装有*nix 或 Windows 的服务器、台式机或大型机上运行批处理?没关系。需要将您的流程扩展到多台服务器?无论如何,大多数 Java 都是在廉价的商用硬件上运行的,在机架上增加一台服务器并不像购买一台新的大型机那样是资本支出。事实上,为什么要拥有服务器呢?云是运行批处理的好地方。您可以随心所欲地扩展,并且只需为您使用的 CPU 周期付费。我想不出比批处理更好的利用云资源的方法。
然而,Java 的“一次编写,随处运行”的特性并不是 Spring 批处理方法带来的唯一灵活性。灵活性的另一个方面是能够在系统间共享代码。您可以在批处理过程中使用已经在 web 应用中测试和调试过的相同服务。事实上,能够访问曾经被锁在其他平台上的业务逻辑是迁移到这个平台的最大优势之一。通过使用 POJOs 来实现您的业务逻辑,您可以在您的 web 应用中、在您的批处理过程中使用它们——几乎可以在任何使用 Java 进行开发的地方使用它们。
Spring Batch 的灵活性还在于它能够扩展用 Java 编写的批处理过程。让我们看看扩展批处理过程的选项:
- 大型机:大型机在可扩展性方面的额外容量有限。并行完成任务的唯一真正方法是在单一硬件上并行运行完整的程序。这种方法受到以下事实的限制:您需要编写和维护代码来管理并行处理以及与之相关的困难,例如跨程序的错误处理和状态管理。此外,您受到单台机器资源的限制。
- 自定义处理:从头开始,即使是在 Java 中,也是一项令人望而生畏的任务。为大量数据获得正确的可伸缩性和可靠性是非常困难的。同样,您也面临着为负载平衡而编码的问题。当您开始跨物理设备或虚拟机进行分布时,您还会面临巨大的基础架构复杂性。你必须关心片段之间的通信是如何工作的。你还有数据可靠性的问题。当你的一个定制工人倒下时会发生什么?名单还在继续。我不是说做不到;我是说,你的时间可能更好地用于编写业务逻辑,而不是重新发明轮子。
- Java 和 Spring Batch: 虽然 Java 本身就有处理前一项中的大部分元素的工具,但是以一种可维护的方式将这些部分组合在一起是非常困难的。SpringBatch 帮你搞定了。想要在单个服务器上的单个 JVM 中运行批处理吗?没问题。您的企业正在成长,现在需要将账单计算工作分配到五台不同的服务器上,以便在一夜之间全部完成?你被保护了。数据可靠性?只需进行一些配置并记住一些关键原则,就可以完全处理事务回滚和提交计数。
正如您在深入研究 Spring Batch 框架时所看到的,困扰先前批处理选项的问题可以通过设计良好且经过测试的解决方案得到缓解。到目前为止,本章已经讨论了为您的批处理选择 Java 和开源的技术原因。然而,技术问题并不是做出这种决定的唯一原因。找到合格的开发资源来编码和维护系统的能力是很重要的。如前所述,批处理过程中的代码往往比您现在正在开发的 web 应用具有更长的生命周期。因此,找到了解相关技术的人和技术本身的能力一样重要。Spring Batch 基于非常流行的 Spring 框架。它遵循 Spring 的惯例,使用 Spring 的工具以及任何其他基于 Spring 的应用。因此,任何有 Spring 经验的开发人员都能够以最小的学习曲线掌握 Spring Batch。但是你能找到 Java,特别是 Spring 资源吗?
用 Java 做很多事情的一个理由是社区的支持。Spring 框架家族通过他们的论坛在网上拥有一个非常活跃的大型社区。该家族的 Spring Batch 项目是迄今为止 Spring 项目中发展最快的论坛之一。除此之外,还拥有强大的优势,如有需要,可以访问源代码并购买支持服务,所有支持基础都包含在此选项中。
最后你来成本。许多成本都与任何软件项目有关:硬件、软件许可、工资、咨询费、支持合同等等。然而,Spring Batch 解决方案不仅最划算,而且总体上也是最便宜的。使用商用硬件和开源操作系统和框架(Linux、Java、Spring Batch 等),唯一的经常性成本是开发工资、支持合同和基础设施——远远低于与其他选项相关的经常性许可成本和硬件支持合同。
我认为证据很清楚。使用 Spring Batch 不仅是技术上最合理的途径,而且也是最具成本效益的方法。推销已经说得够多了:让我们开始了解到底什么是 SpringBatch。
其他用途为 Spring 批
我敢打赌,现在您一定在想,Spring Batch 是否只适合替换大型机。当你考虑你正在进行的项目时,你并不是每天都在翻出 COBOL 代码。如果这就是这个框架的全部优点,那么它就不是一个非常有用的框架。然而,这个框架可以帮助您处理许多其他用例。
最常见的用例是数据迁移。当您重写系统时,通常会将数据从一种形式迁移到另一种形式。风险在于,您可能会编写出测试不佳的一次性解决方案,并且不具备常规开发所具备的数据完整性控制。然而,当你想到 Spring Batch 的特性时,它似乎是一个自然的选择。您不必编写大量代码来启动和运行一个简单的批处理作业,但是 Spring Batch 提供了提交计数和回滚功能,这些功能是大多数数据迁移应该包括的,但很少做到。
Spring Batch 的第二个常见用例是任何需要并行处理的流程。随着芯片制造商接近摩尔定律的极限,开发人员意识到继续提高应用性能的唯一方法不是更快地处理单个事务,而是并行处理更多事务。最近发布了许多有助于并行处理的框架。Apache Hadoop 的 MapReduce 实现 GridGain 和其他实现在最近几年出现,试图利用多核处理器和通过云可用的众多服务器。然而,像 Hadoop 这样的框架要求您修改代码和数据,以适应它们的算法或数据结构。Spring Batch 提供了跨多个内核或服务器扩展您的流程的能力(如图 1-1 所示,带有主/从步骤配置),并且仍然能够使用您的 web 应用使用的相同对象和数据源。
图 1-1。简化并行处理
最后,您会看到持续或 24/7 处理。在许多使用案例中,系统接收恒定或接近恒定的数据馈送。虽然接受数据的速度对于防止积压是必要的,但是当你查看数据的处理时,将数据分成一次处理的块可能更有效(如图图 1-2 所示)。Spring Batch 提供了一些工具,可以让您以一种可靠的、可伸缩的方式进行这种类型的处理。使用该框架的特性,您可以做一些事情,比如从队列中读取消息,将它们分批成块,并在一个永无止境的循环中一起处理它们。因此,您可以在高容量的情况下增加吞吐量,而不必理解从头开发这样一个解决方案的复杂细微差别。
图 1-2。批处理 JMS 处理以提高吞吐量
正如您所看到的,Spring Batch 是一个框架,虽然它是为类似大型机的处理而设计的,但可以用来简化各种开发问题。了解了 batch 是什么以及为什么应该使用 Spring Batch 之后,让我们最终开始看看框架本身。
SpringBatch 框架
Spring Batch 框架(Spring Batch)是 Accenture 和 SpringSource 合作开发的,作为一种基于标准的方法来实现常见的批处理模式和范例。
Spring Batch 实现的特性包括数据验证、输出格式化、以可重用方式实现复杂业务规则的能力,以及处理大型数据集的能力。当你仔细阅读本书中的例子时,你会发现,如果你对 Spring 很熟悉,Spring Batch 是有意义的。
让我们从框架的 30000 英尺视图开始,如图图 1-3 所示。
图 1-3。SpringBatch 建筑
Spring Batch 由分层配置的三层组成。顶部是应用层,它由所有定制代码和配置组成,用于构建您的批处理过程。您的业务逻辑、服务等等,以及如何组织作业的配置,都被视为应用。请注意,应用层并不位于核心层和基础设施层之上,而是包裹着另外两层。原因是,尽管您开发的大部分内容都是由与核心层一起工作的应用层组成的,但有时您会编写定制的基础结构,如定制的读取器和写入器。
应用层大部分时间都在与下一层——核心层——互动。核心层包含定义批处理域的所有部分。核心组件的元素包括作业和步骤接口,以及用于执行作业的接口:JobLauncher 和 JobParameters。
在这一切之下是基础设施层。为了进行任何处理,您需要读取和写入文件、数据库等等。您必须能够处理失败后重试作业时要做的事情。这些部分被认为是公共基础设施,存在于框架的基础设施组件中。
注意一个常见的误解是 Spring Batch 是或者有一个调度器。它没有。在框架中没有办法安排作业在给定的时间或基于给定的事件运行。启动作业的方式有很多种,从简单的 cron 脚本到 Quartz,甚至是像 UC4 这样的企业调度程序,但是没有一种方式是在框架本身之内。第六章介绍了如何启动一项工作。
让我们浏览一下 Spring Batch 的一些功能。
用 Spring 定义工作
批处理有许多不同的特定于领域的概念。一个任务是一个由许多步骤组成的过程。每一步都可能有输入和输出。当一个步骤失败时,它可能是也可能不是可重复的。作业的流程可能是有条件的(例如,仅当收入计算步骤返回的收入超过 1,000,000 美元时,才执行奖金计算步骤)。Spring Batch 提供了定义这些概念的类、接口和 XML 模式,使用 POJOs 和 XML 来适当地划分关注点,并以使用过 Spring 的人熟悉的方式将它们连接在一起。例如,清单 1-1 显示了一个用 XML 配置的基本 Spring 批处理作业。其结果是一个批处理框架,只需对 Spring 有一个基本的了解就可以很快上手。
清单 1-1。样本 Spring 批量作业定义
`
管理工作
能够编写一个处理一些数据一次就不再运行的 Java 程序是一回事。但是任务关键型流程需要更强大的方法。保留作业状态以供重新执行、在作业失败时通过事务管理维护数据完整性以及保存过去作业执行的性能指标以供趋势分析,这些都是您在企业批处理系统中所期望的功能。这些功能 Spring Batch 都有,默认大部分都是开启的;在开发过程中,它们只需要对性能和需求进行最小的调整。
本地和远程并行
如前所述,批处理作业的规模以及能够扩展它们的需求对于任何企业批处理解决方案都至关重要。Spring Batch 提供了以多种不同方式实现这一点的能力。从一个简单的基于线程的实现,其中每个提交时间间隔在线程池的自己的线程中处理;并行运行完整的步骤;涉及通过分区配置从远程主机获得工作单元的工写入器网格;Spring Batch 提供了一组不同的选项,包括并行块/步骤处理、远程块处理和分区。
标准化输入/输出
从具有复杂格式的平面文件、XML 文件(XML 是流式的,从不作为一个整体加载)甚至数据库中读入,或者写入文件或 XML,都可以只通过 XML 配置来完成。从代码中抽象出文件和数据库输入输出等内容的能力是 Spring Batch 中编写的作业的可维护性的一个属性。
Spring 批量管理项目
编写自己的批处理框架并不意味着必须重新开发 Spring Batch 现成的性能、可伸缩性和可靠性特性。您还需要开发某种形式的管理工具集来完成启动和停止进程之类的工作,并查看以前作业运行的统计数据。然而,如果您使用 Spring Batch,它包括所有的功能以及一个更新的附加项目:Spring Batch Admin 项目。Spring Batch Admin 项目提供了一个基于 web 的控制中心,它为您的批处理过程提供控制(比如启动一个作业,如图 1-4 所示)以及随着时间的推移监控您的过程的性能的能力。
图 1-4。Spring 批量管理项目用户界面
和 Spring 的所有特征
尽管 Spring Batch 包含了一系列令人印象深刻的功能,但最重要的是它是基于 Spring 构建的。Spring 为任何 Java 应用提供了详尽的特性列表,包括依赖注入、面向方面编程(AOP)、事务管理和大多数常见任务的模板/助手(JDBC、JMS、电子邮件等等),在 Spring 框架上构建企业批处理几乎提供了开发人员需要的一切。
如您所见,Spring Batch 为开发人员带来了很多好处。Spring 框架的成熟开发模型、可伸缩性和可靠性特性以及管理应用都可以让您使用 Spring Batch 快速运行批处理过程。
这本书是如何工作的
在了解了批处理和 Spring Batch 的内容和原因之后,我相信您已经迫不及待地想要深入研究一些代码,并了解用这个框架构建批处理是怎么回事了。第二章回顾了批处理作业的领域,定义了一些我已经开始使用的术语(作业、步骤等等),并带您建立您的第一个 Spring 批处理项目。你通过写一句“你好,世界!”来尊敬众神批处理作业,看看运行时会发生什么。
我写这本书的一个主要目标是,不仅深入研究 Spring Batch 框架是如何工作的,而且向您展示如何在一个实际的例子中应用这些工具。第三章为您在第十章中实施的项目提供需求和技术架构。
总结
本章回顾了批处理的历史。它涵盖了批处理开发人员面临的一些挑战,并证明了使用 Java 和开源技术来克服这些挑战的合理性。最后,通过研究 Spring Batch 框架的高级组件和特性,开始了对它的概述。到目前为止,您应该对所面临的情况有了一个很好的认识,并且理解 Spring Batch 中存在应对挑战的工具。现在,你需要做的就是学会如何做。我们开始吧。
二、SpringBatch 101
Java 世界充满了开源框架。每一个都有自己的学习曲线,但是当你学习大多数新的框架时,你至少理解了这个领域。例如,当您学习 Struts 或 Spring MVC 时,您可能以前开发过基于 web 的应用。有了以前的经验,将您的定制请求处理转换成给定框架处理它的方式实际上只是学习一种新语法的问题。
然而,学习一个全新领域的框架有点困难。你会遇到诸如作业、步骤和项目处理器这样的术语,就好像它们在你所处的上下文中有意义一样。事实是,它可能不会。所以,我选择这一章作为批处理 101。本章涵盖以下主题:
- 批处理的架构:这一节开始深入探究批处理的组成,并定义了在本书的其余部分将会看到的术语。
- 项目设置:我边做边学。这本书的编排方式向您展示了 Spring Batch 框架如何工作的例子,解释了它为什么会这样工作,并为您提供了编码的机会。本节涵盖了基于 Maven 的 Spring 批处理项目的基本设置。
- 你好,世界!热力学第一定律讲能量守恒。运动第一定律是关于静止物体如何保持静止,除非受到外力作用。不幸的是,计算机科学的第一定律似乎是,无论你学习什么新技术,你都必须写一句“你好,世界!”使用所述技术的程序。在这里你遵守法律。
- 运行作业:如何执行您的第一个作业可能不会立即显现出来,因此我将带您了解作业是如何执行的,以及如何传入基本参数。
- 作业结果:您可以通过查看作业的完成情况来完成。本节介绍了什么是状态,以及它们如何影响 Spring Batch 的功能。
考虑到所有这些,工作到底是什么?
批量架构
最后一章花了一些时间讨论 Spring Batch 框架的三层:应用层、核心层和基础设施层。应用层代表您开发的代码,它在很大程度上与核心层接口。核心层由构成批处理域的实际组件组成。最后,基础设施层包括项目读取器和写入器,以及解决可重启性等问题所需的类和接口。
本节将深入 Spring Batch 的架构,并定义上一章中提到的一些概念。然后,您将了解一些对批处理至关重要的可伸缩性选项,以及是什么让 Spring Batch 如此强大。最后,本章讨论了大纲管理选项以及在文档中哪里可以找到关于 Spring Batch 的问题的答案。您从批处理的架构开始,查看核心层的组件。
检查工作和步骤
图 2-1 展示了一项工作的本质。通过 XML 配置,批处理作业是按照特定顺序执行的步骤集合,作为预定义流程的一部分。让我们以用户银行账户的夜间处理为例。步骤 1 可以是加载从另一个系统接收的事务文件。第二步将所有的存款存入账户。最后,第 3 步将把所有的借方记入账户。该作业代表将交易应用到用户帐户的整个过程。
图 2-1。一个批处理作业
当您深入观察时,在单个步骤中,您会看到一个独立的工作单元,它是工作的主要组成部分。每个步骤最多有三个部分:ItemReader、ItemProcessor 和 ItemWriter。请注意,这些元素(ItemReader、ItemProcessor 和 ItemWriter)的名称都是单数。那是故意的。这些代码中的每一段都在要处理的每条记录上执行。读取器读入单个记录,并将其传递给项目处理器进行处理,然后将其发送给项目写入器以某种方式持久化。
我说过一个步骤有三个部分。一个步骤不一定要有 ItemProcessor..让一个步骤只包含一个 ItemReader 和一个 ItemWriter(在数据迁移作业中很常见)或者只包含一个 tasklet(当您没有任何数据要读取或写入时,相当于一个 ItemProcessor)是可以的。表 2-1 展示了 Spring Batch 提供的表示这些概念的接口。
表 2-1。组成批处理作业的接口
| **界面** | **描述** | | :-- | :-- | | `org.springframework.batch.core.Job` | 表示作业的对象,在作业的 XML 文件中配置。还提供执行作业的能力。 | |org.springframework.batch.core.Step
| 与作业一样,表示 XML 中配置的步骤,并提供执行步骤的能力。 |
|
org.springframework.batch.item.ItemReader<T>
| 提供输入项目能力的策略界面。 |
|
org.springframework.batch.item.ItemProcessor<T>
| 将业务逻辑应用于所提供的单个项目的工具。 |
|
org.springframework.batch.item.ItemWriter<T>
| 提供输出项目列表能力的策略界面。 |
Spring 构建作业的方式的一个优点是,它将每一步解耦到自己独立的处理器中。每一步都负责获取自己的数据,将所需的业务逻辑应用于数据,然后将数据写入适当的位置。这种分离提供了许多特性:
- 灵活性:仅仅通过修改 XML 就能改变处理顺序的能力是许多框架都在谈论的,但很少有人实现。SpringBatch 是一个交付。想想之前的银行账户例子。,如果您想在贷记之前应用借记,唯一需要的更改是重新排序作业 XML 中的步骤(第四章给出了一个例子)。您还可以跳过一个步骤,根据上一个步骤的结果有条件地执行一个步骤,甚至只需调整 XML 就可以并行运行多个步骤。
- 可维护性:由于每个步骤的代码都与之前和之后的步骤相分离,所以这些步骤很容易进行单元测试、调试和更新,而对其他步骤几乎没有影响。分离的步骤还使得在多个任务中重用步骤成为可能。正如您将在接下来的章节中看到的,steps 只不过是 Spring beans,可以像 Spring 中的任何其他 bean 一样重用。
- 可扩展性:工作中的分离步骤提供了许多选项来扩展您的工作。您可以并行执行各个步骤。您可以将一个步骤中的工作划分到多个线程中,并并行执行单个步骤的代码(您将在本章后面看到更多相关内容)。这些能力中的任何一种都可以让您满足业务的可伸缩性需求,同时对代码的直接影响最小。
- 可靠性:通过将每一步和每一步中的每一部分解耦,你可以构建作业,使它们可以在流程中的某个给定点重新启动。如果在第 3 步(共 7 步)中处理了 1000 万条记录中的 50,000 条记录后作业失败,您可以从它停止的地方重新启动它。
工作执行
让我们看看当一个作业运行时,组件及其关系会发生什么。注意图 2-2 中大多数组件共享的部分是 JobRepository。这是一个数据存储(在内存或数据库中),用于保存有关作业和步骤执行的信息。一个作业执行或 步骤执行是关于作业或步骤的单次运行的信息。在本章后面的章节和第五章中,你会看到更多关于执行和存储库的细节。
图 2-2。工作组成及其关系
运行作业从 JobLauncher 开始。JobLauncher 通过检查 JobRepository 来验证作业以前是否运行过,验证传递给作业的参数,最后执行作业。
作业和步骤的处理非常相似。一个作业遍历它被配置运行的步骤列表,执行每一个步骤。当一个项目块完成时,Spring Batch 用执行结果更新存储库中的 JobExecution 或 StepExecution。一个步骤遍历 ItemReader 读入的项目列表。当步骤处理每个项目块时,存储库中的步骤执行会随着它在步骤中的位置而更新。像当前提交计数、开始和结束时间以及其他信息都存储在存储库中。当作业或步骤完成时,相关的执行会在存储库中更新为最终状态。
Spring Batch 从版本 1 到版本 2 的变化之一是增加了分块处理。在版本 1 中,一次读入、处理和写出一条记录。这样做的问题是,它没有利用 Java 的文件和数据库 I/O 提供的批量写功能(缓冲写和批量更新)。在 Spring Batch 的版本 2 和更高版本中,框架已经更新。阅读和处理仍然是单一的操作;如果数据无法处理,就没有理由将大量数据加载到内存中。但是现在,只有在出现提交计数间隔时,才会发生写入。这允许更高性能的记录写入以及更强大的回滚机制。
并行化
一个简单的批处理的体系结构由一个单线程进程组成,该进程从头到尾依次执行一个作业的各个步骤。然而,Spring Batch 提供了许多并行化选项,您在前进的过程中应该了解这些选项。(第十一章详细介绍了这些选项。)有四种不同的方法来并行化您的工作:通过多线程步骤划分工作、完整步骤的并行执行、远程分块和分区。
多线程步骤
实现并行化的第一种方法是通过多线程步骤进行分工。在 Spring Batch 中,一个作业被配置为处理被称为 chunks 的块中的工作,在每个块之后提交一次。通常,每个块都是连续处理的。如果您有 10,000 条记录,并且提交计数设置为 50 条记录,您的作业将处理记录 1 到 50 然后提交,处理记录 51 到 100 然后提交,依此类推,直到处理完所有 10,000 条记录。Spring Batch 允许您并行执行大量工作以提高性能。有了三个线程,你可以增加三倍的吞吐量,如图图 2-3 所示。?? 1
图 2-3。多线程步骤
并行步骤
并行化的下一个方法是并行执行步骤的能力,如图 2-4 所示。假设您有两个步骤,每个步骤将一个输入文件加载到您的数据库中;但是步骤之间没有关系。在加载下一个文件之前必须等待一个文件已经加载,这有意义吗?当然不是,这就是为什么这是一个何时使用并行处理步骤能力的经典例子。
图 2-4。平行步进加工
1 这是理论上的吞吐量增加。许多因素会阻止进程实现这样的线性并行化。
远程分块
最后两种并行化方法允许您将处理分散到多个 JVM 上。在以前的所有情况下,处理都是在单个 JVM 中执行的,这会严重阻碍可伸缩性选项。当您可以跨多个 JVM 水平扩展流程的任何部分时,满足大量需求的能力就会提高。
第一个远程处理选项是远程分块。在这种方法中,使用主节点中的标准 ItemReader 执行输入;然后,输入通过一种持久通信的形式(例如 JMS)发送到一个远程从属 ItemProcessor,它被配置为消息驱动的 POJO。当处理完成时,从机将更新的项目发送回主机进行写入。因为这种方法在主设备上读取数据,在从设备上处理数据,然后再发送回来,所以需要注意的是,它可能会占用大量网络资源。这种方法适用于 I/O 成本比实际处理成本低的情况。
分区
Spring Batch 中并行化的最后一种方法是分区,如图 2-5 所示。同样,您使用主/从配置;但是这一次您不需要持久的通信方法,主服务器只作为一组从服务器步骤的控制器。在这种情况下,您的每个从属步骤都是独立的,其配置与本地部署的相同。唯一的区别是从属步骤从主节点而不是从作业本身接收工作。当所有的从机都完成了它们的工作,主步骤就被认为完成了。这种配置不需要具有保证交付的持久通信,因为 JobRepository 保证没有重复的工作,并且所有工作都已完成——不像远程分块方法,在远程分块方法中,JobRepository 不知道分布式工作的状态。
图 2-5。分区工作
批量管理
任何企业系统都必须能够启动和停止流程,监控它们的当前状态,甚至查看结果。对于 web 应用,这很容易:在 web 应用中,您可以看到您请求的每个操作的结果,而像 Google Analytics 这样的工具提供了关于您的应用如何被使用和执行的各种指标。
然而,在批处理世界中,可能有一个 Java 进程在服务器上运行了八个小时,除了日志文件和该进程正在处理的数据库之外,没有任何输出。这种情况很难控制。出于这个原因,Spring 开发了一个名为 Spring Batch Admin 的 web 应用,它允许您启动和停止作业,并提供每个作业执行的详细信息。
文档
Spring Batch 的优势之一是真正的开发人员编写了它,他们拥有在各种企业中开发批处理的经验。从这一经历中不仅得到了一个全面的框架,还得到大量的文档。Spring Batch 网站包含了我曾经工作过的开源项目的最好的文档集合之一。除了正式文档,JavaDoc 对于 API 细节也很有用。最后,Spring Batch 提供了 19 个不同的示例作业,供您在开发自己的批处理应用时参考(参见表 2-2 )。
表 2-2。样本批处理作业
| **批处理作业** | **描述** | | :-- | :-- | | adhocLoopJob | 一个无限循环,用于演示通过 JMX 公开元素以及在后台线程(而不是主 JobLauncher 线程)中运行作业。 | | beanwrappermapperssamplejob | 一种包含两个步骤的作业,用于演示文件字段到域对象的映射以及基于文件的输入的验证。 | | compositeItemWriterSampleJob | 一个步骤只能有一个读取器和写入器。CompositeWriter 是解决这个问题的方法。这个示例作业演示了如何操作。 | | 客户过滤作业 | 使用 ItemProcessor 过滤掉无效的客户。该作业还会更新步骤执行的过滤器计数字段。 | | 委派工作 | 使用 ItemReaderAdapter,将输入的读取委托给 POJO 的已配置方法。 | | 足球工作 | 足球统计工作。在加载两个输入文件(一个包含玩家数据,另一个包含游戏数据)后,该作业为玩家和游戏生成一组摘要统计信息,并将它们写入日志文件。 | | groovyJob | 使用 Groovy(一种动态 JVM 语言)编写文件的解压缩脚本。 | | headerFooterSample 示例 | 使用回调,添加了在输出中呈现页眉和页脚的功能。 | | 休眠作业 | 默认情况下,Spring 批处理读取器和写入器不使用 Hibernate。这份工作展示了如何将 Hibernate 集成到您的工作中。 | | 无限循环作业 | 只是一个无限循环的作业,用于演示停止和重启场景 | | 广泛的工作 | 提供了许多不同 I/O 选项的示例,包括带分隔符和固定宽度的文件、多行记录、XML、JDBC 和 iBATIS 集成。 | | jobSampleJob | 演示从另一个作业执行一个作业。 | | loopFlowSample | 使用 decision 标记,演示如何以编程方式控制执行流。 | | 邮件作业 | 使用 SimpleMailMessageItemWriter 将电子邮件作为每个项目的输出形式发送。 | | 多重作业 | 将文件记录组视为代表单个项目的列表。 | | 多重排序 | 作为多行输入概念的扩展,使用自定义读取器读取包含多行嵌套记录的文件。使用标准编写器,输出也是多行的。 | | 平行作业 | 将记录读入临时表,多线程步骤在临时表中处理这些记录。 | | 分区文件作业 | 使用 MultiResourcePartitioner 并行处理文件集合。 | | 分区 JdbcJob | 不是查找多个文件并并行处理每个文件,而是划分数据库中的记录数进行并行处理。 | | restartSampleJob | 处理开始时抛出一个假异常,以展示重新启动出错的作业并从停止的地方重新开始的能力。 | | 重新取样 | 使用一些有趣的逻辑,展示了 Spring Batch 如何在放弃并抛出错误之前多次尝试处理一个项目。 | | skipSampleJob | 基于 tradeJob 示例。但是,在此作业中,有一条记录未通过验证并被跳过。 | | 任务作业 | Spring Batch 最基本的用途是微线程。此示例显示了如何通过 MethodInvokingTaskletAdapter 将任何现有方法用作微线程。 | | 贸易工作 | 模拟真实世界的场景。这一分三步的工作将交易信息导入数据库,更新客户账户,并生成报告。 |项目设置
到目前为止,您已经了解了为什么要使用 Spring Batch,并研究了框架的组件。然而,看图表和学习新的行话只会让你到此为止。在某些时候,你需要钻研代码:所以,拿起编辑器,让我们开始钻研吧。
在本节中,您将构建您的第一个批处理作业。您将逐步完成 Spring 批处理项目的设置,包括从 Spring 获取所需的文件。然后,您配置一个作业并编写代码“Hello,World!”SpringBatch 版本。最后,您将学习如何从命令行启动批处理作业。
获得 SpringBatch
在开始编写批处理流程之前,您需要获得 Spring Batch 框架。有三种方法可以做到这一点:使用 SpringSource 工具套件(STS),下载 zip 发行版,或者使用 Maven 和 Git。
使用 SpringSource 工具套件
SpringSource(Spring 框架及其所有衍生物的维护者)已经将一个 Eclipse 发行版与一组专门为 Spring 开发设计的插件放在了一起。特性包括创建 Spring 项目、XML 文件和 beans 的向导,远程部署应用的能力,以及 OSGi 管理。你可以从 SpringSource 网站下载。
下载 Zip 发行版
Spring Batch 框架也可以通过从 SpringSource 网站下载 zip 文件获得,有两个选项:所有依赖项或无依赖项(如文件名中的-无依赖项所示)。鉴于该项目是为 Maven 使用而设置的(尽管为使用 Ant 的人提供了一个 build.xml 文件),下载无依赖性选项是一个更好的选择。
zip 文件包含两个目录:dist 和 samples。dist 包含发布 jar 文件:两个用于核心,两个用于基础设施,两个用于测试(各有一个源代码和编译)。在 samples 目录中,您可以找到一个 samples 项目(spring-batch-samples ),其中包含了您在本章前面看到的所有示例批处理作业;一个项目 shell (spring-batch-simple-cli ),可用作任何 spring 批处理项目的起点;以及两者的 Maven 父项目。这个模板项目是您开始使用 Spring Batch 的最简单的方式,并且将是您构建我们的项目前进的方式。
从 Git 结账
获取 Spring Batch 代码的最后一种方法是从 SpringSource 使用的源代码库 Github 中获取。Git 版本控制系统是一个分布式版本控制系统,它允许您在本地使用存储库的完整副本..
清单 2-1。从 Github 查看项目
$ git clone git://github.com/SpringSource/spring-batch.git
该命令导出 Spring Batch 项目的源代码,包括项目的外壳、示例应用和所有 Spring Batch 框架的源代码。清单 2-1 中的命令将获得整个 Spring 批处理 Git 库。为了获得一个特定的版本,从你的检出库中执行清单 2-2 中的命令。
清单 2-2。获取 Spring Batch 的特定版本
$ git checkout 2.1.7.RELEASE
配置 Maven
为了在您的构建中使用 Maven,您需要稍微调整一下本地 Maven 安装。作为 Spring 项目下载发行版的一部分提供的项目对象模型(POM)文件中没有配置 Spring Maven 存储库。因此,您应该将它们添加到 settings.xml 文件中。清单 2-3 显示了您需要的附加配置。
清单 2-3。从 SVN 获取仓库 DDL
<pluginRepositories> <pluginRepository> <id>com.springsource.repository.bundles.release</id> <name>SpringSource Enterprise Bundle Repository</name> <url>http://repository.springsource.com/maven/bundles/release</url> </pluginRepository> </pluginRepositories>
创建了项目 shell 并配置了 Maven 之后,您可以通过运行一个快速的mvn clean install
来测试配置。构建成功后,您就可以开始第一个批处理作业了。
是法律:你好,世界!
计算机科学的法则很清楚。任何时候你学习一项新技术,你必须创造一个“你好,世界!”程序使用所说的技术,所以让我们开始吧。不要觉得你需要理解这个例子的所有活动部分。未来的章节将更详细地讨论每一部分。
在深入研究新代码之前,您应该清理一些不需要的文件和对它们的引用。这些文件虽然是作为示例提供的,但并不保存在典型的 Spring 批处理项目中。首先,我们可以删除所有的 java 源代码和测试。它们位于 src/main/java 和 src/test/java 目录中。一旦删除了这些,我们就可以删除 module-context.xml 文件了。这是一个示例作业配置,您的项目中不需要它。最后,因为您删除了项目配置中引用的几个 java 文件,所以也需要更新。在 src/main/resources/launch-context . XML 文件中,您需要删除 module-context.xml 顶部的导入以及文件底部的 dataSourceInitializer bean。dataSourceIntializer 将在第十二章中进一步讨论。
如前所述,作业是用 XML 配置的。创造你的“你好,世界!”job,在 src/main/resources 中新建一个名为 jobs 的目录;在新目录中,创建一个名为 helloWorld.xml 的 XML 文件,如清单 2-4 所示。
清单 2-4。那个“你好,世界!”工作
`
<beans:beans xmlns ="http://www.springframework.org/schema/batch"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="helloWorld"
class="com.apress.springbatch.chapter2.HelloWorld"/>
</beans:beans>`
如果这看起来有点眼熟,它应该。这是前面讨论过的高级分解,只是以 XML 的形式。
注意尽管大多数 Spring 都为 XML 配置选项添加了注释等价物,但 Spring Batch 没有。作为 2.0 版本的一部分,Spring 添加了一个名称空间来帮助管理 XML。
如果您完成了这个过程,有四个主要部分:launch-context.xml 的导入、bean 声明、步骤定义和作业定义。Launch-context.xml 是一个包含在 shell 项目中的文件,该项目包含许多为您的作业配置的基础设施。像 datasource、JobLauncher 和项目中所有作业通用的其他元素都可以在这里找到。第三章更详细地介绍了这个文件。目前,默认设置有效。
bean 声明应该看起来像任何其他 Spring bean,这是有原因的:它就像任何其他 Spring bean 一样。HelloWorld bean 是一个小任务,它完成这项工作。一个小任务是一种特殊类型的步骤,用于在没有读取器或写入器的情况下执行一个功能。通常,一个微线程用于一个单一的功能,比如执行一些初始化,调用一个存储过程,或者发送一封电子邮件通知您任务已经完成。第四章讲述了微线程和其他步骤类型的语义细节。
下一块是台阶。如前所述,作业由一个或多个步骤组成。在 HelloWorld 作业中,您从执行您的小任务的单个步骤开始。Spring Batch 提供了一种使用批处理 XSD 配置步骤的简单方法。您使用 tasklet 标记创建一个小任务,并引用您之前定义的小任务。然后将它包装在一个只有 id 的步骤标签中。这定义了一个可重用的步骤,您可以根据需要在工作中多次引用它。
最后,你定义你的工作。这项工作实际上只不过是要执行的步骤的有序列表。在这种情况下,你只有一步。如果您想知道作业定义中的步骤标记是否与您在作业定义中使用的标记类型相同,那么它是相同的。如果愿意,您可以内联声明这些步骤。但是,在本例中,我在作业之外创建了一个步骤,并将其作为作业内步骤的父步骤。 2 我这样做有两个原因:保持 XML 的整洁,并且如果需要的话,可以方便地将步骤提取到其他 XML 文件中。您将在以后的章节中看到,XML for 步骤会变得非常冗长;这里展示的方法有助于保持作业的可读性。
您的作业已经配置好了,但是您的配置中有一个不存在的类:HelloWorld tasklet。在 src/main/Java/com/a press/spring batch/chapter 2 目录中创建 tasklet。正如您所猜测的,代码非常简单;参见清单 2-5 。
清单 2-5。 HelloWorld 小任务
`package com.apress.springbatch.chapter2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, world!";
public RepeatStatus execute( StepContribution arg0, ChunkContext arg1 ) throws Exception
{
System.out.println( HELLO_WORLD );
return RepeatStatus.FINISHED;
}
}`
要创建 HelloWorld tasklet,您需要实现 tasklet 接口的单一方法:execute。StepContribution 和 ChunkContext 表示该小任务正在执行的步骤(提交计数、跳过计数等)的上下文。未来的章节将更详细地讨论这些问题。
管理你的工作
真的是这样。让我们尝试构建和运行作业。要编译它,从项目的根目录运行mvn clean compile
。当构建成功时,运行作业。Spring Batch 自带了名为 CommandLineJobRunner 的作业运行器。正如您所猜测的,它是打算从…命令行运行的!在本书中,您将从项目的目标目录中执行作业,这样就不需要设置类路径。CommandLineJobRunner 接受两个或更多参数:包含作业配置的 XML 文件的路径、要执行的作业的名称和作业参数列表。对于 HelloWorldJob,只需传递前两个参数。要执行作业,运行清单 2-6 中的命令。
清单 2-6。执行 HelloWorld 作业
java -jar hello-world-0.0.1-SNAPSHOT.jar jobs/helloWorld.xml helloWorldJob
2 第四章详细介绍了步骤的父属性。
运行完作业后,请注意,在传统的 Spring 风格中,一个简单的“Hello,World!”但是如果您仔细观察(在输出的第 33 行周围),就会发现:
`2010-12-01 23:15:42,442 DEBUG
org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.scope.context.StepContextRepeatCallback] -
Hello, world!
2010-12-01 23:15:42,443 DEBUG
org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.step.tasklet.TaskletStep] - <Applying contribution:
[StepContribution: read=0, written=0, filtered=0, readSkips=0, writeSkips=0, processSkips=0, exitStatus=EXECUTING]>`
恭喜你!你刚刚运行了你的第一个 Spring 批处理作业。那么,到底发生了什么?正如本章前面所讨论的,当 Spring Batch 运行一个作业时,作业运行器(在本例中是 CommandLineJobRunner)加载要运行的作业的应用上下文和配置(由传入的前两个参数指定)。从那里,作业运行器将 JobInstance 传递给执行作业的 JobLauncher。在这种情况下,执行作业的单个步骤,并相应地更新 JobRepository。
探索作业知识库
等等。JobRepository?这在你的 XML 中没有说明。这些信息都去哪里了?它进入了作业存储库,这是应该的。问题是 Spring Batch 被配置为默认使用 HSQLDB,所以所有这些元数据虽然在作业执行期间存储在内存中,但现在都不见了。让我们通过切换到 MySQL 来解决这个问题,这样您就可以更好地管理元数据,并看看当您运行作业时会发生什么。在这一节中,您将了解如何配置 JobRepository 来使用 MySQL,并通过运行 HelloWorldJob 探索 Spring Batch 向数据库中记录了什么。
作业储存库配置
要更改 Spring Batch 存储数据的位置,需要做三件事:更新 batch.properties 文件,更新 pom,并在数据库中创建批处理模式。 3 让我们首先修改位于项目的/src/main/resources 目录中的 batch.properties 文件。属性应该非常简单。清单 2-7 显示了我的清单中的内容。
清单 2-7。 batch.properties 文件
`batch.jdbc.driver=com.mysql.jdbc.Driver
batch.jdbc.url=jdbc:mysql://localhost:3306/spring_batch_test
use this one for a separate server process so you can inspect the results
(or add it to system properties with -D to override at run time).
batch.jdbc.user=root
batch.jdbc.password=p@ssw0rd
batch.schema=spring_batch_test
batch.schema.script=schema-mysql.sql`
3 我假设你已经安装了 MySQL。如果没有,请前往[www.mysql.com](http://www.mysql.com)
下载并获取安装说明。
请注意,我注释掉了 batch.schema.script 行。当您运行作业时,dataSourceIntializer 会执行指定的脚本。当您从事开发工作时,这很有帮助,但是如果您想要持久化数据,这就没那么有用了。
现在属性文件指向 MySQL 的本地实例,您需要更新 POM 文件,以便在类路径中包含 MySQL 驱动程序。为此,找到 HSQLDB 依赖项,并如清单 2-8 所示更新它。
清单 2-8。 Maven MySQL 依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.3</version> </dependency>
在这个依赖关系中,5.1.3 是 MySQL 在本地运行的版本。
配置好数据库连接后,Spring Batch 需要您创建模式。使用 MySQL,您可以创建如清单 2-9 所示的模式。
清单 2-9。创建数据库模式
mysql> create database spring_batch_test; Query OK, 1 row affected (0.00 sec) mysql> use spring_batch_test; Database changed mysql> source ~/spring_batch/src/main/resources/org/springframework/batch/core/schema-mysql.sql
就这样。让我们再次运行作业(确保首先执行mvn clean compile
,将更新后的 batch.properties 文件复制到目标)。使用与前面相同的命令,您应该会看到相同的输出。不同的是,这一次,SpringBatch 留下了一些东西。我们来看看数据库。
作业知识库表
Spring Batch 使用数据库来维护单次执行期间以及执行之间的状态。记录了有关作业实例、传入的参数、执行结果以及每个步骤的结果的信息。下面是作业存储库中的六个表;下面几节描述它们的关系: 4
使用 MySQL 和其他一些数据库的用户可能会看到另外三个“表”:batch_job_execution_seq、batch_job_seq 和 batch_step_execution_seq。这些用于维护数据库顺序,这里不讨论。
- 批处理 _ 作业 _ 实例
- 批处理作业参数
- 批处理 _ 作业 _ 执行
- 批处理作业执行上下文
- 批处理 _ 步骤 _ 执行
- 批处理 _ 步骤 _ 执行 _ 上下文
批处理 _ 作业 _ 实例
让我们从 BATCH_JOB_INSTANCE 表开始。如前所述,在创建作业时会创建一个作业实例。这就像给新来的打电话一样。但是,作业实例实际上是作业实例本身和作业参数(存储在 BATCH_JOB_PARAMS 表中)的组合。此组合只能执行一次才能成功。让我再说一遍:一个作业只能用相同的参数运行一次。我不会长篇大论地解释为什么我不喜欢这个特性,但是我要说的是,通常将运行的日期和时间作为作业参数来传递,以避开这个问题。运行 HelloWorld 作业后,BATCH_JOB_INSTANCE 表看起来类似于表 2-3 中所示。
表 2-3。批处理 _ 作业 _ 实例表
| **字段** | **描述** | **值** | | :-- | :-- | :-- | | 作业实例标识 | 表的主键 | one | | 版本 | 版本 5 备案 | Zero | | 作业名称 | 执行的作业的名称 | helloWorldJob | | 驾驶 | 用于唯一标识作业实例的作业名称和参数的散列 | d 41 D8 CD 98 f 00 b 204 e 980098 ECF 8427 e |批处理作业参数
BATCH_JOB_PARAMS 表包含传递给作业的所有参数,这不足为奇。如前一节所述,参数是 Spring Batch 用来标识作业运行的一部分。在这种情况下,BATCH_JOB_PARAMS 表为空,因为您没有向作业传递任何参数。然而,BATCH_JOB_PARAMS 表中的字段显示在表 2-4 中。
要了解更多关于领域驱动设计的版本和实体,请阅读 Eric Evans (Addison-Wesley,2003)的领域驱动设计。
表 2-4。批处理 _ 作业 _ 参数表
| **字段** | **描述** | | :-- | :-- | | 作业实例标识 | BATCH_JOB_INSTANCE 表的外键 | | 类型 _CD | 存储的值的类型(字符串、日期、长整型或双精度型) | | KEY_NAME | 参数键(作业参数作为键/值对传入) | | STRING_VAL | 如果参数类型是字符串,则为值 | | 日期 _VAL | 日期参数 | | 长 _VAL | 长参数 | | 双精度浮点型 | 双精度或浮点参数 |批处理 _ 作业 _ 执行和批处理 _ 步骤 _ 执行
创建作业实例后,它将被执行。作业执行的状态保存在 BATCH_JOB_EXECUTION 表中,您猜对了。开始时间、结束时间和上次执行的结果都存储在这里。我知道你在想什么:如果一个参数相同的作业只能运行一次,那么 BATCH_JOB_EXECUTION 表有什么意义呢?作业和参数的组合只能运行一次才能成功。如果一个作业运行并且失败(假设它被配置为能够重新运行),它可以根据需要再次运行任意多次,直到它成功。当处理超出您控制的数据时,这在批处理世界中是很常见的。当作业处理数据时,它可以找到导致进程抛出错误的坏数据。有人修复了数据并重新启动了作业。
BATCH_STEP_EXECUTION 表的作用与 BATCH_JOB_EXECUTION 表相同。BATCH_STEP_EXECUTION 中维护开始时间、结束时间、提交次数以及与步骤状态相关的其他参数。
执行 HelloWorld 作业后,BATCH_JOB_EXECUTION 表中只有一条记录。注意表 2-5 中的时间都是一样的:因为System.out.println(HELLO_WORLD);
不需要很长时间。
表 2-5。批处理 _ 作业 _ 执行表
| **字段** | **描述** | **值** | | :-- | :-- | :-- | | 作业执行标识 | 表的主键 | one | | 版本 | 记录的版本 | Two | | 作业实例标识 | BATCH_JOB_INSTANCE 表的外键 | one | | 创建时间 | 创建作业执行的时间 | 2010-10-25 18:08:30 | | 开始时间 | 作业执行的开始时间 | 2010-10-25 18:08:30 | | 结束时间 | 不管成功与否,执行的结束时间 | 2010-10-25 18:08:30 | | 状态 | 返回到工单的状态 | 完成 | | 退出代码 | 返回给作业的退出代码 | 完成 | | 退出 _ 消息 | 返回给作业的任何退出消息 | | | 上次更新时间 | 上次更新此记录的时间 | 2010-10-25 18:08:30 |您的 BATCH_STEP_EXECUTION 表也只包含一条记录,因为您的作业只有一个步骤。表 2-6 列出了执行后表格中的列和值。
作业和步骤执行上下文表
剩下两个上下文表,BATCH_JOB_EXECUTION_CONTEXT 和 BATCH_STEP_EXECUTION_CONTEXT。这些表是与作业或步骤相关的 ExecutionContext 的持久版本。ExecutionContext 是 Spring 批处理,类似于 web 应用中的 servlet 上下文或会话,因为它是存储信息的全局位置。它本质上是一个键/值对的映射,其范围要么是作业,要么是步骤。作业或步骤执行上下文用于在给定范围内传递信息;对于作业,它用于在步骤之间传递信息,对于步骤,它用于在多个记录的处理过程中传递信息。
表 BATCH_JOB_EXECUTION_CONTEXT 和 BATCH_STEP_EXECUTION_CONTEXT 是这些映射的序列化版本。在这种情况下,它们都包含相同的数据,只有外键(这是表的主键)不同(BATCH_STEP_EXECUTION_CONTEXT 引用到 BATCH_STEP_EXECUTION 表,BATCH_JOB_EXECUTION_CONTEXT 引用 BATCH_JOB_EXECUTION 表)。表 2-7 显示了表格包含的内容。
表 2-7。批处理 _ 作业 _ 执行 _ 上下文和批处理 _ 步骤 _ 执行 _ 上下文表
| **字段** | **描述** | **值** | | :-- | :-- | :-- | | 作业执行标识/步骤执行标识 | 批处理作业执行/批处理步骤执行表的外键 | one | | 简短上下文 | 上下文的字符串表示形式 | {"map":""} | | 序列化上下文 | 供将来重试时使用的序列化执行上下文,依此类推 | 空 |总结
在这一章中,你用 Spring Batch 弄湿了你的脚。您浏览了 batch 域,涵盖了什么是作业和步骤,以及它们如何通过作业存储库进行交互。您了解了该框架的不同特性,包括在 XML 中映射批处理概念的能力、健壮的并行化选项、正式文档(包括可用示例作业的列表)以及管理应用 Spring Batch Admin。
从那里,你写出了 SpringBatch 版的《你好,世界!。您了解了获取 Spring Batch 框架的不同方法,包括从 g it 中检出它、使用 SpringSource 工具套件以及下载 zip 发行版。当您设置好项目后,您用 XML 创建了作业,编写了一个小任务,并执行了作业。最后,您探索了 Spring Batch 用来维护其运行的作业信息的作业存储库。
我想指出的是,您几乎没有看到 Spring Batch 能做什么。下一章将介绍一个示例应用的设计,您将在本书的后面部分构建该应用,并概述 Spring Batch 如何解决您在没有它的情况下必须自己处理的问题。
三、示例作业
这本书不仅解释了 Spring Batch 的许多特性是如何工作的,还详细演示了它们。每章都包括一些例子,展示每个特性是如何工作的。然而,旨在传达单个概念和技术的示例可能不是展示这些技术如何在真实世界的示例中协同工作的最佳方式。因此,在第十章中,你创建了一个用来模拟真实场景的示例应用。
我选择的场景很简单:一个你很容易理解的领域,但是它提供了足够的复杂性,所以使用 Spring Batch 是有意义的。银行对账单是常见批处理的一个例子。这些流程每晚运行,根据上个月的交易生成报表。这个例子是标准银行对账单的一个衍生物:经纪对账单。经纪对账单批处理流程展示了如何将 Spring Batch 的以下功能结合使用来实现结果:
各种输入和输出选项:Spring Batch 最重要的特性之一是从各种来源读取和写入的良好抽象的选项。代理语句从平面文件、数据库和 web 服务获得输入。在输出端,您可以写入数据库和平面文件。利用了各种各样的读取器和写入器。
错误处理:维护批处理过程最糟糕的部分是当它们中断时,通常是在凌晨 2:00,而你是接到电话来解决问题的人。因此,健壮的错误处理是必须的。示例语句过程涵盖了许多不同的场景,包括日志记录、跳过有错误的记录和重试逻辑。
可伸缩性:在现实世界中,批处理过程需要能够容纳大量数据。在本书的后面,您将使用 Spring Batch 的可伸缩性特性来调整批处理过程,以便它可以处理数百万个客户。
为了构建我们的批处理作业,我们需要一组工作需求。因为我们将使用用户故事来定义我们的需求,所以我们将在下一节中从整体上来看敏捷开发过程。
了解敏捷开发
在本章深入探讨你在第十章中开发的批处理过程的个别需求之前,让我们花点时间回顾一下你使用的方法。在我们的行业中,关于各种敏捷过程已经说了很多;因此,不要指望你以前对主题的任何了解,让我们从建立敏捷和开发过程对本书的意义开始。
敏捷过程有 12 条原则,实际上它的所有变体都规定了这些原则。它们如下:
- 客户满意来自工作软件的快速交付。
- 无论发展到什么阶段,变化都是受欢迎的。
- 经常交付工作软件。
- 商业和发展必须每天携手合作。
- 与积极的团队一起构建项目。给他们工具,相信他们能完成工作。
- 面对面的交流是最有效的形式。
- 工作软件是衡量成功的第一标准。
- 努力实现可持续发展。团队的所有成员都应该能够无限期地保持开发速度。
- 继续追求卓越的技术和优秀的设计。
- 通过消除不必要的工作来减少浪费。
- 自组织团队产生最好的需求、架构和设计。
- 定期让团队反思,以确定如何改进。
不管你是在使用极限编程(XP)、Scrum 还是任何其他当前流行的变体。关键是这十几条原则仍然适用。
请注意,并不是所有的都适用于你的情况。和一本书面对面工作很难。你可能会独自完成这些例子,所以团队激励方面也不完全适用。然而,有些部分确实适用。一个例子是工作软件的快速交付。这会驱使你读完这本书。您将通过构建应用的小部分,验证它们与单元测试一起工作,然后添加到它们上面来完成它。
即使有例外,敏捷的原则也为任何开发项目提供了一个坚实的框架,本书尽可能多地应用了这些原则。让我们通过检查您记录样例工作:用户故事的需求的方式来开始了解它们是如何应用的。
用用户故事捕捉需求
用户故事是记录需求的敏捷方法。作为客户对应用应该做什么的看法,故事的目标是传达用户将如何与系统交互,并记录交互的可测试结果。用户故事有三个主要部分:
- 标题:标题应该简单明了地陈述故事的内容。加载交易文件。计算费用等级。生成打印文件。所有这些都是故事标题的好例子。您会注意到这些标题不是特定于 GUI 的。仅仅因为你没有 GUI 并不意味着你不能在用户之间进行交互。在这种情况下,用户是您正在记录的批处理过程或您与之交互的任何外部系统。
- 叙述:这是对你所记录的交互的简短描述,从用户的角度来写。通常,格式类似于“给定情况 Y,X 做了一些事情,然后发生了其他事情。”在接下来的章节中,您将看到如何处理批处理的故事(假设它们本质上是纯技术性的)。
- 验收标准:验收标准是可测试的需求,可以用来识别一个故事何时完成。前面陈述中的重要词是可测试的。为了使验收标准有用,它必须能够以某种方式进行验证。这些不是主观的需求,而是开发人员可以用来说“是的,它确实这样做了”或“不,它没有”的硬性要求
让我们看一个通用遥控器的用户故事作为例子:
- 标题:打开电视
- 叙述:作为用户,在关闭电视、接收器和有线电视盒的情况下,我将能够按下我的通用遥控器上的电源按钮。然后,遥控器将打开电视、接收器和有线电视盒,并对它们进行配置以观看电视节目。
- 验收标准:
- 在万能遥控器上有一个电源按钮。
- 当用户按下电源按钮时,将发生以下情况:
- 电视机将打开电源。
- AV 接收器将通电。
- 有线电视盒将通电。
- 有线电视盒将被设置到频道 187。
- AV 接收器将被设置为 SAT 输入。
- 电视机将被设置为视频 1 输入。
“打开电视”用户故事以简短的描述性标题“打开电视”开始。它继续叙述。在这种情况下,叙述提供了当用户按下电源按钮时会发生什么的描述。最后,验收标准列出了开发人员和 QA 的可测试需求。请注意,每个标准都是开发人员可以轻松检查的:他们可以看着他们开发的产品说是或不是,他们写的东西是否符合标准。
用户故事与用例
用例是需求文档的另一种常见形式。类似于用户故事,它们以演员为中心。用例是 Rational 统一过程(RUP)选择的文档形式。它们旨在记录参与者和系统之间交互的每个方面。正因为如此,他们过度以文档为中心的焦点(为了文档而写文档),以及他们臃肿的格式,用例已经失宠,在敏捷开发中被用户故事所取代。
用户故事标志着开发周期的开始。让我们继续看看在这个周期的剩余时间里使用的一些其他工具。
用测试驱动的开发来捕获设计
测试驱动开发(TDD)是另一种敏捷实践。当使用 TDD 时,开发人员首先编写一个失败的测试,然后实现代码使测试通过。TDD(也称为测试优先开发)旨在要求开发人员在编码之前考虑他们试图编码什么,已经被证明可以使开发人员更有效率,更少地使用他们的调试器,并最终得到更干净的代码。
TDD 的另一个优点是测试可以作为可执行的文档。与由于缺乏维护而变得陈旧的用户故事或其他形式的文档不同,自动化测试总是作为代码持续维护的一部分进行更新。如果您想了解一段代码是如何工作的,您可以查看单元测试,了解开发人员打算使用他们的代码的场景的完整情况。
尽管 TDD 有许多优点,但在本书中你不会经常用到它。这是一个很好的开发工具,但它不是解释事物如何工作的最佳工具。然而,第十二章着眼于所有类型的测试,从单元测试到功能测试,使用开源工具包括 JUnit、Mockito 和 Spring 中的测试附件。
使用源代码控制系统
在第二章中,当您使用 Git 检索 Spring Batch 的源代码时,您快速浏览了一下源代码控制。尽管这不是一个必要条件,但是强烈建议您在所有的开发中使用源代码控制系统。无论您选择建立一个中央 Subversion 存储库还是在本地使用 Git,源代码控制提供的特性对于高效的编程都是必不可少的。
你可能在想,“为什么我要对那些我在学习时要扔掉的代码使用源代码控制?”这是我能想到的使用它的最强有力的理由。通过使用版本控制系统,你给自己一个安全网去尝试一些事情。提交您的工作代码;尝试一些可能不起作用的东西。如果是,提交新的修订。如果没有,回滚到前一个版本,不会造成任何伤害。想一想你最近一次在没有版本控制的情况下学习新技术的情形。我敢肯定,有时你会沿着一条没有成功的道路编码,然后因为没有先前的工作副本而不得不调试。通过使用版本控制,你可以在一个受控的环境中避免犯错误。
使用真实的开发环境
在敏捷环境中,还有许多其他方面需要开发。给自己找个好主意。因为这本书是有意为 IDE 不可知论者而写的,所以它不会深入讨论每本书的优点和缺点。但是,一定要有一个好的,并且学好,包括键盘快捷键。
虽然在学习某项技术时,花费大量时间建立持续集成环境对您来说可能没有意义,但为了您的个人发展,建立一个用于一般用途的环境可能是值得的。你永远不知道你正在开发的小部件什么时候会成为下一件大事,当事情开始变得令人兴奋时,你会讨厌不得不回去设置源代码控制和持续集成等。有几个不错的持续集成系统是免费的,但是我强烈推荐 Hudson(或者它的兄弟 Jenkins)。它们都易于使用且高度可扩展,因此您可以配置各种附加功能,包括与 Sonar 和其他代码分析工具集成以及执行自动化功能测试。
理解陈述工作的要求
现在您已经看到了在学习 Spring Batch 时鼓励您使用的开发过程的各个部分,让我们看看您将在本书中开发什么。图 3-1 显示了你期望每个季度从你的股票经纪人那里收到的作为你的经纪账户对账单的邮件。
图 3-1。经纪声明,格式化并打印在信笺上
如果你分解一下这个陈述是如何产生的,实际上有两个部分。第一张不过是一张漂亮的纸,第二张就印在上面。这是你在本书中创作的第二件作品,如图 3-2 所示。
图 3-2。纯文本经纪声明
通常,语句创建如下。批处理会创建一个仅包含文本的打印文件。然后,打印文件被发送到打印机,打印机将文本打印到装饰纸上,生成最终报表。打印文件是您使用 Spring Batch 创建的部分。您的批处理将执行以下功能:
- 导入客户信息和相关交易的文件。
- 从 web 服务中检索数据库中所有股票的收盘价格。
- 将先前下载的股票价格导入数据库。
- 计算每个账户的定价水平。
- 根据上一步计算的水平,计算每笔交易的交易费。
- 打印上个月经纪账户的文件。
让我们来看看这些特性都需要什么。为您的工作提供了一个客户事务平面文件,其中包含有关客户及其当月事务的信息。您的作业更新现有的客户信息,并将他们的交易添加到数据库中。当交易被导入后,作业从 web 服务获取数据库中每只股票的最新价格,以便计算每个账户的当前价值。该作业将下载的价格导入数据库。
初始导入完成后,您的工作可以开始计算交易费用。经纪公司通过对每笔交易收取费用来赚钱。这些费用是基于客户一个月的交易量。客户的交易越多,每次交易收取的费用就越少。计算交易费用的第一步是确定用户属于哪个级别或阶层;然后,您可以计算客户交易的价格。当所有的计算都完成后,就可以生成用户的月结单了。
这个特性列表旨在提供一个完整的视图,展示 Spring Batch 在现实问题中是如何使用的。在整本书中,您将了解 Spring Batch 提供的特性,这些特性可以帮助您开发类似于这个场景所需的批处理过程。在第十章中,您实现了批处理作业,以满足以下用户案例中概述的需求:
| **名称** | **必需的** | **格式** | | :-- | :-- | :-- | | 客户税务 ID | 真实的 | \d{9} | | 客户名字 | 错误的 | \w+ | | 客户姓氏 | 错误的 | \w+ | | 客户地址 1 | 错误的 | \w+ | | 客户城市 | 错误的 | \w+ | | 客户状态 | 错误的 | [A-Z]{2} | | 客户邮政编码 | 错误的 | \d{4} | | 客户账号 | 错误的 | \d{16} |导入交易:作为批处理,我会将客户信息及其相关交易导入到数据库中,以供将来处理。验收标准:
批处理作业将预定义的客户/交易文件导入数据库表格。
文件导入后,将被删除。
客户/交易文件将有两种记录格式。首先是识别后续交易所属的客户。第二个是个人交易记录。
客户记录的格式是以下字段的逗号分隔记录:
客户记录将如下所示:
205866465,Joshua,Thompson,3708 Park,Fairview,LA,58517,3276793917668488
| **名称** | **必需的** | **格式** | | | :-- | :-- | :-- | :-- | | 客户账号 | 真实的 | \d{16} | | | 股票代码 | 真实的 | \w+ | | | 真实数量 | | \d+ | | | 真实价格 | | \d+\。\d{2} | | | 交易时间戳 | True MM\DD\YYYY | 时:分:秒 |
交易记录的格式为以下字段的逗号分隔记录:
事务记录如下所示:
3276793917668488,KSS,5767,7074247,2011-04-02 07:00:08
| **名称** | **必需的** | **格式** | | :-- | :-- | :-- | | 股票代码 | 真实的 | \w+ | | 收盘价格 | 真实的 | \d+\。\d{,2} |所有交易将作为新交易导入。
将创建一个包含无效客户记录的错误文件。
任何无效的交易记录将与客户记录一起写入错误文件
获取股票收盘价:A 在批处理过程中,在预定的执行时间,我将查询 Yahoo stock web 服务以获取我们的客户在上个月持有的所有股票的收盘价。我将用这些数据构建一个文件,以便将来导入。验收准则
该进程每次运行时都会输出一个文件。
文件将由每个股票代码的一个记录组成。
文件中的每条记录都有以下以逗号分隔的字段:
股票报价文件将从 URL
[
download.finance.yahoo.com/d/quotes.csv?s=&f=sl1 ](http://download.finance.yahoo.com/d/quotes.csv?s=<QUOTES>&f=sl1)
获得,其中<QUOTES>
是由加号(+)分隔的股票代码列表,sl1
表示我想要股票代码和最近的交易价格。?? 1
1 你可以在[www.gummy-stuff.org/Yahoo-data.htm](http://www.gummy-stuff.org/Yahoo-data.htm).
找到关于这项网络服务的更多信息
| **层** | **交易** | | :-- | :-- | | 我< = | Ten | | II <= | One hundred | | III <=1, | 000 | | 四> | Ten thousand |
使用 URL
[
download.finance.yahoo.com/d/quotes.csv?s=HD&f=sl1](http://download.finance.yahoo.com/d/quotes.csv?s=HD&f=sl1)
返回的一个示例记录是:"HD",31.46
。导入股票价格:作为批处理,当我收到股票价格文件时,我会将该文件导入到数据库中以供将来处理。验收标准:
该过程将读取作业中上一步下载的文件。
每只股票的股价将储存在数据库中,供每笔交易参考。
文件成功导入后,将被删除。
文件的记录格式可以在 story Get 股票收盘价中找到。
任何格式错误的记录都将记录在单独的错误文件中,以供将来分析。
计算定价等级:作为批处理,在导入所有输入后,我将计算每个客户所处的定价等级,并将其存储起来以备将来使用。验收标准:
该流程将根据客户在一个月内的交易次数来计算每笔交易的价格。
每个等级将由以下阈值决定:
| **梯队** | **公式** | | :-- | :-- | | 我 | 9 美元+购买额的 0.1% | | 二 3 美元 | | | 三 2 美元 | | | 四 1 美元 | |
将存储与客户相关的等级值,用于未来的费用计算。
计算每笔交易的费用:作为批处理,在我完成计算定价层级后,我将计算客户将被收取的每笔交易的经纪费。验收标准:
该流程将根据客户所在的层级计算每笔交易的费用(在计算定价层级故事中计算)。
计算每笔交易价格的公式如下:
打印账户摘要:作为批处理,所有计算完成后,我会为每个客户打印一份摘要。该摘要将提供客户账户的概述,以及构成其投资组合总价值的细分。验收标准:
该流程将为每个客户生成一个文件。
摘要将以一行文字开始,说明以下内容,并完全对齐
Your Account Summary Statement Period:<BEGIN_DATE> to <END_DATE>
其中,开始日期是上个月的第一个日历日期,结束日期是上个月的最后一个日历日期。
在摘要标题之后,每种证券类型(证券和现金)将有一个单独的行项目,以及该账户的当前价值。
在每一个明细项目之后,将打印一个总的账户值。以下是该部分的一个示例:
Your Account Summary Statement Period: 07/01/2010 to 09/30/2010 Market Value of Current Securities $21,680.50 Current Cash Balance $254,953.23 Total Account Value $276,633.73
| **姓名** | **必需的** | **格式** | | :-- | :-- | :-- | | 股票代码 | 真实的 | \w+ | | 真实数量 | | \d+ | | 真实价格 | | \d+\。\d{2} | | 总值 | 真实的 | 数量*美元格式的价格 |打印账户明细:作为批处理,在每个账户汇总后,我将打印每个账户的明细构成。账户详情将为客户提供其账户构成的详细信息,以及他们的投资情况。验收标准:
账户详情将被附加到每个客户的账户摘要中。
详细信息将以标题“账户详细信息”开始,左对齐。
在新的一行,将指定账户的现金余额。
在现金余额下方,将显示一个标题,说明“证券”,靠左对齐。
对于客户持有的每种股票,将显示以下字段:
下面是这部分的一个例子。
`Account Detail
Cash $245,953.23
Securities
SHLD 100 $71.98 $7,198.00
CME 50 289.65 14482.50
Total Account Value $276,633.73`
打印报表表头:作为批处理,我会在每页的顶部打印一个表头。这将提供关于账户、客户和经纪公司的一般信息。验收标准:
除了客户的地址和账号之外,标题都是静态文本。
下面是一个标题示例,其中 Michael Minella 的姓名和地址是客户的姓名和地址,帐号是客户的帐号:
`Brokerage Account Statement
Apress Investment Company Customer Service Number
1060 West Addison St. (800) 867-5309
Chicago, IL 60613 Available 24/7
Michael Minella
1313 Mockingbird Lane
Chicago, IL 60606
Account Number 10938398571278401298`
这就满足了需求。如果你现在头晕,没关系。在下一节中,您将开始概述如何使用 Spring Batch 处理这个语句过程。然后,在本书的其余部分,您将学习如何实现使其工作所需的各个部分。
设计批处理作业
如前所述,这个项目的目标是利用 Spring Batch 提供的特性来创建一个健壮的、可维护的解决方案。为了实现这个目标,这个例子包含了现在看起来有点复杂的元素,比如标题、多种文件格式导入和包含副标题的复杂输出。原因是 Spring Batch 恰恰为这些特性提供了便利。让我们通过概述作业和描述其步骤来深入了解如何构建这个批处理过程。
职位描述
为了实现语句生成过程,您需要构建一个包含六个步骤的作业。图 3-3 显示了该流程的批处理作业流程,以下章节描述了这些步骤。
图 3-3。股票对账单工作流
导入客户交易数据
要开始这项工作,首先要导入客户和交易数据。该数据包含在平面文件中,具有由两种记录类型组成的复杂格式。第一种记录类型是客户的记录类型,由客户的姓名、地址和帐户信息组成。第二种记录类型由每笔交易的详细信息组成,包括股票代码、支付的价格、购买或出售的数量以及交易发生时的时间戳。使用 Spring Batch 读取多行记录的能力允许您用最少的代码处理这个文件。您为导入数据的 JDBC 持久化编写一个数据访问对象(DAO),如清单 3-1 所示。
清单 3-1。客户/交易输入文件
392041928,William,Robinson,9764 Jeopardy Lane,Chicago,IL,60606 HD,31.09,200,08:38:05 WMT,53.38,500,09:25:55 ADI,35.96,-300,10:56:10 REGN,29.53,-500,10:56:22 938472047,Robert,Johnson,1060 Addison St,Chicago,IL,60657 CABN,0.890,10000,14:52:15 NUAN,17.11,15000,15:02:45
检索股票收盘价
导入客户信息和交易后,您继续获取股票价格信息。这是一个简单的步骤,检索前一个月交易的股票的所有收盘价。为此,您创建一个 tasklet(正如您在上一章中对 Hello,World 批处理过程所做的那样)来调用需求中指定的 Yahoo web 服务,并下载一个包含所需股票数据的 CSV。这一步将输出写入一个类似于清单 3-2 中的文件,供下一步处理。
清单 3-2。股票收盘价输入文件
SHLD,71.98
CME,289.65 GOOG,590.83 F,16.28
将股票价格导入数据库
这一步读取文件并将数据导入数据库。这一步展示了 Spring Batch 提供的声明式 I/O 的优势。这项工作的输入和输出都不需要您编写代码。Spring Batch 提供了通过框架的股票组件读取您在上一步中下载的 CSV 文件以及更新数据库的能力。
你可能想知道为什么不直接导入数据。原因是错误处理。您正在导入由第三方来源提供给您的数据。因为您不能确定数据的质量,所以您需要能够处理导入过程中可能出现的任何错误。通过将数据写入文件供以后的步骤处理,您可以重新启动该步骤,而不必重新下载股票价格。
计算交易费用等级
到目前为止,您还没有对正在读取和写入的数据进行任何真正的处理。您所做的只是将数据从一个文件传输到一个数据库(为了更好地测量,进行了一些验证)。完成所需数据的导入后,开始进行所需的计算。你的经纪公司根据客户的交易量收取费用。客户交易越多,每次交易收取的费用就越少。通过层级分配向客户收取的金额;每一层都由前一个月的交易数量定义,并有一个与之相关的金额。
在这一步中,您将在读取器和写入器之间引入一个项目处理器,以确定客户所属的层。您通过 XML 声明读取器来加载客户的交易信息,声明写入器来以相同的方式更新客户的帐户。
计算交易费用
当你确定了每个客户的等级,你就可以计算每笔交易的费用。在上一步中,您在客户级别处理了记录,每个客户都被分配了一个层。在此步骤中,您在单个事务级别处理记录。可以想象,您在这一步中处理的记录比之前的任何一步都多;稍后,当本书谈到可伸缩性选项时,您将更详细地研究这一步。然而,首先,这一步看起来与前一步几乎完全一样,但是对于读取器来说使用了不同的 SQL,对于条目处理器来说使用了不同的逻辑,对于写入器来说又使用了另一个JdbcItemWriter
。
生成客户月报表
最后一步似乎是最复杂的——但正如你所知,外表可能具有欺骗性。这一步包括语句本身的生成。它展示了将解耦解决方案应用于批处理问题的一些好处。通过提供自定义编码的格式化程序,您可以用一个简单的类完成几乎所有的工作。这一步还使用了头的回调。
所有这些在理论上听起来都很棒,但留下了许多有待回答的问题。很好。在本书的剩余部分,您将研究如何在流程中实现这些特性,以及检查异常处理和重启/重试逻辑等内容。不过,在继续之前,您应该熟悉的最后一个项目是数据模型。这将有助于澄清这个系统是如何构建的。让我们来看看。
了解数据模型
在本书中,你已经看到了你所创造的工作的所有不同部分。在您进入实际开发之前,让我们进入最后一个难题。批处理是数据驱动的。由于没有用户界面,各种数据存储最终成为该流程唯一的外部交互。这一节着眼于示例应用所使用的数据模型。
图 3-4 概述了该批处理过程的特定应用表。明确地说,这个图表并没有包含运行这个批处理作业所需的所有表。第二章简要地看了一下 Spring Batch 在作业存储库中使用的表。除了这些表之外,所有这些表都将存在于数据库中。因为单独部署批处理模式并不少见,而且你在上一章已经回顾过了,所以我选择不在图 3-4 中提及它。
图 3-4。样本应用数据模型
对于批处理应用,您有四个表:Customer、Account、Transaction 和 Ticker。当您查看表中的数据时,请注意您没有存储生成语句所需的所有字段。有些字段(如帐户摘要中的总计)需要在处理过程中进行计算。除此之外,数据模型应该看起来相对简单:
- 客户:该记录包含所有特定于客户的信息,包括姓名和税务标识号。
- 账号。每个客户都有一个账户。出于您的目的,每个帐户都有一个号码和一个流动现金余额,根据需要从中扣除费用。客户的交易费用层也存储在这一层。
- 交易。每笔交易在交易表中都有相应的记录。这里的数据用于确定账户的当前状态(持有多少股份等等)。
- 跑马灯。对于经纪公司的客户交易的每只股票,该表中都有一条记录,其中包含股票的报价机和最近的收盘价。
总结
本章讨论了敏捷开发过程以及如何将它应用到批量开发中。本章继续沿着这些思路,通过用户故事为你在本书的整个过程中构建的示例应用定义需求。从这一点上,这本书从《SpringBatch》的“是什么”和“为什么”切换到了“如何”
在下一章中,您将深入探究 Spring Batch 的作业和步骤概念,并查看许多其他特定的示例。
四、了解作业和步骤
在第二章中,你创建了自己的第一份工作。您完成了作业和步骤的配置,执行了作业,并配置了一个数据库来存储您的作业存储库。在那个“你好,世界!”例如,您开始对 Spring Batch 中的作业和步骤有所了解。本章继续深入探讨工作和步骤。您首先要学习什么是与 Spring Batch 框架相关的作业和步骤。
从那里,您可以深入了解执行作业或步骤时会发生什么,从加载它们并验证它们的有效性,一直到完成它们。然后,您钻研一些代码,查看您可以配置的作业和步骤的各个部分,并在此过程中学习最佳实践。最后,您将看到批处理难题的不同部分如何通过 Spring 批处理过程中涉及的各种作用域相互传递数据。
尽管您在本章中深入研究了各个步骤,但是一个步骤中最大的部分是它们的读取器和写入器,这里没有涉及。第七章和 9 探讨 Spring Batch 中可用的输入和输出功能。本章尽可能简化每个步骤的 I/O 方面,以便您可以专注于工作中复杂的步骤。
介绍工作
随着 web 应用的激增,您可能已经习惯了将应用分成请求和响应的想法。每个请求包含发生的单个独特处理的数据。请求的结果通常是返回给用户的某种视图。一个 web 应用可以由几十到几百个这样的独特交互组成,每个都以相同的方式构建,如图 4-1 所示。
图 4-1。网络应用的请求/响应处理
然而,当您考虑批处理作业时,您实际上是在谈论一组操作。术语1是描述一份工作的好方法。再次使用 web 应用示例,考虑购物车应用的结帐过程是如何工作的。当您点击购物车中的商品结账时,您将经历一系列步骤:注册或登录、确认送货地址、输入账单信息、确认订单、提交订单。这个流程类似于什么是工作。
*就本书而言,作业被定义为一个独特的、有序的步骤列表,可以从头到尾独立执行。让我们来分解一下这个定义,这样你就能更好地理解你在做什么:
- 独特:Spring Batch 中的作业是通过 XML 配置的,类似于使用核心 Spring 框架配置 beans 的方式,因此是可重用的。对于相同的配置,您可以根据需要多次执行作业。因此,没有理由多次定义同一个职务。
- 有序步骤列表: 2 回到结账流程示例,步骤的顺序很重要。如果您一开始就没有注册,就无法验证您的送货地址。如果您的购物车是空的,您将无法执行结账流程。工作中的步骤顺序很重要。在客户的交易被导入到您的系统之前,您不能生成客户对帐单。在计算完所有费用之前,您无法计算帐户余额。您可以按照允许所有步骤按逻辑顺序执行的顺序来组织作业。
- 可以从头到尾执行: 第一章将批处理定义为一个不需要与某种形式的完成进行额外交互就可以运行的过程。作业是一系列无需外部依赖即可执行的步骤。您不会构建一个作业,以便第三步是等待,直到文件被发送到一个目录进行处理。相反,当文件到达时,您有一个作业开始。
- 独立:每个批处理作业应该能够在没有外部依赖影响的情况下执行。这并不意味着一个作业不能有依赖关系。相反,没有多少实际工作(除了“你好,世界”)是没有外部依赖性的。但是,该作业应该能够管理这些依赖关系。如果文件不存在,它会优雅地处理错误。它不等待文件被传递(那是调度程序的责任,等等)。一个作业可以处理它被定义要做的过程的所有元素。
作为比较,图 4-2 显示了批处理过程与图 4-1 中的 web 应用的执行情况。
对于那些熟悉 Spring Web Flow 框架的人来说,工作在结构上与 Web 应用中的流程非常相似。
虽然大多数作业由有序的步骤列表组成,但是 Spring Batch 确实支持并行执行步骤的能力。这个特性将在后面讨论。
图 4-2。批处理过程中的数据流
正如你在图 4-2 中看到的,批处理过程在运行时,所有的输入都是可用的。没有用户交互。在执行下一步之前,对数据集执行每一步直到完成。在深入研究如何在 Spring Batch 中配置作业的各种特性之前,我们先来讨论一下作业的执行生命周期。
跟踪作业的生命周期
当一个作业被执行时,它会经历一个生命周期。了解这一生命周期非常重要,因为您需要构建自己的作业,并了解作业运行时发生了什么。当您用 XML 定义作业时,您实际上是在为作业提供蓝图。就像为 Java 类编写代码就像为 JVM 定义一个创建实例的蓝图一样,作业的 XML 定义是 Spring Batch 创建作业实例的蓝图。
作业的执行从作业运行器开始。作业运行程序旨在使用传递的参数执行 name 请求的作业。Spring Batch 提供了两个作业运行器:
CommandLineJobRunner
:该作业运行器旨在从脚本或直接从命令行使用。使用时,CommandLineJobRunner
bootstraps 启动并执行所请求的任务,并传递参数。- 当使用类似 Quartz 或 JMX 钩子的调度程序来执行一个任务时,通常 Spring 是自举的,Java 进程在任务执行之前是活动的。在这种情况下,Spring 被引导时会创建一个
JobRegistry
,其中包含可运行的作业。JobRegistryBackgroundJobRunner
用于创建JobRegistry
。
CommandLineJobRunner
和JobRegistryBackgroundJobRunner
(都位于org.springframework.batch.core.launch.support
包中)是框架提供的两个作业运行器。你用第二章里的CommandLineJobRunner
来运行“你好,世界!”job,你会在整本书中继续使用它。
虽然 job runner 是用来与 Spring Batch 接口的,但它不是框架的标准部分。没有JobRunner
接口,因为每个场景需要不同的实现(尽管 Spring Batch 提供的两个作业运行器都使用 main 方法来启动)。相反,真正进入框架执行的是org.springframework.batch.core.launch.JobLauncher
接口的实现。
Spring Batch 提供了单个JobLauncher
、org.springframework.batch.core.launch.support.SimpleJobLauncher
。这个类使用 Core Spring 的TaskExecutor
接口来执行请求的作业。您可以看到这是如何配置的,但是需要注意的是,在 Spring 中有多种方式来配置org.springframework.core.task.TaskExecutor
。如果使用了org.springframwork.core.task.SyncTaskExecutor
,作业将在与JobLauncher
相同的线程中执行。任何其他选项都在自己的线程中执行作业。
图 4-3。一个Job
、JobInstance
、JobExecution
和的关系
运行批处理作业时,会创建一个org.springframework.batch.core.JobInstance
。一个JobInstance
表示作业的一次逻辑运行,由作业名和这次运行传递给作业的参数来标识。作业的运行不同于执行作业的尝试。如果您有一个预期每天运行的作业,那么您应该在 XML 中配置一次(定义蓝图)。每天您都会有一次新的运行或JobInstance
,因为您向作业传递了一组新的参数(其中一个是日期)。当每个JobInstance
尝试一次或JobExecution
成功完成时,它将被视为完成。
注意一个
JobInstance
只能执行一次才能成功完成。因为JobInstance
是由作业名和传入的参数标识的,这意味着您只能使用相同的参数运行一次作业。
您可能想知道 Spring Batch 如何从一次尝试到另一次尝试知道一个JobInstance
的状态。在第二章的中,您查看了作业存储库,其中有一个batch_job_instance
表。此表是所有其他表派生的基础。是batch_job_instance
和batch_job_params
标识了一个JobInstance
(batch_job_instance.job_key
实际上是名称和参数的散列)。
是运行作业的实际尝试。如果一个任务第一次从开始运行到结束,只有一个JobExecution
与给定的JobInstance
相关。如果一个任务在第一次运行后以错误状态结束,那么每次试图运行JobInstance
时,都会创建一个新的JobExecution
(通过向同一个任务传递相同的参数)。对于 Spring Batch 为您的作业创建的每个JobExecution
,都会在batch_job_execution
表中创建一条记录。当JobExecution
执行时,它的状态也被保存在batch_job_execution_context
中。这允许 Spring Batch 在发生错误时在正确的点重新启动作业。
配置作业
理论说够了。让我们进入一些代码。本节深入探讨了配置作业的各种方法。正如在第二章中提到的,和所有的 Spring 一样,Spring 批量配置是通过 XML 完成的。考虑到这一点,Spring Batch 2 中添加的一个非常受欢迎的特性是添加了一个批处理 XSD,使批处理作业的配置更加简洁。
注意一个好的最佳实践是在以任务名称命名的 XML 文件中配置每个任务。
基本作业配置
清单 4-1 显示了一个基本 Spring 批处理作业的外壳。根据记录,这不是一个有效的工作。Spring Batch 中的作业需要至少有一个步骤或被声明为抽象。 3 在任何情况下,这里的重点都是工作而不是步骤,所以你要在本章后面的工作中添加步骤。
你在第二章的“你好,世界!”中使用了这种格式 job,对于以前使用过 Spring 的人来说应该很熟悉。就像 Spring 框架的大多数其他扩展一样,您可以像 Spring 的任何其他用途一样配置 beans,并拥有一个定义特定于域的标记的 XSD。在这种情况下,您在beans
标签中包含 Spring Batch 的 XSD。
清单 4-1。??basicJob.xml
`
<beans xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<batch:job id="basicJob">
...
</batch:job>
`
标签之后的第一个文件是 ?? 文件的导入,它位于项目的 ?? 目录中。你在第二章中使用了这个文件,但并没有真正深入进去,所以现在让我们来看看它。清单 4-2 显示了launch-context.xml
。请注意,这个launch-context.xml
是 zip 文件的一个显著精简版本。本书讨论了文件的其余部分,因为您将在以后的章节中使用它的各个部分。现在,让我们把重点放在让 Spring Batch 工作所需的部件上。
清单 4-2。??launch-context.xml
`
<bean id="placeholderProperties"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigure”
`
3 之后,本章着眼于抽象的工作。
launch-context.xml
拥有上一节中讨论的大部分元素及其依赖关系。它从一个数据源开始。您可以使用标准的 Spring 配置来配置一个数据源,Spring Batch 使用该数据源来访问作业存储库,并且该数据源也可用于批处理过程可能需要的任何其他数据库访问。值得注意的是,Spring Batch 为JobRepository
使用的数据库不需要与用于业务处理的模式相同。
transactionManager
也在这个文件中配置。事务处理在批处理作业中非常重要,因为您要分块处理大量数据,并且每个数据块都是一次提交的。这也是使用核心 Spring 部件的标准配置。
请注意,您正在使用属性来指定可能因环境而异的值。在transactionManager
之后,您配置 Spring 的PropertyPlaceholderConfigurer
来在运行时处理这些属性的填充。您正在使用batch.properties
文件来指定值,它包含在 zip 文件提供的源代码中。
接下来是jobRepository
。这是您将在launch-context.xml
文件中配置的第一个 Spring 批处理组件。jobRepository
用于维护作业的状态,并对每一步进行 SpringBatch。在这种情况下,您正在配置框架用来在数据库上执行 CRUD 操作的句柄。第五章讲述了jobRepository
的一些高级配置,包括改变模式前缀等等。这个示例配置提供了两个必需的依赖项:一个数据源和一个事务管理器。
这里最后一块launch-context.xml
是jobLauncher
豆。如前一节所述,从执行的角度来看,作业启动器是进入 Spring Batch framework 的门户。它被配置为依赖于jobRepository
。
定义了通用组件后,让我们回到basicJob.xml
。关于配置,90%的作业配置是步骤的有序定义,这将在本章后面介绍。关于basicJob
请注意,您还没有配置任何对作业存储库或事务管理器的引用。这是因为默认情况下,Spring 使用名为jobRepository
的jobRepository
和名为transactionManager
的事务管理器。你会在第五章中看到如何具体配置这些元素,其中讨论了如何使用JobRepository
及其元数据。
工作继承
与配置作业相关的大多数选项都与执行相关,因此您将在稍后讲述作业执行时看到这些选项。然而,有一个实例可以改变作业配置,在这里讨论是有意义的:继承的使用。
像大多数其他面向对象的编程方面一样,Spring Batch 框架允许您一次性配置作业的公共方面,然后用其他作业扩展基本作业。那些其他作业继承它们正在扩展的作业的属性。但是 Spring Batch 中的继承有一些需要注意的地方。Spring Batch 允许从一个作业到另一个作业继承所有作业级配置。这是很重要的一点。您不能定义具有可以继承的通用步骤的作业。允许您继承的是重新启动作业的能力、作业监听器和任何传入参数的验证器。为此,您需要做两件事:声明父作业抽象,并将其指定为任何想要继承其功能的作业中的父作业。
清单 4-3 将父作业配置为可重启 4 ,然后用sampleJob
扩展它。因为sampleJob
扩展了baseJob
,所以也是可重启的。清单 4-4 展示了如何配置一个配置了参数验证器的抽象作业,并扩展它来继承验证器。
清单 4-3。 inheritanceJob.xml
同职务继承
`
清单 4-4。参数验证器继承
`
虽然作业的大部分配置可以从父作业继承,但并不是全部都可以。以下是您可以在父作业中定义并由其子作业继承的内容列表:
- 可重启:指定作业是否可重启
- 一个参数增量器:每增加一个
JobExecution
就增加一个作业参数 - 监听器:任何作业级别的监听器
- 作业参数验证器:验证传递给作业的参数是否满足任何要求
4 可重启性在第六章中有更详细的介绍。
所有这些概念都是新的,将在本章后面讨论。现在,您需要知道的是,当在抽象作业上设置这些值时,任何扩展父作业的作业都会继承它们。孩子不能继承的东西包括步骤配置、步骤流程和决策。这些必须在使用它们的任何作业中定义。
继承不仅有助于巩固公共属性的配置,而且有助于标准化某些事情是如何完成的。因为上一个例子开始关注参数和它们的验证,这看起来像是一个合乎逻辑的下一个主题。
工作参数
您已经读到过几次JobInstance
是由作业名和传递给作业的参数来标识的。您还知道,正因为如此,您不能使用相同的参数多次运行相同的作业。如果你这样做了,你会收到一个org.springframework.batch.core.launch.JobInstanceAlreadyCompleteException
,告诉你如果你想再次运行这个任务,你需要改变参数(如清单 4-5 所示)。
清单 4-5。当您尝试使用相同的参数运行一个作业两次时会发生什么情况
2010-11-28 21:06:03,598 ERROR org.springframework.batch.core.launch.support.CommandLineJobRunner.main() [org.springframework.batch.core.launch.support.CommandLineJobRunner] - <Job Terminated in error: A job instance already exists and is complete for parameters={}. If you want to run this job again, change the parameters.> org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={}. If you want to run this job again, change the parameters. at org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(Simpl eJobRepository.java:122) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ...
那么,如何将参数传递给作业呢?Spring Batch 不仅允许您向作业传递参数,还允许您在作业运行之前自动递增参数 5 或验证参数。首先,看看如何将参数传递给作业。
向作业传递参数取决于您如何调用作业。作业运行器的功能之一是创建一个org.springframework.batch.core.JobParameters
的实例,并将其传递给JobLauncher
执行。这是有意义的,因为如果您从命令行启动一个作业,那么您传递参数的方式与从 Quartz 调度程序启动作业的方式是不同的。因为到目前为止您一直在使用CommandLineJobRunner
,所以让我们从那里开始。
5 对于每个JobInstance
来说,增加一个参数是有意义的。例如,如果作业的运行日期是其参数之一,这可以通过参数增量器自动解决。
向CommandLineJobRunner
传递参数就像在命令行上传递key=value
对一样简单。清单 4-6 展示了如何使用到目前为止你调用作业的方式将参数传递给作业。
清单 4-6。将参数传递给CommandLineJobRunner
java –jar sample-application-0.0.1-SNAPSHOT.jar jobs/sampleJob.xml sampleJob name=Michael
在清单 4-6 中,您传递了一个参数name
。当您将 parameter 传递到批处理作业中时,您的作业运行器会创建一个JobParameters
的实例,作为作业接收的所有参数的容器。
JobParameters
不过是一个java.util.Map<String, JobParameter>
对象的包装器。注意,虽然在这个例子中你传入了String
s,但是Map
的值是一个 org . spring framework . batch . core .JobParameter
实例。原因在于类型。Spring Batch 提供了参数的类型转换,并且在JobParameter
类上提供了特定于类型的访问器。如果您将参数的类型指定为long
,它可以作为java.lang.Long
使用。String
、Double
和java.util.Date
均可开箱转换。为了利用转换,你告诉 Spring Batch 参数名后面括号中的参数类型,如清单 4-7 所示。注意,Spring Batch 要求每个的名称都是小写的。
清单 4-7。指定参数的类型
java –jar sample-application-0.0.1-SNAPSHOT.jar jobs/sampleJob.xml sampleJob param1(string)=Spring param2(long)=33
要查看传递到作业中的参数,可以查看作业存储库。第二章注意到有一个作业参数表叫做batch_job_params
,但是因为你没有传递任何参数给你的作业,所以它是空的。如果您在执行了清单 4-6 和 4-7 中的示例后浏览该表,您应该会看到表 4-1 中显示的内容。
既然您已经知道了如何将参数放入批处理作业,那么一旦有了参数,如何访问它们呢?如果您快速查看一下ItemReader
、ItemProcessor
、ItemWriter
和Tasklet
接口,您会很快注意到所有感兴趣的方法都没有将JobParameters
实例作为它们的参数之一。根据您尝试访问参数的位置,有几个不同的选项:
ChunkContext
:如果您查看一下HelloWorld
小任务,您会看到execute
方法接收了两个参数。第一个参数是org.springframework.batch.core.StepContribution
,它包含关于您在该步骤中所处位置的信息(写计数、读计数等等)。第二个参数是ChunkContext
的一个实例。它提供了作业在执行时的状态。如果你在一个小任务中,它包含了你正在处理的程序块的所有信息。关于该块的信息包括关于步骤和作业的信息。正如你可能猜到的,ChunkContext
有一个对org.springframework.batch.core.scope.context.StepContext
的引用,其中包含了你的JobParameters
。- 后期绑定:对于任何不是微线程的框架,获取参数的最简单方法是通过 Spring 配置注入它。鉴于
JobParameters
是不可变的,在引导过程中绑定它们非常有意义。
清单 4-8 显示了一个更新的HelloWorld
小任务,它利用输出中的name
参数作为如何从ChunkContext
访问参数的例子。
清单 4-8。在微线程中访问JobParameters
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.item.ExecutionContext;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, %s";
public RepeatStatus execute( StepContribution step,
ChunkContext context ) throws Exception {
String name =
(String) context.getStepContext().getJobParameters().get("name");
System.out.println( String.format(HELLO_WORLD, name) );
return RepeatStatus.FINISHED;
}
}`
尽管 Spring Batch 将作业参数存储在JobParameter
类的一个实例中,但是当您以这种方式获取参数时,getJobParameters()
会返回一个Map<String, Object>
。因此,需要以前的造型。
清单 4-9 展示了如何使用 Spring 的后期绑定将作业参数注入到组件中,而不必引用任何JobParameters
代码。除了使用 Spring 的 EL(表达式语言)来传递值之外,任何将要配置后期绑定的 bean 都需要将范围设置为step
。
清单 4-9。通过后期绑定获取作业参数
<bean id="helloWorld" class="com.apress.springbatch.chapter4.HelloWorld" scope="step"> <property name="name" value="#{jobParameters[name]}"/> </bean>
值得注意的是,为了让清单 4-9 中的配置能够工作,HelloWorld
类需要被更新以接受新的参数。清单 4-10 显示了这种参数关联方法的更新代码。
清单 4-10。更新HelloWorld
小任务
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.item.ExecutionContext;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, %s";
private String name;
public RepeatStatus execute( StepContribution step,
ChunkContext context ) throws Exception {
String name =
(String) context.getStepContext().getJobParameters().get("name");
System.out.println( String.format(HELLO_WORLD, name) );
return RepeatStatus.FINISHED;
}
public void setName(String newName) {
name = newName;
}
public String getName() {
return name;
}
}`
由于能够将参数传递到作业中并投入使用,本章接下来讨论的 Spring Batch 框架中内置了两个特定于参数的功能:参数验证和每次运行增加给定参数的能力。让我们从参数验证开始,因为在前面的例子中已经提到过了。
验证工作参数
每当一个软件获得外部输入时,确保输入对你所期望的是有效的是一个好主意。网络世界使用客户端 JavaScript 和各种服务器端框架来验证用户输入,批处理参数的验证也不例外。幸运的是,Spring 使得验证作业参数变得非常容易。为此,您只需要实现org.springframework.batch.core.JobParametersValidator
接口,并在您的工作中配置您的实现。清单 4-11 展示了 Spring Batch 中一个作业参数验证器的例子。
清单 4-11。验证参数名的参数验证器是一个String
`package com.apress.springbatch.chapter4;
import java.util.Map;
import org.springframework.batch.core.*;
import org.apache.commons.lang.StringUtils;
public class ParameterValidator implements JobParametersValidator{
public void validate(JobParameters params) throws
JobParametersInvalidException {
String name = params.getString("name");
if(!StringUtils.isAlpha(name)) {
throw new
JobParametersInvalidException("Name is not alphabetic");
}
}
}`
如你所见,结果方法是validate
方法。因为这个方法是无效的,只要没有抛出JobParametersInvalidException
,就认为验证通过。在本例中,如果您传递名称 4566,就会抛出异常,作业以状态COMPLETED
完成。这一点值得注意。您传入的参数无效并不意味着作业没有正确完成。在传递无效参数的情况下,作业被标记为COMPLETED
,因为它对接收到的输入进行了所有有效的处理。当你思考这个问题的时候,它是有意义的。一个JobInstance
由作业名和传入作业的参数来标识。如果您传入了无效的参数,您不希望重复这样的操作,所以可以声明作业已完成。
除了像前面一样实现您自己的定制参数验证器之外,Spring Batch 还提供了一个验证器来确认所有必需的参数都已被传递:org.springframework.batch.core.job.DefaultJobParametersValidator
。要使用它,您可以像配置您的定制验证器一样配置它。DefaultJobParametersValidator
有两个可选依赖:requiredKeys
和optionalKeys
。两者都是String
数组,接受一个参数名列表,这些参数名要么是必需的,要么是唯一允许的可选参数。清单 4-12 显示了DefaultJobParametersValidator
的两种配置,以及如何将其添加到您的工作中。
清单 4-12。 DefaultJobParametersValidator
配置在parameterValidatorJob.xml
`<beans:bean id="requiredParamValidator"
class="org.springframework.batch.core.job.DefaultJobParametersValidator">
<beans:property name="requiredKeys" value="batch.name,batch.runDate"/>
</beans:bean>
<beans:bean id="optionalParamValidator"
class="org.springframework.batch.core.job.DefaultJobParametersValidator">
<beans:property name="requiredKeys" value="batch.name,batch.runDate"/>
<beans:property name="optionalKeys" value="batch.address"/>
</beans:bean>
如果您使用requiredParamValidator
,如果您没有传递参数batch.name
和batch.runDate
,您的作业将抛出一个异常。如果需要,可以传入更多的参数,但这两个参数不能为空。另一方面,如果使用optionalParamValidator
,如果batch.name
和batch.runDate
没有传递给作业,作业再次抛出异常,但是如果传递了除batch.address
之外的任何参数,作业也会抛出异常。这两个验证器的区别在于,第一个验证器可以接受除必需参数之外的任何参数。第二个只能接受指定的三个。在这两种情况下,如果无效的场景发生,就会抛出一个JobParametersInvalidException
并将作业标记为已完成,如前所述。
递增工作参数
到目前为止,您一直在一个作业只能用一组给定的参数运行一次的限制下运行。如果你一直跟着例子走,你可能已经想到了如果你试图用相同的参数运行相同的作业两次会发生什么,如清单 4-5 中的所示。不过有个小漏洞:使用JobParametersIncrementer
。
org.springframework.batch.core.JobParametersIncrementer
是 Spring Batch 提供的一个接口,允许您为给定的作业唯一地生成参数。您可以为每次运行添加时间戳。您可能有一些其他的业务逻辑需要一个参数随着每次运行而递增。该框架提供了该接口的一个实现,它增加了一个长参数,默认名称为run.id
。
清单 4-13 展示了如何通过添加对作业的引用来为您的作业配置一个JobParametersIncrementer
。
清单 4-13。在工作中使用JobParametersIncrementer
`<beans:bean id="idIncrementer"
class="org.springframework.batch.core.launch.support.RunIdIncrementer"/>
一旦您配置了JobParametersIncrementer
(在这种情况下,框架提供了org.springframework.batch.core.launch.support.RunIdIncrementer
),您还需要做两件事情来完成这项工作。首先,您需要为一个JobExplorer
实现添加配置。第五章详细介绍了什么是JobExplorer
以及如何使用。目前,只知道 Spring Batch 需要它来增加参数。清单 4-14 显示了配置,但是它已经在包含在 zip 文件发行版中的launch-context.xml
中配置好了。
清单 4-14。配置为JobExplorer
<bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"> <property name="dataSource" ref="dataSource"/> </bean>
使用JobParametersIncrementer
的最后一块拼图会影响你如何称呼你的工作。当你想增加一个参数的时候,你需要在调用你的作业的时候把参数–next
加到命令里。这告诉 Spring Batch 根据需要使用增量器。
现在,当你用清单 4-15 中的命令运行你的作业时,你可以用相同的参数运行它任意多次。
清单 4-15。命令运行一个作业并增加参数
java –jar sample-application-0.0.1-SNAPSHOT.jar jobs/sampleJob.xml sampleJob name=Michael -next
事实上,去试一试吧。当您运行 sampleJob 三四次后,查看batch_job_params
表,看看 Spring Batch 是如何使用两个参数执行您的作业的:一个名为name
的String
,值为迈克尔,另一个名为run.id
的long
。run.id
的值每次都会改变,每次执行增加 1。
您在前面已经看到,您可能希望在每次运行作业时将一个参数作为时间戳。这在每天运行一次的作业中很常见。为此,您需要创建自己的JobParametersIncrementer
实现。配置和执行与之前相同。然而,你没有使用RunIdIncrementer
,而是使用了DailyJobTimestamper
,其代码在清单 4-16 中。
清单 4-16。??DailyJobTimestamper.java
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersIncrementer;
import java.util.Date;
import org.apache.commons.lang.time.DateUtils;
public class DailyJobTimestamper implements JobParametersIncrementer {
/**
- Increment the current.date parameter.
*/
public JobParameters getNext( JobParameters parameters ) {
Date today = new Date();
if ( parameters != null && !parameters.isEmpty() ) {
Date oldDate = parameters.getDate( "current.date", new Date() );
today = DateUtils.addDays(oldDate, 1);
}
return new JobParametersBuilder().addDate( "current.date", today )
.toJobParameters();
}
}`
很明显,工作参数是框架的重要组成部分。它们允许您在运行时为作业指定值。它们还用于唯一标识您的作业运行。在整本书中,您更多地使用它们来配置运行作业的日期和重新处理错误文件。现在,让我们看看作业级别的另一个强大特性:作业监听器。
使用工作监听器
当您使用 web 应用时,反馈对用户体验至关重要。用户单击一个链接,页面会在几秒钟内刷新。然而,正如您所看到的,批处理并没有提供太多的反馈。你启动一个过程,它就会运行。就这样。是的,您可以查询作业存储库来查看作业的当前状态,还有 Spring Batch Admin web 应用,但是很多时候您可能希望在作业中的某个给定点发生一些事情。假设您希望在作业失败时发送电子邮件。也许您希望将每个作业的开始和结束记录到一个特殊的文件中。您希望在作业开始时(一旦JobExecution
被创建并持久化,但在执行第一步之前)或结束时发生的任何处理都是通过作业监听器来完成的。
有两种方法可以创建作业监听器。第一种是通过实现org.springframework.batch.core.JobExecutionListener
接口。这个接口有两个方法的结果:beforeJob
和afterJob
。每一个都将JobExecution
作为一个参数,它们分别在作业执行之前和之后被执行——你猜对了。关于afterJob
方法需要注意的一件重要事情是,不管作业结束时的状态如何,它都会被调用。因此,您可能需要评估作业结束时的状态,以确定要做什么。清单 4-17 给出了一个简单监听器的例子,它打印出一些关于作业运行前后的信息,以及作业完成时的状态。
清单 4-17。??JobLoggerListener.java
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
public class JobLoggerListener implements JobExecutionListener {
public void beforeJob(JobExecution jobExecution) {
System.out.println(jobExecution.getJobInstance().getJobName()
+ " is beginning execution");
}
public void afterJob(JobExecution jobExecution) {
System.out.println(jobExecution.getJobInstance()
.getJobName()
+ " has completed with the status " +
jobExecution.getStatus());
}
}`
如果你还记得的话,这本书之前说过 Spring Batch 的配置还不支持注释。那是谎言。支持少量注释,@BeforeJob
和@AfterJob
是其中两个。当使用注释时,唯一的区别,如清单 4-18 所示,是你不需要实现JobExecutionListener
接口。
清单 4-18。??JobLoggerListener.java
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.annotation.AfterJob;
import org.springframework.batch.core.annotation.BeforeJob;
public class JobLoggerListener {
@BeforeJob
public void beforeJob(JobExecution jobExecution) {
System.out.println(jobExecution.getJobInstance().getJobName()
+ " is beginning execution");
}
@AfterJob
public void afterJob(JobExecution jobExecution) {
System.out.println(jobExecution.getJobInstance()
.getJobName()
+ " has completed with the status " +
jobExecution.getStatus());
}
}`
在这两种情况下,这两个选项的配置是相同的。回到 XML 的世界,您可以在工作中配置多个监听器,如清单 4-19 所示。
清单 4-19。在listenerJob.xml
中配置作业监听器
`<beans:bean id="loggingListener"
class="com.apress.springbatch.chapter4.JobLoggerListener"/>
前面,本章讨论了作业继承。这种继承会对您在工作中如何配置侦听器产生影响。当一个作业有侦听器,并且它的父作业也有侦听器时,您有两种选择。第一种选择是让子侦听器覆盖父侦听器。如果这是你想要的,那么你没有什么不同。然而,如果你希望父的和子的监听器都被执行,那么当你配置子的监听器列表时,你使用merge
属性,如清单 4-20 所示。
清单 4-20。合并父子作业中配置的监听器
`<beans:bean id="loggingListener"
class="com.apress.springbatch.chapter4.JobLoggerListener"/>
<beans:bean id="theEndListener"
class="com.apress.springbatch.chapter4.JobEndingListener"/>
侦听器是一个有用的工具,能够在工作的某些点上执行逻辑。侦听器也可用于批处理难题的许多其他部分,例如步骤、读取器、编写器等等。在本书后面的章节中,您会看到它们各自的组件。现在,还有一个与乔布斯有关的话题。
执行上下文
批处理本质上是有状态的。他们需要知道自己正处于哪一步。他们需要知道他们在该步骤中处理了多少记录。这些和其他有状态的元素不仅对于任何批处理的正在进行的处理是至关重要的,而且对于重新启动之前失败的进程也是至关重要的。例如,假设一个每晚处理 100 万笔交易的批处理过程在处理了 90 万笔记录后停止。即使是周期性的提交,当您重启时,您如何知道从哪里恢复呢?重建执行状态的想法可能令人望而生畏,这就是 Spring Batch 为您处理它的原因。
您之前已经了解了JobExecution
如何表示执行作业的实际尝试。正是这一级别的域需要维护状态。当JobExecution
通过一个作业或步骤时,状态会改变。这种状态在ExecutionContext
保持着。
如果您考虑 web 应用如何存储状态,通常是通过HttpSession
。 6 ExecutionContext
本质上是批处理作业的会话。除了简单的key-value
对,ExecutionContext
提供了一种安全存储作业状态的方法。web 应用的会话和ExecutionContext
的一个区别是,在你的工作过程中,你实际上有多个ExecutionContext
。JobExecution
有一个ExecutionContext
,每个StepExecution
也有一个ExecutionContext
(你将在本章后面看到)。这允许在适当的级别确定数据的范围(特定于步骤的数据或整个作业的全局数据)。图 4-4 显示了这些元素之间的关系。
图 4-4。关系ExecutionContext
年代
ExecutionContext
提供了一种“安全”的方式来存储数据。存储是安全的,因为进入ExecutionContext
的所有内容都保存在作业存储库中。你简要地看了一下第二章中的batch_job_execution_context
和batch_step_execution_context
表,但是它们当时并没有包含任何有意义的数据。让我们看看如何向ExecutionContext
添加数据和从中检索数据,以及这样做时它在数据库中是什么样子。
操纵执行上下文
如前所述,ExecutionContext
是JobExecution
或StepExecution
的一部分。正因为如此,要获得对ExecutionContext
的控制,你需要根据你想要使用的JobExecution
或StepExecution
来获得它。清单 4-21 展示了如何在HelloWorld
小任务中获得ExecutionContext
的句柄,并在上下文中添加你正在打招呼的人的名字。
本章忽略了以某种客户端形式(cookies、胖客户端等等)维护状态的 web 框架。
清单 4-21。向作业的ExecutionContext
添加名称
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.item.ExecutionContext;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, %s";
public RepeatStatus execute( StepContribution step,
ChunkContext context ) throws Exception {
String name =
(String) context.getStepContext()
.getJobParameters()
.get("name");
ExecutionContext jobContext = context.getStepContext()
.getStepExecution()
.getJobExecution()
.getExecutionContext();
jobContext.put(“user.name", name);
System.out.println( String.format(HELLO_WORLD, name) );
return RepeatStatus.FINISHED;
}
}`
注意,您必须做一些遍历才能到达作业的ExecutionContext
。在这种情况下,您所做的就是从块到作业的步骤,沿着作用域树向上移动。如果你看一下StepContext
的 API,你会发现有一个getJobExecutionContext()
方法。该方法返回一个代表作业的ExecutionContext
的当前状态的Map<String, Object>
。尽管这是一种访问当前值的便捷方式,但它的使用有一个限制因素:对由StepContext.getJobExecutionContext()
方法返回的Map
所做的更新不会持久保存到实际的ExecutionContext
。因此,如果出现错误,您对那个Map
所做的任何更改,如果没有对真正的ExecutionContext
所做的更改,都将丢失。
清单 4-21 的例子显示了使用作业的ExecutionContext
,但是获取和操作步骤的ExecutionContext
的能力以同样的方式工作。那样的话,你直接从StepExecution
那里得到ExecutionContext
,而不是JobExecution
。清单 4-22 显示了更新后使用步骤的ExecutionContext
而不是作业的代码。
清单 4-22。向作业的ExecutionContext
添加名称
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.item.ExecutionContext;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, %s";
public RepeatStatus execute( StepContribution step,
ChunkContext context ) throws Exception {
String name =
(String) context.getStepContext()
.getJobParameters()
.get("name");
ExecutionContext jobContext = context.getStepContext()
.getStepExecution()
.getExecutionContext();
jobContext.put(“user.name", name);
System.out.println( String.format(HELLO_WORLD, name) );
return RepeatStatus.FINISHED;
}
}`
执行上下文持久性
随着作业的处理,Spring Batch 会将您的状态持久化,作为提交每个块的一部分。这种持久性的一部分是保存作业和当前步骤的ExecutionContext
s. 第二章查看了表的布局。让我们继续执行带有来自清单 4-21 的更新的sampleJob
作业,看看数据库中保存的值是什么样的。表 4-2 显示了name
参数设置为Michael
时,单次运行后batch_job_execution_context
表的内容。
表 4-2。BATCH_JOB_EXECUTION_CONTEX``T
的内容
表 4-2 由三列组成。第一个是对与这个ExecutionContext
相关的JobExecution
的引用。第二个是Job
的ExecutionContext
的 JSON 表示。该字段随着处理的进行而更新。最后,SERIALIZED_CONTEXT
字段包含一个序列化的 Java 对象。SERIALIZED_CONTEXT
仅在作业运行或失败时被填充。
本章的这一节介绍了 Spring Batch 中作业的不同部分。然而,为了使一个作业有效,它至少需要一个步骤,这就把您带到了 Spring Batch 框架的下一个主要部分:步骤。
工作步骤
如果一个作业定义了整个过程,那么一个步骤就是一个作业的组成部分。这是一个独立的顺序批处理机。我称之为批处理机是有原因的。一个步骤包含一项工作所需的所有部分。它处理自己的输入。它有自己的处理器。它处理自己的输出。事务在一个步骤中是独立的。从设计上来说,这些步骤是不连贯的。这允许您作为开发人员根据需要自由地组织您的工作。
在本节中,您将采用与上一节中对 jobs 所做的相同的方式深入探讨各个步骤。您将了解 Spring Batch 如何一步一步地将处理分解,以及由于以前版本的框架,这种方式发生了怎样的变化。您还将看到许多关于如何配置工作中的步骤的示例,包括如何控制步骤之间的流程以及有条件的步骤执行。最后,配置语句作业所需的步骤。记住所有这些,让我们通过看步骤如何处理数据来开始看步骤。
组块与项目处理
批处理通常是关于处理数据的。当您考虑要处理的数据单元是什么时,有两种选择:单个项目或一大块项目。单个项由单个对象组成,该对象通常代表数据库或文件中的一行。因此,基于项目的处理是一次一行、一条记录或一个对象地读取、处理然后写入数据,如图 4-5 所示。
图 4-5。基于项目的处理
可以想象,这种方法会有很大的开销。当您知道将向数据库提交大量行或将它们写入文件时,写入单个行的低效率是巨大的。
当 Spring Batch 1.x 在 2008 年问世时,基于项目的处理是记录的处理方式。从那以后,SpringSource 和 Accenture 的人升级了这个框架,在 Spring Batch 2 中,他们引入了基于组块处理的概念。批处理世界中的块是需要处理的记录或行的子集,通常由提交间隔定义。在 Spring Batch 中,当您处理一个数据块时,它是由每次提交之间处理的行数定义的。
图 4-6 显示了为块处理而设计的批处理过程中,数据是如何流动的。在这里,您可以看到,尽管每一行仍然是单独读取和处理的,但是在提交时,对单个块的所有写入都是同时发生的。这种处理上的小调整带来了巨大的性能提升,并为许多其他处理能力开辟了天地。
图 4-6。基于组块的处理
基于块的处理允许您做的事情之一是远程处理块。当你考虑到像网络开销这样的事情时,远程处理单个项目的成本太高了。但是,如果您可以一次将整个数据块发送到远程处理器,那么它不但不会降低性能,还会显著提高性能。
随着您在本书中对步骤、读取器、写入器和可伸缩性的了解越来越多,请记住 Spring Batch 所基于的基于块的处理。让我们继续深入研究如何配置您的工作的构建块:步骤。
步进配置
到目前为止,您已经认识到工作实际上只不过是要执行的有序步骤列表。因此,通过在作业中列出步骤来配置步骤。让我们来看看如何配置一个步骤以及您可以使用的各种选项。
基本步骤
当您考虑 Spring Batch 中的步骤时,有两种不同的类型:基于块的处理步骤和小任务步骤。虽然您在前面的“Hello,World!”约伯,稍后你会看到更多细节。现在,我们从如何配置基于块的步骤开始。
正如您前面看到的,块是由它们的提交间隔定义的。如果提交间隔设置为 50 项,那么您的作业读入 50 项,处理 50 项,然后一次写出 50 项。因此,事务管理器在基于块的步骤的配置中起着关键作用。清单 4-23 显示了如何为面向块的处理配置一个基本步骤。
清单 4-23。stepJob.xml
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="inputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[inputFile]}"/>
</beans:bean>
<beans:bean id="outputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[outputFile]}"/>
</beans:bean>
<beans:bean id="inputReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" ref="inputFile"/>
<beans:property name="lineMapper">
<beans:bean
class="org.springframework.batch.item.file.mapping.PassThroughLineMapper"/>
</beans:property>
</beans:bean>
<beans:bean id="outputWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter">
<beans:property name="resource" ref="outputFile"/>
<beans:property name="lineAggregator">
<beans:bean
class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
</beans:property>
</beans:bean>
</beans:beans>`
清单 4-23 可能看起来有些吓人,但让我们把注意力放在最后的工作和步骤配置上。文件的其余部分是一个基本的ItemReader
和ItemWriter
的配置,分别在第七章和第九章中介绍。当你浏览清单 4-23 中的作业时,你会看到这个步骤是从step
标签开始的。与任何其他 Spring Bean 一样,所需要的只是 id 或名称。在step
标签内是一个tasklet
标签。org.springframework.batch.core.step.tasklet.Tasklet
界面实际上是针对您将要执行的步骤类型的策略界面。在这种情况下,您正在配置org.springframework.batch.core.step.item.ChunkOrientedTasklet<I>
。您不必担心在这里专门配置类;请注意,也可以使用其他类型的微线程。示例步骤的最后一部分是chunk
标记。在这里,您定义了您的步骤的块是什么。您说使用inputReader
bean(ItemReader
接口的实现)作为读取器,使用outputWriter
bean(ItemWriter
接口的实现)作为写入器,一个块包含 50 个条目。
注意当你用 Spring 配置 beans 时,最好使用
id
属性而不是name
属性。为了让 Spring 工作,它们都必须是唯一的,但是使用id
属性允许 XML 验证器强制执行。
注意commit-interval
属性很重要。在示例中设置为 50。这意味着在读取和处理 50 条记录之前,不会写入任何记录。如果在处理 49 个项目后出现错误,Spring Batch 将回滚当前块(事务)并将作业标记为失败。如果您将 commit-interval 值设置为 1,您的作业将读入一个项目,处理该项目,然后写入该项目。本质上,您将回到基于项目的处理。这样做的问题是,在提交时间间隔内,不仅仅只有一个项目被持久化。作业的状态也在作业存储库中更新。您将在本书的后面实验提交间隔,但是您现在需要知道将commit-interval
设置得尽可能高是很重要的。
了解其他类型的微线程
尽管您的大多数步骤都是基于块的处理,因此使用了ChunkOrientedTasklet
,但这不是唯一的选择。Spring Batch 提供了另外三个Tasklet
接口的实现:CallableTaskletAdapter
、MethodInvokingTaskletAdapter
和SystemCommandTasklet
。先来看CallableTaskletAdapter
。
CallableTaskletAdapter
org . spring framework . batch . core . step . tasklet .CallableTaskletAdapter
是一个适配器,允许您配置java.util.concurrent.Callable<RepeatStatus>
接口的实现。如果你不熟悉这个新的接口,Callable<V>
接口与java.lang.Runnable
接口相似,都是为了在一个新线程中运行。然而,不像Runnable
接口不返回值,也不能抛出检查过的异常,而Callable
接口可以返回值(本例中为RepeatStatus
)并抛出检查过的异常。
该适配器的实现实际上非常简单。它在你的Callable
对象上调用call()
方法,并返回call()
方法返回的值。就是这样。显然,如果您想在另一个线程中执行您的步骤的逻辑,而不是在执行该步骤的线程中,您会使用这个方法。如果你查看清单 4-24 中的,你会发现要使用这个适配器,你需要将CallableTaskletAdapter
配置成一个普通的 Spring bean,然后在tasklet
标签中引用它。在清单 4-24 所示的CallableTaskletAdapter
bean 的配置中,CallableTaskletAdapter
包含一个单一的依赖:可调用对象本身。
清单 4-24。使用CallableTaskletAdapter
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="callableObject"
class="com.apress.springbatch.chapter4.CallableLogger"/>
<beans:bean id="callableTaskletAdapter"
class="org.springframework.batch.core.step.tasklet.CallableTaskletAdapter">
<beans:property name="callable" ref="callableObject"/>
</beans:bean>
</beans:beans>`
关于CallableTaskletAdapter
需要注意的一点是,尽管小任务是在不同于步骤本身的线程中执行的,但这并不会使步骤的执行并行化。直到Callable
对象返回一个有效的RepeatStatus
对象,这一步的执行才算完成。在此步骤被认为完成之前,配置了此步骤的流程中的其他步骤将不会执行。在本书的后面,您将看到如何以多种方式并行处理,包括并行执行步骤。
method vokingtaskletadapter
下一个Tasklet
实现是org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter
。这个类类似于 Spring 框架中许多可用的实用程序类。它允许你在另一个类上执行一个预先存在的方法,作为你工作的一部分。比方说,您已经有了一个服务,它执行您想要在批处理作业中运行一次的逻辑。您可以使用MethodInvokingTaskletAdapter
来调用方法,而不是编写真正包装该方法调用的Tasklet
接口的实现。清单 4-25 显示了MethodInvokingTaskletAdapter
的配置示例。
清单 4-25。使用MethodInvokingTaskletAdapter
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="service"
class="com.apress.springbatch.chapter4.ChapterFourService"/>
<beans:bean id="methodInvokingTaskletAdapter"
class="org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter">
<beans:property name="targetObject" ref="service"/>
<beans:property name="targetMethod" value="serviceMethod"/>
</beans:bean>
</beans:beans>`
清单 4-25 所示的例子指定了一个对象和一个方法。使用这种配置,适配器调用不带参数的方法并返回一个ExitStatus.COMPLETED
结果,除非指定的方法也返回类型org.springframework.batch.core.ExitStatus
。如果它确实返回了一个ExitStatus
,那么该方法返回的值将从微线程返回。如果你想配置一组静态参数,你可以使用在本章前面读到的传递作业参数的后期绑定方法,如清单 4-26 所示。
清单 4-26。使用MethodInvokingTaskletAdapter
和参数
`beans:bean id="methodInvokingTaskletAdapter"
class="org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter"
scope="step">
<beans:property name="targetObject" ref="service"/>
<beans:property name="targetMethod" value="serviceMethod"/>
<beans:property name="arguments" value="#{jobParameters[message]}"/>
</beans:bean>
</beans:beans>`
系统命令任务板
Spring Batch 提供的最后一类Tasklet
实现是org.springframework.batch.core.step.tasklet.SystemCommandTasklet
。这个小任务用来——你猜对了——执行一个系统命令!指定的系统命令异步执行。因此,清单 4-27 中的所示的超时值(以毫秒为单位)非常重要。清单中的interruptOnCancel
属性是可选的,但是它向 Spring Batch 表明,如果作业异常退出,是否要终止与系统进程相关联的线程。
清单 4-27。使用SystemCommandTasklet
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml" />
<beans:bean id="tempFileDeletionCommand"
class="org.springframework.batch.core.step.tasklet.SystemCommandTasklet">
<beans:property name="command" value="rm – rf /temp.txt " />
<beans:property name="timeout" value="5000" />
<beans:property name="interruptOnCancel" value="true" />
</beans:bean>
</beans:beans>`
SystemCommandTasklet
允许您配置一些可能影响系统命令执行方式的参数。清单 4-28 显示了一个更健壮的例子。
清单 4-28。使用SystemCommandTasklet
与全环境配置
`<beans:bean id="touchCodeMapper"
class="org.springframework.batch.core.step.tasklet.SimpleSystemProcessExitCodeMapper"/>
<beans:bean id="taskExecutor"
class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
<beans:bean id="robustFileDeletionCommand"
class="org.springframework.batch.core.step.tasklet.SystemCommandTasklet">
<beans:property name="command" value="touch temp.txt" />
<beans:property name="timeout" value="5000" />
<beans:property name="interruptOnCancel" value="true" />
<beans:property name="workingDirectory"
value="/Users/mminella/spring-batch" />
<beans:property name="systemProcessExitCodeMapper"
ref="touchCodeMapper"/>
<beans:property name="terminationCheckInterval" value="5000" />
<beans:property name="taskExecutor" ref="taskExecutor" />
<beans:property name="environmentParams"
value="JAVA_HOME=/java,BATCH_HOME=/Users/batch" />
</beans:bean>
</beans:beans>`
清单 4-28 包括配置中的五个可选参数:
workingDirectory
:这是执行命令的目录。在本例中,这相当于在执行实际命令之前先执行cd ˜/spring-batch
。根据你正在执行的命令,系统代码可能有不同的含义。该属性允许您使用
org.springframework.batch.core.step.tasklet.SystemProcessExitCodeMapper
接口的实现来映射什么系统返回代码与什么 Spring 批处理状态值。默认情况下,Spring 提供了该接口的两个实现:org.springframework.batch.core.step.tasklet.ConfigurableSystemProcessExitCodeMapper
,它允许您在 XML 配置中配置映射;以及org.springframework.batch.core.step.tasklet.SimpleSystemProcessExitCodeMapper
,如果返回代码是 0,则返回ExitStatus.FINISHED
,如果是其他代码,则返回ExitStatus.FAILED
。
terminationCheckInterval
:因为默认情况下系统命令是以异步方式执行的,所以小任务会定期检查是否已经完成。默认情况下,该值设置为一秒,但是您可以将其配置为以毫秒为单位的任何值。
taskExecutor
: 这允许你配置你自己的TaskExecutor
来执行系统命令。我们不鼓励您配置同步任务执行器,因为如果系统命令导致问题,可能会锁定您的作业。
environmentParams
: 这是在执行命令之前可以设置的环境参数列表。
在上一节中,您已经看到了 Spring Batch 中有许多不同的小任务类型。然而,在离开主题之前,还有一个小任务类型需要讨论:小任务步骤。
小任务步骤
tasklet 步骤与您看到的其他步骤不同。但它应该是你最熟悉的,因为它是你在《你好,世界!工作。不同之处在于,在这种情况下,您编写自己的代码作为微线程执行。使用MethodInvokingTaskletAdapter
是定义小任务步骤的一种方式。在这种情况下,您允许 Spring 将处理转发到您的代码。这让您可以开发常规的 POJOs,并将它们用作步骤。
创建小任务步骤的另一种方法是实现Tasklet
接口,就像在第二章的中创建HelloWorld
小任务一样。在那里,您实现了接口中所需的execute
方法,并返回一个RepeatStatus
对象来告诉 Spring Batch 在您完成处理后要做什么。清单 4-29 有你在第二章中构建的HelloWorld
小任务代码。
清单 4-29。 HelloWorld
小任务
`package com.apress.springbatch.chapter2;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
public class HelloWorld implements Tasklet {
private static final String HELLO_WORLD = "Hello, world!";
public RepeatStatus execute( StepContribution arg0,
ChunkContext arg1 ) throws Exception {
System.out.println( HELLO_WORLD );
return RepeatStatus.FINISHED;
}
}`
当您的Tasklet
实现中的处理完成时,您返回一个org.springframework.batch.repeat.RepeatStatus
对象。这里有两个选项:RepeatStatus.CONTINUABLE
和RepeatStatus.FINISHED
。乍一看,这两个值可能会混淆。如果你返回RepeatStatus.CONTINUABLE
,你不是说工作可以继续。您告诉 Spring Batch 再次运行 tasklet。例如,假设您希望在一个循环中执行一个特定的小任务,直到满足给定的条件,但是您仍然希望使用 Spring Batch 来跟踪该小任务执行了多少次、事务等等。您的微线程可以返回RepeatStatus.CONTINUABLE
,直到条件满足。如果您返回RepeatStatus.FINISHED
,这意味着这个小任务的处理已经完成(不管成功与否),并继续下一个处理。
您可以像配置任何其他小任务类型一样配置小任务步骤。清单 4-30 显示了使用HelloWorld
微线程配置的HelloWorldJob
。
清单 4-30。HelloWorldJob
`<beans:bean id="helloWorld" class="com.apress.springbatch.chapter2.HelloWorld/>
…`
你可能很快会指出这个列表与第二章中的不一样,你可能是对的。原因是你还没有看到第二章中使用的另一个特性:分步继承。
分步继承
像作业一样,步骤可以相互继承。不像乔布斯,步骤不一定要抽象才能继承。Spring Batch 允许您在配置中配置完全定义的步骤,然后让其他步骤继承它们。让我们通过查看在第二章的中使用的例子,清单 4-31 中的HelloWorldJob
来开始讨论步骤继承。
清单 4-31。??HelloWorldJob
`<beans:bean id="helloWorld"
class="com.apress.springbatch.chapter2.HelloWorld"/>
在清单 4-31 的中,您配置了小任务实现(helloWorld
bean),然后您配置了引用小任务的步骤(helloWorldStep
)。Spring Batch 不要求step
元素嵌套在job
标签中。一旦您定义了您的步骤helloWorldStep
,那么当您在实际工作中按顺序声明步骤时,您可以继承它helloWorldJob
。你为什么要这么做?
在这个简单的例子中,这种方法没有什么好处。然而,随着步骤变得越来越复杂,经验表明,最好在工作范围之外配置您的步骤,然后用工作中的步骤继承它们。这使得实际的作业声明更具可读性和可维护性。
显然,可读性不是使用继承的唯一原因,即使在这个例子中也不是这样。让我们潜入更深的地方。在这个例子中,您在step1
中真正做的是继承步骤helloWorldStep
及其所有属性。然而,step1
选择不覆盖其中任何一个。
步骤继承提供了比作业继承更完整的继承模型。在步骤继承中,您可以完全定义一个步骤,继承该步骤,然后添加或覆盖您希望的任何值。您也可以将一个步骤声明为抽象的,只在那里放置公共属性。
清单 4-32 显示了步骤如何添加和覆盖其父级中配置的属性的例子。您从父步骤vehicleStep
开始,它声明了读取器、写入器和提交间隔。然后创建两个继承自vehicleStep
的步骤:carStep
和truckStep
。每个都使用已经在vehicleStep
中配置的相同的读取器和写入器。在每种情况下,他们都添加了一个做不同事情的项目处理器。carStep
选择使用继承的 50 个项目的提交间隔,而truckStep
覆盖了提交间隔并将其设置为 5 个项目。
清单 4-32。在步骤继承中添加属性
`
通过声明一个抽象的步骤,就像在 Java 中一样,你可以省略掉原本需要的东西。在一个抽象步骤中,如清单 4-33 ,你可以省略reader
、writer
、processor
和tasklet
属性。这通常会在 Spring 试图构建该步骤时导致初始化错误;但是因为它被声明为抽象的,Spring 知道这些将被继承它的步骤填充。
清单 4-33。一个抽象步骤及其实现
`<beans:bean id="inputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[inputFile]}"/>
</beans:bean>
<beans:bean id="outputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[outputFile]}"/>
</beans:bean>
<beans:bean id="inputReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" ref="inputFile"/>
<beans:property name="lineMapper">
<beans:bean
class="org.springframework.batch.item.file.mapping.PassThroughLineMapper"/>
</beans:property>
</beans:bean>
<beans:bean id="outputWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter">
<beans:property name="resource" ref="outputFile"/>
<beans:property name="lineAggregator">
<beans:bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
</beans:property>
</beans:bean>
</beans:beans>`
在清单 4-33 ,commitIntervalStep
是一个抽象步骤,用于为扩展该步骤的任何步骤配置提交间隔。您在扩展抽象步骤copyStep
的步骤中配置所需的元素。您可以在这里指定读取器和写入器。copyStep
与commitIntervalStep
具有相同的提交间隔 15,无需重复配置。
步骤继承允许您配置可以在各个步骤中重用的公共属性,并以可维护的方式构建 XML 配置。本节的最后一个例子使用了几个特定于块的属性。为了更好地理解它们,让我们回顾一下如何使用 Spring Batch 在基于块的处理中提供的不同特性。
块大小配置
因为基于块的处理是 Spring Batch 2 的基础,所以理解如何配置它的各种选项以充分利用这一重要特性非常重要。本节涵盖了配置块大小的两个选项:静态提交计数和CompletionPolicy
实现。所有其他块配置选项都与错误处理相关,将在该部分中讨论。
开始看块配置,清单 4-34 有一个基本的例子,只不过是配置了一个读取器、写入器和提交间隔。读取器是ItemReader
接口的实现,编写器是ItemWriter
的实现。在本书后面的章节中,每个接口都有自己的专用章节,所以本节不详细介绍它们。所有你需要知道的是他们分别为这个步骤提供输入和输出。commit-interval 定义了一个块中有多少项(在本例中是 50 项)。
清单 4-34。一个基本的组块配置
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml" />
<beans:bean id="inputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[inputFile]}"/>
</beans:bean>
<beans:bean id="outputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[outputFile]}"/>
</beans:bean>
<beans:bean id="inputReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" ref="inputFile"/>
<beans:property name="lineMapper">
<beans:bean class="org.springframework.batch.item.file.mapping.PassThroughLineMapper"/>
</beans:property>
</beans:bean>
<beans:bean id="outputWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter">
<beans:property name="resource" ref="outputFile"/>
<beans:property name="lineAggregator">
<beans:bean
class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
</beans:property>
</beans:bean>
</beans:beans>`
尽管通常你是根据一个硬数字来定义块的大小,这个硬数字是用清单 4-34 中的commit-interval
属性配置的,但这并不总是一个足够健壮的选项。假设您有一项工作需要处理大小不同的块(例如,在一个事务中处理一个帐户的所有事务)。Spring Batch 提供了通过实现org.springframework.batch.repeat.CompletionPolicy
接口以编程方式定义块何时完成的能力。
CompletionPolicy
接口允许决策逻辑的实现来决定一个给定的块是否完整。Spring Batch 附带了这个接口的许多实现。默认情况下,它使用org.springframework.batch.repeat.policy.SimpleCompletionPolicy
,它计算处理的项目数,并在达到配置的阈值时标记块完成。另一个开箱即用的实现是org.springframework.batch.repeat.policy.TimeoutTerminationPolicy
。这允许您在块上配置超时,以便它可以在给定的时间后优雅地退出。“优雅地退出”在这个上下文中是什么意思?这意味着该块被认为是完整的,所有事务处理正常继续。
正如您无疑可以推断的那样,很少有时候超时本身就足以决定一个处理块何时完成。TimeoutTerminationPolicy
更有可能被用作org.springframework.batch.repeat.policy.CompositeCompletionPolicy
的一部分。此策略允许您配置多个策略来确定区块是否已完成。当您使用CompositeCompletionPolicy
时,如果任何策略认为一个块是完整的,那么这个块被标记为完整的。清单 4-35 显示了一个使用 3 毫秒的超时和 200 个项目的正常提交计数来确定块是否完成的例子。
清单 4-35。使用超时和常规提交计数
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml" />
<beans:bean id="inputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[inputFile]}" />
</beans:bean>
<beans:bean id="outputFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[outputFile]}" />
</beans:bean>
<beans:bean id="inputReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" ref="inputFile" />
<beans:property name="lineMapper">
<beans:bean class="org.springframework.batch.item.file.mapping.PassThroughLineMapper" />
</beans:property>
</beans:bean>
<beans:bean id="outputWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter">
<beans:property name="resource" ref="outputFile" />
<beans:property name="lineAggregator">
<beans:bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator" />
</beans:property>
</beans:bean>
<beans:bean id="chunkTimeout"
class="org.springframework.batch.repeat.policy.TimeoutTerminationPolicy">
<beans:constructor-arg value="3" />
</beans:bean>
<beans:bean id="commitCount"
class="org.springframework.batch.repeat.policy.SimpleCompletionPolicy">
<beans:property name="chunkSize" value="200" />
</beans:bean>
<beans:bean id="chunkCompletionPolicy"
class="org.springframework.batch.repeat.policy.CompositeCompletionPolicy">
<beans:property name="policies">
util:list
<beans:ref bean="chunkTimeout" />
<beans:ref bean="commitCount" />
</util:list>
</beans:property>
</beans:bean>
</beans:beans>`
使用CompletionPolicy
接口的实现并不是确定一个块有多大的唯一选择。也可以自己实现。在您查看实现之前,让我们先看一下接口。
CompletionPolicy
接口需要四个方法:两个版本的isComplete
、start
和update
。如果从类的生命周期来看,首先是调用了start
方法。该方法初始化策略,以便它知道块正在启动。重要的是要注意到,CompletionPolicy
接口的实现是有状态的,并且应该能够通过它自己的内部状态来确定块是否已经完成。start
方法在程序块开始时将这个内部状态重置为实现所需的状态。以SimpleCompletionPolicy
为例,start
方法在块开始时将内部计数器重置为 0。对于每一个已经被处理的条目,调用一次update
方法来更新内部状态。回到SimpleCompletionPolicy
的例子,update
每增加一项,内部计数器就加 1。最后,还有两个isComplete
方法。第一个isComplete
方法签名接受一个RepeatContext
作为它的参数。该实现旨在使用其内部状态来确定块是否已经完成。第二个签名将RepeatContext
和RepeatStatus
作为参数。该实现被期望基于状态来确定块是否已经完成。清单 4-36 显示了一个CompletionPolicy
实现的例子,一旦处理了少于 20 个的任意数量的项目,就认为块完成;清单 4-37 显示了配置。
清单 4-36。随机块大小CompletionPolicy
实现
`package com.apress.springbatch.chapter4;
import java.util.Random;
import org.springframework.batch.repeat.CompletionPolicy;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.RepeatStatus;
public class RandomChunkSizePolicy implements CompletionPolicy {
private int chunkSize;
private int totalProcessed;
public boolean isComplete(RepeatContext context) {
return totalProcessed >= chunkSize;
}
public boolean isComplete(RepeatContext context, RepeatStatus status) {
if (RepeatStatus.FINISHED == status) {
return true;
} else {
return isComplete(context);
}
}
public RepeatContext start(RepeatContext context) {
Random random = new Random();
chunkSize = random.nextInt(20);
totalProcessed = 0;
System.out.println("The chunk size has been set to " + chunkSize);
return context;
}
public void update(RepeatContext context) {
totalProcessed++;
}
}`
清单 4-37。配置RandomChunkSizePolicy
`<beans:bean id="randomChunkSizer"
class="com.apress.springbatch.chapter4.RandomChunkSizePolicy" />
当您进行错误处理时,您将探索块配置的其余部分。这一节介绍了重试和跳过逻辑,其余大多数选项都围绕着这一点。本章讨论的下一步元素也是从工作中延续下来的:听众。
步骤监听器
在本章前面,当您查看作业侦听器时,您看到了它们可以触发的两个事件:作业的开始和结束。步骤侦听器涵盖相同类型的事件(开始和结束),但是针对单个步骤,而不是整个作业。本节涵盖了org.springframework.batch.core.StepExecutionListener
和org.springframework.batch.core.ChunkListener
接口,这两个接口分别允许在一个步骤和程序块的开始和结束时处理逻辑。注意,该步骤的侦听器被命名为StepExecutionListener
,而不仅仅是StepListener
。实际上有一个StepListener
接口,不过它只是一个标记接口,所有与步骤相关的侦听器都会扩展它。
StepExecutionListener
和ChunkListener
都提供了类似于JobExecutionListener
接口中的方法。StepExecutionListener
有一个beforeStep
和一个afterStep
,而ChunkListener
有一个beforeChunk
和一个afterChunk
,正如你所料。这些方法除了afterStep
都是无效的。afterStep
返回一个ExitStatus
,因为监听器被允许在步骤返回到作业之前修改步骤本身返回的ExitStatus
。当作业不仅仅需要知道操作是否成功来确定处理是否成功时,此功能非常有用。例如,在导入文件后进行一些基本的完整性检查(是否将正确数量的记录写入数据库,等等)。通过注释配置监听器的能力也保持一致,Spring Batch 提供了 @BeforeStep
、@AfterStep
、@BeforeChunk
和@AfterChunk
注释来简化实现。清单 4-38 显示了一个StepListener
,它使用注释来标识方法。
清单 4-38。日志记录步骤开始和停止监听器
`package com.apress.springbatch.chapter4;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.core.annotation.BeforeStep;
public class LoggingStepStartStopListener {
@BeforeStep
public void beforeStep(StepExecution execution) {
System.out.println(execution.getStepName() + “ has begun!");
}
@AfterStep
public ExitStatus afterStep(StepExecution execution) {
System.out.println(execution.getStepName() + “ has ended!");
return execution.getExitStatus();
}
}`
在步骤配置中,所有步骤侦听器的配置被合并到一个列表中。与作业监听器相似,继承也以同样的方式工作,允许您覆盖列表或将它们合并在一起。清单 4-39 配置您之前编码的LoggingStepStartStopListener
。
清单 4-39 。配置LoggingStepStartStopListener
`...
<beans:bean id="loggingStepListener"
class="com.apress.springbatch.chapter4.LoggingStepStartStopListener"/>
正如您所看到的,监听器几乎在 Spring Batch 框架的每一层都可用,允许您挂起批处理作业的处理。它们通常不仅用于在组件之前执行某种形式的预处理或评估组件的结果,还用于错误处理,正如您在一点中看到的。
下一节将介绍这些步骤的流程。尽管到目前为止所有的步骤都是按顺序处理的,但这并不是 Spring Batch 的要求。您将学习如何执行简单逻辑来确定下一步要执行的步骤,以及如何将流外部化以便重用。
步骤流程
单行:这就是你的工作到目前为止的样子。您已经排列了这些步骤,并使用next
属性允许它们一个接一个地执行。然而,如果这是执行步骤的唯一方式,Spring Batch 将会非常有限。相反,该框架的写入器提供了一个强大的选项集合,用于定制您的作业流。
首先,让我们看看如何决定下一步执行什么步骤,或者是否执行给定的步骤。这是使用 Spring Batch 的条件逻辑实现的。
条件逻辑
在 Spring Batch 的一个作业中,步骤按照您使用step
标签的next
属性指定的顺序执行。唯一的要求是将第一步配置为作业中的第一步。如果你想以不同的顺序执行步骤,这很容易:你需要做的就是使用next
标签。如清单 4-40 所示,如果一切顺利,你可以使用next
标签来指示一个任务从step1
转到step2a
,或者如果step1
返回FAILED
的ExitStatus
则转到step2b
。
清单 4-40。 If
/ Else
逻辑步进执行
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="passTasklet"
class="com.apress.springbatch.chapter4.LogicTasklet">
<beans:property name="success" value="true"/>
</beans:bean>
<beans:bean id="successTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step succeeded!"/>
</beans:bean>
<beans:bean id="failTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step failed!"/>
</beans:bean>
</beans:beans>`
next
标签使用on
属性来评估步骤的ExitStatus
并决定做什么。值得注意的是,在本章的课程中,你已经看到了org.springframework.batch.core.ExitStatus
和org.springframework.batch.core.BatchStatus
。BatchStatus
是JobExecution
或StepExecution
的一个属性,用于识别作业或步骤的当前状态。ExitStatus
是在作业或步骤结束时返回到 Spring Batch 的值。on
属性评估ExitStatus
的决策。因此,清单 4-40 中的例子相当于这样的 XML 语句:“如果step1
的退出代码不等于FAILED
,则转到step2a
,否则转到step2b
”
因为ExitStatus
的值实际上只是String
的值,所以使用通配符的能力可以让事情变得有趣。Spring Batch 允许在on
标准中使用两个通配符:
*
匹配零个或多个字符。比如C*
匹配 C ,完成,正确。?
匹配单个字符。在这种情况下,?AT
与猫或猫匹配,但与不匹配。
虽然评估ExitStatus
让你开始决定下一步做什么,但它可能不会带你走完全程。例如,如果您跳过了当前步骤中的任何记录,而不想执行某个步骤,该怎么办?光从ExitStatus
你是不会知道的。
注意 Spring Batch 在配置转换时会帮到你。它自动将转换从最严格到最不严格排序,并按此顺序应用它们。
Spring Batch 提供了一种确定下一步做什么的编程方式。您可以通过创建一个org.springframework.batch.core.job.flow.JobExecutionDecider
接口的实现来做到这一点。这个接口有一个方法decide
,它接受JobExecution
和StepExecution
并返回一个FlowExecutionStatus
(一个BatchStatus
/ ExitStatus
对的包装)。有了JobExecution
和StepExecution
可供评估,你就应该可以获得所有的信息,以便对你的工作下一步该做什么做出适当的决定。清单 4-41 显示了随机决定下一步应该做什么的JobExecutionDecider
的实现。
清单 4-41。??RandomDecider
`package com.apress.springbatch.chapter4;
import java.util.Random;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
public class RandomDecider implements JobExecutionDecider {
private Random random = new Random();
public FlowExecutionStatus decide(JobExecution jobExecution,
StepExecution stepExecution) {
if (random.nextBoolean()) {
return new
FlowExecutionStatus(FlowExecutionStatus.COMPLETED.getName());
} else {
return new
FlowExecutionStatus(FlowExecutionStatus.FAILED.getName());
}
}
}`
要使用RandomDecider
,您需要在您的步骤上配置一个额外的属性,称为decider
。这个属性指的是实现JobExecutionDecider
的 Spring bean。清单 4-42 显示了RandomDecider
的配置。您可以看到配置将您在决策器中返回的值映射到可执行的步骤。
清单 4-42。 If
/ Else
逻辑步进执行
`...
<beans:bean id="decider"
class="com.apress.springbatch.chapter4.RandomDecider"/>
<beans:bean id="successTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step succeeded!"/>
</beans:bean>
<beans:bean id="failTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step failed!"/>
</beans:bean>
因为您现在知道了如何按顺序或通过逻辑来指导您的处理,所以您不会总是想直接进入另一个步骤。您可能希望结束或暂停作业。下一节将介绍如何处理这些场景。
结束工作
您在前面已经了解到,一个JobInstance
不能被多次执行直到成功完成,并且一个JobInstance
由作业名和传递给它的参数来标识。因此,如果以编程方式完成作业,您需要知道作业结束时的状态。实际上,在 Spring Batch 中,有三种状态可以通过编程结束作业:
Completed: 这个结束状态告诉 Spring Batch 处理已经成功结束。当
JobInstance
完成时,不允许使用相同的参数重新运行。失败:在这种情况下,作业没有成功运行完成。Spring Batch 允许使用相同的参数重新运行处于失败状态的作业。
停止:在停止状态下,作业可以重新启动。停止的作业的有趣之处在于,尽管没有发生错误,但作业可以从停止的地方重新启动。在步骤之间需要人工干预或其他检查或处理的情况下,这种状态非常有用。
值得注意的是,这些状态是通过 Spring Batch 评估步骤的ExitStatus
来确定在JobRepository
中持久化什么BatchStatus
来识别的。ExitStatus
可以从步骤、程序块或作业中返回。BatchStatus
保持在StepExecution
或JobExecution
并持续在JobRepository
。让我们开始看看如何用 completed 状态结束每个状态中的作业。
要根据某个步骤的退出状态配置作业以完成状态结束,可以使用end
标记。在这种状态下,您不能使用相同的参数再次执行相同的作业。清单 4-43 显示了end
标签有一个属性,它声明了触发作业结束的ExitStatus
值。
清单 4-43。结束处于完成状态的作业
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
`xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="passTasklet"
class="com.apress.springbatch.chapter4.LogicTasklet">
<beans:property name="success" value="false"/>
</beans:bean>
<beans:bean id="successTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step succeeded!"/>
</beans:bean>
<beans:bean id="failTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step failed!"/>
</beans:bean>
</beans:beans>`
一旦运行了conditionalStepLogicJob
,如您所料,batch_step_execution
表包含了该步骤返回的ExitStatus
,而batch_job_execution
包含了COMPLETED
,与所采用的路径无关。
对于失败状态,允许您使用相同的参数重新运行作业,配置看起来类似。不使用end
标签,而是使用fail
标签。清单 4-44 显示了fail
标签有一个额外的属性:exit-code
。它允许您在导致作业失败时添加额外的详细信息。
清单 4-44。在失败状态下结束作业
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
`<beans:import resource="../launch-context.xml"/>
<beans:bean id="passTasklet"
class="com.apress.springbatch.chapter4.LogicTasklet">
<beans:property name="success" value="true"/>
</beans:bean>
<beans:bean id="successTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step succeeded!"/>
</beans:bean>
<beans:bean id="failTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step failed!"/>
</beans:bean>
</beans:beans>`
当你用清单 4-44 中的配置重新运行conditionalStepLogicJob
时,结果会有点不同。这一次,如果step1
以ExitStatus
FAILURE
结束,作业在jobRepository
中被识别为失败,这允许它以相同的参数重新执行。
当以编程方式结束作业时,作业可以处于的最后一种状态是停止状态。在这种情况下,您可以重新启动作业;当您这样做时,它会在您配置的步骤重新启动。清单 4-45 显示了一个例子。
清单 4-45。在停止状态下结束作业
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="passTasklet"
class="com.apress.springbatch.chapter4.LogicTasklet">
<beans:property name="success" value="true"/>
</beans:bean>
<beans:bean id="successTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step succeeded!"/>
</beans:bean>
<beans:bean id="failTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The step failed!"/>
</beans:bean>
</beans:beans>`
使用这个最终配置执行conditionalStepLogicJob
,如清单 4-45 中的,允许您使用相同的参数重新运行作业。然而,这一次,如果选择了FAILURE
路径,当作业重新启动时,执行从step2a
开始。
从一个步骤到下一个步骤的流程不仅仅是您添加到潜在复杂工作配置中的另一层配置;它也可以在一个可重用的组件中配置。下一节讨论如何将步骤流封装成可重用的组件。
外部化流程
您已经认识到一个步骤不需要在 XML 的job
标记中配置。这让您可以从给定的作业中将步骤的定义提取到可重用的组件中。步骤的顺序也是如此。在 Spring Batch 中,对于如何外部化步骤的顺序,有三个选项。首先是创建一个流程,这是一个独立的步骤序列。二是用流水步;虽然配置非常相似,但是JobRepository
中的状态持久性略有不同。最后一种方法是从您的作业中调用另一个作业。本节将介绍这三个选项的工作原理。
流程看起来很像一项工作。它以同样的方式配置,但是用一个flow
标签代替了一个job
标签。清单 4-46 展示了如何使用flow
标签定义一个流,给它一个 id,然后在你的工作中使用flow
标签引用它。
清单 4-46。定义流程
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans
`http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="loadStockFile"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message"
value="The stock file has been loaded"/>
</beans:bean>
<beans:bean id="loadCustomerFile"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message"
value="The customer file has been loaded" />
</beans:bean>
<beans:bean id="updateStart"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message"
value="The stock file has been loaded" />
</beans:bean>
<beans:bean id="runBatchTasklet"
class="com.apress.springbatch.chapter4.MessageTasklet">
<beans:property name="message" value="The batch has been run" />
</beans:bean>
</beans:beans>`
当您将流程作为作业的一部分执行并查看jobRepository
时,您会看到流程中的步骤被记录为作业的一部分,就好像它们是在第一个位置配置的一样。最后,从JobRepository
的角度来看,使用流程和在工作本身中配置步骤没有区别。
外部化步骤的下一个选项是使用流程步骤。使用这种技术,流的配置是相同的。但是您没有使用flow
标签将流程包含在您的作业中,而是使用了一个step
标签及其flow
属性。清单 4-47 演示了如何使用一个流程步骤来配置与清单 4-46 所使用的相同的示例。
清单 4-47。使用流程步骤
`...
使用flow
标签和 flow step 有什么区别?这取决于在JobRepository
中发生了什么。使用flow
标签的结果与您在工作中配置步骤的结果相同。使用流程步骤会添加一个附加条目。当您使用流程步骤时,Spring Batch 会将包含流程的步骤记录为一个单独的步骤。为什么这是一件好事?主要好处是用于监控和报告目的。使用流程步骤允许您从整体上查看流程的影响,而不必汇总各个步骤。
将步骤发生的顺序外部化的最后一种方法是根本不要将它们外部化。在这种情况下,您不是创建流,而是从另一个作业中调用一个作业。与流程步骤相似,流程步骤为流程和流程中的每个步骤的执行创建一个StepExecutionContext
,作业步骤为调用外部作业的步骤创建一个JobExecutionContext
。清单 4-48 显示了一个工作步骤的配置。
清单 4-48。使用作业步骤
`
<beans:bean id="jobParametersExtractor"
class="org.springframework.batch.core.step.job.DefaultJobParametersExtractor">
<beans:property name="keys" value="job.stockFile,job.customerFile"/>
</beans:bean>
您可能想知道清单 4-48 中的jobParametersExtractor
bean。启动作业时,它由作业名称和作业参数来标识。在这种情况下,您没有手工将参数传递给子作业preProcessingJob
。相反,您定义一个类来从父作业的JobParameters
或ExecutionContext
(DefaultJobParameterExtractor
检查两个地方)中提取参数,并将这些参数传递给子作业。您的提取器从job.stockFile
和job.customerFile
作业参数中提取值,并将这些值作为参数传递给preProcessingJob
。
当preProcessingJob
执行时,它在JobRepository
中被识别,就像任何其他作业一样。它有自己的作业实例、执行上下文和相关的数据库记录。
关于使用作业步骤方法的一个警告:这似乎是处理作业依赖的一个好方法。创建单个作业,然后将它们与主作业连接在一起,这是一个强大的功能。然而,这可能会严重限制流程执行时的控制。在现实世界中,基于外部因素需要暂停批处理周期或跳过作业的情况并不少见(另一个部门无法及时为您获取文件以在要求的窗口内完成流程,等等)。但是,管理作业的能力存在于单个作业级别。管理使用此功能创建的整个作业树是有问题的,应该避免。以这种方式将作业链接在一起并作为一个主作业执行会严重限制处理这类情况的能力,因此也应该避免。
流难题的最后一块是 Spring Batch 提供的并行执行多个流的能力,这将在接下来讨论。
流程并行化
虽然您将在本书后面学习并行化,但是本节将介绍并行执行步骤流的 Spring 批处理功能。使用 Java 进行批处理和 Spring Batch 提供的工具的优势之一是能够以标准化的方式将多线程处理引入批处理世界。并行执行步骤的最简单方法之一是在您的作业中拆分它们。
分割流是一个允许您列出想要并行执行的流的步骤。每个流同时启动,直到其中的所有流都完成了,这个步骤才算完成。如果任何一个流失败,则分流被认为已经失败。要了解分割步骤是如何工作的,请看清单 4-49 。
清单 4-49。使用分割步骤的并行流
<job id="flowJob"> <split id="preprocessingStep" next="batchStep"> <flow> <step id="step1" parent="loadStockFile" next="step2"/> <step id="step2" parent="loadCustomerFile"/>
</flow> <flow> <step id="step3" parent="loadTransactionFile"/> </flow> </split> <step id="batchStep" parent="runBatch"/> </job>
在清单 4-49 中,你识别出两个独立的流:一个加载两个文件,一个加载一个文件。这些流程中的每一个都是并行执行的。三个步骤(step1
、step2
、step3
全部完成后,Spring Batch 执行batchStep
。
就这样。令人惊讶的是,使用 Spring Batch 进行基本并行化是如此简单,如本例所示。鉴于潜在的性能提升, 7 您可以开始明白为什么 Spring Batch 可以成为高性能批处理中非常有效的工具。
后面的章节涵盖了各种错误处理场景,包括从整个作业级别到事务级别的错误处理。但是因为步骤都是关于处理大块的项目,所以下一个主题是处理单个项目时可用的一些错误处理策略。
物品错误处理
Spring Batch 2 基于基于块的处理的概念。因为块是基于事务提交边界的,所以本书在第ItemReader
和ItemWriter
章讨论了如何处理块的错误。然而,个别项目通常是错误的原因,Spring Batch 提供了一些项目级的错误处理策略供您使用。具体来说,Spring Batch 允许您跳过某个项目的处理,或者在失败后再次尝试处理某个项目。
让我们从在你通过跳过放弃一个项目之前重试处理它开始。
项目重试
当您处理大量数据时,由于不需要人工干预的事情而出现错误并不罕见。如果跨系统共享的数据库出现死锁,或者 web 服务调用由于网络故障而失败,那么停止处理数百万个项目是处理这种情况的一种非常激烈的方式。更好的方法是允许您的作业再次尝试处理给定的项目。
在 Spring Batch 中实现重试逻辑有三种方式:配置重试次数,使用RetryTemplate
,使用 Spring 的 AOP 特性。第一种方法让 Spring Batch 定义一些允许的重试尝试和触发新尝试的异常。清单 4-50 显示了在执行remoteStep
的过程中抛出RemoteAccessException
时,项目的基本重试配置。
7 并非所有的并行化都会带来性能的提升。在不正确的情况下,并行执行步骤会对您的工作性能产生负面影响。
清单 4-50。基本重试配置
<job id="flowJob"> <step id="retryStep"> <tasklet> <chunk reader="itemReader" writer="itemWriter" processor="itemProcessor" commit-interval="20" retry-limit="3"> <retryable-exception-classes> <include class="org.springframework.remoting.RemoteAccessException"/> </retryable-exception-classes> </chunk> </tasklet> </step> </job>
在flowJob
的retryStep
中,当步骤(itemReader
、itemWriter
或itemProcessor
)中的任何组件抛出RemoteAccessException
时,该项目在步骤失败前最多重试三次。
向批处理作业添加重试逻辑的另一种方式是通过org.springframework.batch.retry.RetryTemplate
自己完成。像 Spring 中提供的大多数其他模板一样,这个模板通过提供一个简单的 API 将可重试的逻辑封装在一个方法中,然后由 Spring 管理,从而简化了重试逻辑的开发。在RetryTemplate
的情况下,你需要开发两个部分:org.springframework.batch.retry.RetryPolicy
接口和org.springframework.batch.retry.RetryCallback
接口。RetryPolicy
允许您定义在什么条件下重试项目的处理。Spring 提供了许多实现,包括基于抛出异常的重试(在清单 4-50 中默认使用)、超时等等。编码重试逻辑的另一部分是使用RetryCallback
接口。这个接口提供了一个方法doWithRetry(RetryContext context)
,它封装了要重试的逻辑。当使用RetryTemplate
时,如果要重试一个项目,只要RetryPolicy
指定重试,就会调用doWithRetry
方法。我们来看一个例子。
清单 4-51 显示了使用 30 秒超时策略重试数据库调用的代码。这意味着它将继续尝试执行数据库调用,直到它工作或直到 30 秒过去。
清单 4-51。使用RetryTemplate
和RetryCallback
`package com.apress.springbatch.chapter4;
import java.util.List;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.retry.support.RetryTemplate;
import org.springframework.batch.retry.RetryCallback;
import org.springframework.batch.retry.RetryContext;
public class RetryItemWriter implements ItemWriter
private CustomerDAO customerDao;
private RetryTemplate retryTemplate;
public void write(List<? extends Customer> customers) throws Exception {
for (Customer customer : customers) {
final Customer curCustomer = customer;
retryTemplate.execute(new RetryCallback
public Customer doWithRetry(RetryContext retryContext) {
return customerDao.save(curCustomer);
}
});
}
}
...
}`
清单 4-51 中的代码很大程度上依赖于需要注入的元素(例子中省略了 getters 和 setters)。为了更好地了解这里发生了什么,清单 4-52 显示了这个例子的配置。
清单 4-52。重试CustomerDao
的配置
`<beans:bean id="timeoutPolicy"
class="org.springframework.batch.retry.policy.TimeoutRetryPolicy">
<beans:property name="timeout" value="30000"/>
</beans:bean>
<beans:bean id="timeoutRetryTemplate"
class="org.springframework.batch.retry.support.RetryTemplate">
<beans:property name="retryPolicy" ref="timeoutPolicy"/>
</beans:bean>
<beans:bean id="retryItemWriter"
class="com.apress.springbatch.chapter4.RetryItemWriter">
<beans:property name="customerDao" ref="customerDao"/>
<beans:property name="retryTemplate" ref="timeoutRetryTemplate"/>
</beans:bean>
清单 4-52 中的大多数配置应该是简单明了的。您将org.springframework.batch.retry.policy.TimeoutRetryPolicy
bean 的超时值设置为 30 秒;您将它作为retryPolicy
注入到RetryTemplate
中,并将模板注入到您为清单 4-51 中的编写的ItemWriter
中。然而,关于这个重试的配置,有趣的一点是,作业中没有重试配置。因为您编写了自己的重试逻辑,所以不会在块配置中使用重试限制等。
配置项目重试逻辑的最后一种方法是使用 Spring 的 AOP 工具和 Spring Batch 的org.springframework.batch.retry.interceptor.RetryOperationsInterceptor
来声明性地将重试逻辑应用于批处理作业的元素。清单 4-53 显示了如何声明方面的配置,以将重试逻辑应用于任何save
方法。
清单 4-53。使用 AOP 应用重试逻辑
`aop:config
<aop:pointcut id="saveRetry"
expression="execution(* com.apress.springbatch.chapter4.*.save(..))"/>
<aop:advisor pointcut-ref="saveRetry" advice-ref="retryAdvice"
order="-1"/>
</aop:config>
<beans:bean id="retryAdvice"
class="org.springframework.batch.retry.interceptor.RetryOperationsInterceptor"/>`
这个配置省略了重试次数的定义,等等。要添加这些,您需要做的就是将适当的RetryPolicy
配置到RetryOperationsInterceptor
(它对RetryPolicy
实现有一个可选的依赖)。
RetryPolicy
类似于前面的CompletionPolicy
,因为它允许您以编程方式决定某事——在本例中,何时重试。在 AOP 拦截器或常规重试方法中使用的org.springbatch.retry.RetryPolicy
很重要,但是有一点要注意,如果它不起作用,您可能不想一次又一次地重试。例如,当谷歌的 Gmail 无法连接回服务器时,它首先尝试立即重新连接,然后等待 15 秒,然后 30 秒,以此类推。这种方法可以防止多次重试互相影响。幸运的是,Spring Batch 提供了一个BackoffPolicy
接口来实现这种类型的衰减。你可以通过实现BackoffPolicy
接口或者使用框架提供的ExponentialBackOffPolicy
来自己实现算法。清单 4-54 显示了BackOffPolicy
所需的配置。
清单 4-54。使用 AOP 应用重试逻辑
`<beans:bean id="timeoutPolicy"
class="org.springframework.batch.retry.policy.TimeoutRetryPolicy">
<beans:property name="timeout" value="30000"/>
</beans:bean>
<beans:bean id="backoutPolicy"
class="org.springframework.batch.retry.backoff.ExponentialBackOffPolicy"/>
<beans:bean id="timeoutRetryTemplate"
class="org.springframework.batch.retry.support.RetryTemplate">
<beans:property name="retryPolicy" ref="timeoutPolicy"/>
<beans:property name="backOffPolicy" ref="backOffPolicy"/>
</beans:bean>
<beans:bean id="retryItemWriter"
class="com.apress.springbatch.chapter4.RetryItemWriter">
<beans:property name="customerDao" ref="customerDao"/>
<beans:property name="retryTemplate" ref="timeoutRetryTemplate"/>
</beans:bean>
重试逻辑的最后一个方面是,像 Spring Batch 中的大多数可用事件一样,retry 能够在重试项目时向注册侦听器。然而,在所有其他听众和org.springframework.batch.retry.RetryListener
之间有两个不同之处。首先,RetryListener
没有与接口等价的注释,所以如果您想在重试逻辑上注册一个监听器,您必须实现RetryListener
接口。另一个区别是,这个接口中有三个方法,而不是两个方法用于开始和结束。在RetryListener
中,open
方法在重试块即将被调用时被调用,onError
在每次重试时被调用一次,而close
在整个重试块完成时被调用。
这就涵盖了重试逻辑。另一种处理特定于项的错误的方法是完全跳过该项。
项目跳过
Spring Batch 最伟大的事情之一是能够跳过导致问题的项目。如果项目可以在第二天解决,此功能可以轻松防止半夜打电话处理生产问题。配置跳过项目的能力类似于配置重试逻辑。您所需要做的就是使用chunk
标签上的skip-limit
属性,并指定应该导致项目被跳过的异常。清单 4-55 展示了如何配置一个步骤,允许通过skip-limit
跳过最多 10 个项目。然后声明任何导致除了java.lang.NullPointerException
之外的java.lang.Exception
的子类的项目都被允许跳过。任何抛出NullPointerException
的项目都会导致该步骤错误结束。
清单 4-55。跳过逻辑配置
<job id="flowJob"> <step id="retryStep"> <tasklet> <chunk reader="itemReader" writer="itemWriter" processor="itemProcessor" commit-interval="20" skip-limit="10"> <skippable-exception-classes> <include class="java.lang.Exception"/> <exclude class="java.lang.NullPointerException"/> </skippable-exception-classes> </chunk> </tasklet> </step> </job>
当使用基于项的错误处理时,无论是重试处理一个项还是跳过它,都会有事务性的影响。当本书进入第七章 & 和第九章的阅读和写作项目时,你会学到这些是什么以及如何解决它们。
总结
这一章涵盖了大量的材料。你了解了什么是工作,也看到了它的生命周期。您了解了如何配置作业以及如何通过作业参数与作业进行交互。您编写并配置了监听器,以便在作业的开始和结束时执行逻辑,并且使用ExecutionContext
来处理作业和步骤。
你开始关注一项工作的组成部分:它的步骤。在查看步骤时,您探索了 Spring Batch 中最重要的概念之一:基于块的处理。您了解了如何配置块以及控制它们的一些更高级的方法(通过策略之类的东西)。您了解了侦听器以及如何在一个步骤的开始和结束时使用它们来执行逻辑。您演练了如何使用基本排序或逻辑来对步骤进行排序,以确定下一步要执行的步骤。本章简要介绍了使用split
标签的并行化,并通过介绍基于项目的错误处理(包括跳过和重试逻辑)结束了对步骤的讨论。
作业和步骤是 Spring 批处理框架的结构组件。它们被用来规划一个过程。从这里开始,本书的大部分内容涵盖了所有不同的东西,这些东西被放入了这些作品的结构中。*
五、作业存储库和元数据
当您考虑编写批处理过程时,以独立的方式执行没有 UI 的过程并不困难。当您深入研究 Spring Batch 时,作业的执行只不过是使用 Spring 的 TaskExecutor 的一个实现来运行一个单独的任务。你不需要 SpringBatch 做到这一点。
然而,事情变得有趣的地方是事情出错的时候。如果您的批处理作业正在运行并出现错误,您如何恢复?当错误发生时,您的作业如何知道它在处理中的位置,以及当作业重新启动时应该发生什么?状态管理是处理大量数据的重要部分。这是 Spring Batch 带来的关键特性之一。如本书前面所讨论的,Spring Batch 在作业存储库中执行时维护作业的状态。然后,当重新启动作业或重试某个项目时,它会使用此信息来确定如何继续。这个功能的强大是不言而喻的。
作业存储库有助于批处理的另一个方面是监控。在企业环境中,能够查看作业的处理进度以及趋势元素(如操作需要多长时间或由于错误重试了多少项)至关重要。Spring Batch 为您收集数字的事实使这种类型的趋势分析变得更加容易。
本章详细介绍了作业存储库。它介绍了使用数据库或内存存储库为大多数环境配置作业存储库的方法。您还将简要了解对作业存储库配置的性能影响。配置作业存储库后,您将学习如何使用 JobExplorer 和 JobOperator 来使用作业存储库存储的作业信息。
配置作业储存库
为了让 Spring Batch 能够维护状态,作业存储库需要可用。默认情况下,Spring 提供了两种选择:内存存储库和数据库中的持久存储库。本节将介绍如何配置这些选项,以及这两个选项对性能的影响。让我们从更简单的选项开始,内存中的作业存储库。
使用内存中的作业库
本章的开头几段列出了作业存储库的一系列好处,比如从一次执行到另一次执行维护状态的能力,以及从一次运行到另一次运行的趋势运行统计。然而,由于这些原因,您几乎不会使用内存中的存储库。这是因为当这个过程结束时,所有的数据都会丢失。那么,为什么要使用内存存储库呢?
答案是有时候不需要持久化数据。例如,在开发中,通常使用内存存储库来运行作业,这样您就不必担心在数据库中维护作业模式。这也允许您使用相同的参数多次执行相同的作业,这是开发中的必备功能。出于性能原因,您也可以使用内存中的存储库来运行作业。在可能不需要的数据库中维护作业状态是有成本的。例如,假设您正在使用 Spring Batch 进行数据迁移,将数据从一个数据库表移动到另一个数据库表;开始时目标表是空的,您有少量数据要迁移。在这种情况下,设置和使用 Spring 批处理模式的开销可能没有意义。不需要 Spring Batch 来管理重启等情况可以使用内存中选项。
您目前使用的 JobRepository 是在launch-context.xml
文件中配置的。在前面的例子中,您已经使用 MySQL 配置了作业存储库。要配置您的作业使用内存中的存储库,您可以使用org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean
,如清单 5-1 中的所示。请注意,仍然需要事务管理器。这是因为 JobRepository 存储的数据仍然依赖于事务语义(回滚等),业务逻辑也可能依赖于事务存储。清单中配置的事务管理器org.springframework.batch.support.transaction.ResourcelessTransactionManager
,实际上并不处理事务;它是一个提供虚拟事务的虚拟事务管理器。
清单 5-1。配置内存中的作业库
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:bean id="transactionManager"
class="org.springframework.batch.support.transaction.
ResourcelessTransactionManager"/>
<beans:bean id="jobRepository"
class="org.springframework.batch.core.repository.support.
MapJobRepositoryFactoryBean" p:transactionManager-ref="transactionManager" />
...`
如果您从第二章的中获取 HelloWorld 示例,并将其配置为使用清单 5-1 中的内存元素,您会发现您可以一遍又一遍地运行作业,而不会因为多次使用相同的参数运行相同的作业而引发 Spring Batch 异常。
使用内存中的作业存储库时,您应该记住一些限制。首先,如前所述,因为数据存储在内存中,一旦 JVM 重启,数据就会丢失。其次,因为同步发生在特定 JVM 的内存空间中,所以不能保证在两个 JVM 中执行相同的作业时,不会使用相同的给定参数来执行给定的作业。最后,如果您的工作正在使用 Spring Batch 提供的任何多线程选项(多线程步骤、并行流等等),这个选项将不起作用。
这就是内存选项。通过对配置做一点小小的调整,您可以避免设置数据库来运行批处理作业。然而,考虑到这种方法的局限性和持久作业存储库提供的特性,大多数情况下您将使用数据库来支持您的作业存储库。记住这一点,让我们看看如何在数据库中配置作业存储库。
数据库
使用数据库存储库是配置作业存储库的主要方式。它允许您利用持久性存储的所有好处,而对您的工作的整体性能几乎没有影响。稍后,这一章看一些硬数字来说明使用数据库的成本。
但是现在,让我们从查看示例中使用的 MySQL 配置开始。你可以在清单 5-2 中看到配置。在这种情况下,您从数据源开始。这个例子使用 Apache Commons 提供的org.apache.commons.dbcp.BasicDataSource
,但是您想要使用的任何数据源都可以。清单 5-2 通过batch.properties
文件中的属性设置各种属性(驱动程序类、数据库 URL、用户名和密码)的值。这允许您配置那些特定于您正在工作的环境的属性(您的生产环境与您的测试环境具有不同的值,测试环境与您的本地开发具有不同的值)。
清单 5-2。使用数据库在launch-context.xml
中配置作业储存库
`<beans:bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<beans:property name="driverClassName" value="\({batch.jdbc.driver}" />
<beans:property name="url" value="\){batch.jdbc.url}" />
<beans:property name="username" value="\({batch.jdbc.user}" />
<beans:property name="password" value="\){batch.jdbc.password}" />
</beans:bean>
<beans:bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
lazy-init="true">
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
接下来要配置的是事务管理器。同样,这里通过使用 Spring 提供的基本DataSourceTransactionManager
来保持简单,但是任何事务管理器都可以。DataSourceTransactionManager
只有一个依赖项:一个您已经配置的实际数据源。事务管理器的最后一个方面是它被配置为惰性初始化 1 ,根据 Spring 文档这是可选的,但是因为 Spring Batch 的 shell 项目已经这样配置了它,所以没有理由更改它。
默认情况下,Spring 在启动时实例化所有单例 beans。因为您不知道会发生什么,所以如果不会使用事务管理器,就没有理由去创建它。
最后你来到了工作仓库工厂。关于这种配置,首先要指出的是,您没有使用常规的bean
标记。相反,Spring Batch 提供了一个特定于配置作业存储库的标签。为了以这种方式配置作业存储库,您需要像在清单 5-1 中所做的那样,向launch-context.xml
添加对 Spring 批处理 XSD 的引用,因为默认情况下不包括它。对于job-repository
标签,唯一需要的属性是id
。默认情况下,Spring 自动将JobRepositoryFactoryBean
的data-source
和transaction-manager
属性分别与名为dataSource
和transactionManager
的 beans 关联起来。
在任一配置中,使用bean
标签或job-repository
标签,您可以引用 Spring Batch 提供的两个作业存储库工厂中的第二个。第一个是org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean
,用于内存中的作业存储库。第二个是org.springframework.batch.core.repository.support.JobRepositoryFactoryBean
。这个配置演示的JobRepositoryFactoryBean
使用数据库作为维护作业存储库中状态的方法。要访问底层数据库,您需要满足它的两个依赖关系:一个数据源和一个事务管理器,这两者都在清单 5-2 中配置。
数据库模式配置
Spring 在允许您灵活配置方面做得很好。Spring Batch 允许您修改的内容之一是表前缀。默认情况下,每个表都以 BATCH_ 为前缀,但这可能不是您或您的企业想要的。考虑到这一点,Spring Batch 的开发人员允许您为作业存储库中的表配置表前缀。为此,使用job-repository
标签的table-prefix
属性,如清单 5-3 所示。随着配置的更新,Spring Batch 希望表被命名为 SAMPLE_JOB_EXECUTION,依此类推。
清单 5-3。改变表格前缀
<job-repository id="jobRepository" data-source="dataSource" transaction-manager="transactionManager" table-prefix="SAMPLE_"/>
注意 Spring Batch 只让你配置表格前缀。您不能更改表名或列名的完整名称。
Spring Batch 允许您配置的数据库模式的另一个方面是varchar
数据类型的最大长度。默认情况下,Spring Batch 中的模式脚本将较大的varchar
列的长度设置为 2500(执行表中的 EXIT_MESSAGE 列和执行上下文表中的 SHORT_CONTEXT 列)。如果您正在使用一个字符使用多于一个字节的字符集,或者修改模式,您可以使用它来允许 Spring Batch 存储更大的值。清单 5-4 显示了作业存储库被配置为最大 3000 个。
清单 5-4。配置最大varchar
长度
<job-repository id="jobRepository" data-source="dataSource" transaction-manager="transactionManager" max-varchar-length="3000"/>
重要的是要注意清单 5-4 中的配置并没有真正改变数据库。相反,它会截断所有太长而不适合 EXIT_MESSAGE 列的消息。配置作业存储库的最后一部分,也可能是最重要的一部分,是如何配置事务,接下来我们将讨论这一部分。
交易配置
作业存储库的使用基于事务。当每个处理块完成时,存储库被更新,这触发了事务的结束。您已经看到有两种方式来配置作业存储库,一种是使用常规的 Spring bean
标签,另一种是使用 Spring Batch 名称空间的job-repository
标签。如何配置事务取决于您选择这些选项中的哪一个。
当您使用 Spring Batch 名称空间中的job-repository
标记时,Spring Batch 使用 Spring 的 AOP 特性将存储库包装成一个事务。使用这种方法时,唯一需要配置的是作业存储库接口的createJobExecution
方法的事务隔离级别。这种配置的目的是防止一个JobInstance
同时被多次执行。为了解决这个问题,默认情况下,Spring Batch 将事务的隔离级别设置为其最激进的值SERIALIZABLE
。然而,您的环境可能不需要这么激进的级别,所以 Spring Batch 允许您用job-repository
标记的isolation-level-for-create
属性为createJobExecution
方法配置事务级别。清单 5-5 显示了使用job-repository
标签时如何降低隔离级别。
清单 5-5。设置创建交易级别
<job-repository id="jobRepository" transaction-manager="transactionManager" data-source="dataSource" isolation-level-for-create="READ_COMMITTED"/>
如果像使用内存选项一样使用 Spring bean
标签配置作业存储库,框架不会为您处理任何事务。在这种情况下,您需要手工配置事务性通知,如清单 5-6 所示。这里,您使用 Spring 的 AOP 名称空间来配置事务通知,并将其应用于作业存储库接口中的所有方法。
清单 5-6。手动配置作业存储库事务
`<beans:bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<beans:property name="driverClassName" value="\({batch.jdbc.driver}" />
<beans:property name="url" value="\){batch.jdbc.url}" />
<beans:property name="username" value="\({batch.jdbc.user}" />
<beans:property name="password" value="\){batch.jdbc.password}" />
</beans:bean>
<beans:bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/>
aop:config
<aop:advisor
pointcut="execution(org.springframework.batch.core.repository..Repository+.*(..))"/>
<advice-ref="txAdvice" />
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
tx:attributes
<tx:method name="*" />
</tx:attributes>
</tx:advice>`
<beans:bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean" p:transactionManager-ref="transactionManager" p:dataSource-ref="dataSource" />
作业存储库是 Spring Batch 为您的处理提供的安全网的核心部分。然而,它不仅仅是 Spring Batch 框架自己使用的工具。您可以像框架一样访问数据,还可以操作数据。下一节将向您展示。
使用作业元数据
尽管 Spring Batch 通过一组 Dao 来访问作业存储库表,但是它们为框架的使用和您的使用提供了一个更加实用的 API。在这一节中,您将看到 Spring Batch 在作业存储库中公开数据的两种方式。首先,您将看到 JobExplorer,一个您在上一章中配置的项目,如何配置它并使用它从存储库中读取数据。从这里,您可以转到 JobOperator。您将看到如何配置操作符,以及如何使用它来操作作业存储库中包含的数据。让我们从 JobExplorer 开始。
求职者
org.springframework.batch.core.explore.
JobExplorer 界面是所有访问作业库中历史和活动数据的起点。图 5-1 显示,虽然大部分框架通过 JobRepository 访问存储的关于作业执行的信息,但是 JobExplorer 直接从数据库本身访问。
图 5-1。作业管理组件之间的关系
JobExplorer 的基本目的是提供对作业存储库中数据的只读访问。该接口提供了七种方法,可用于获取有关作业实例和执行的信息。表 5-1 列出了可用的方法及其用途。
表 5-1。求职者的方法
| **方法** | **描述** | | :-- | :-- | | `java.util.Set如您所见,通过 JobExplorer 接口公开的方法可以获得整个作业存储库。但是,在使用 JobExplorer 之前,您需要对其进行配置。清单 5-7 显示了如何在launch-context.xml
文件中配置 JobExplorer。
清单 5-7。 JobExplorer 配置
<beans:bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean" p:dataSource-ref="dataSource" />
JobExplorer 的配置就像任何其他 Spring bean 一样,只有一个依赖项——一个数据源——因此它可以被注入到任何其他元素中。注意,与您配置的大多数其他依赖于 JobRepository 的 Spring Batch 组件不同,这个组件依赖于一个数据源。原因是,如前所述,JobExplorer 不从 JobRepository 获取信息。相反,它直接进入数据库获取信息。
要查看 JobExplorer 是如何工作的,您可以将它注入到前面示例中的HelloWorld
tasklet 中。从那里,您可以看到您可以使用 JobExplorer 做什么。在清单 5-8 中,您配置了注入了 JobExplorer 的HelloWorld
微线程。
清单 5-8。微线程HelloWorld
和作业浏览器的配置
`
<beans xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<batch:step id="helloWorldStep">
<batch:tasklet ref="helloWorld"/>
</batch:step>
<batch:job id="helloWorldJob">
<batch:step id="step1" parent="helloWorldStep"/>
</batch:job>
`
配置了 JobExplorer 后,您可以使用它做许多事情。在 Spring Batch 框架中,您可以使用在第四章中查看的RunIdIncrementer
中的 JobExplorer 来查找之前的run.id
参数值。它的另一个用途是在 Spring Batch Admin web 应用中,在启动一个新实例之前确定一个作业当前是否正在运行。在示例中,您使用它来确定这是否是您第一次运行这个 JobInstance。如果是,你打印消息“你好,迈克尔!”其中迈克尔是传入的值。如果这不是您第一次运行该作业,您可以将消息更新为“欢迎回来 Michael!”清单 5-9 有这个小任务的更新代码。
清单 5-9。更新HelloWorld
小任务
`package com.apress.springbatch.chapter5;
import java.util.List;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
public class HelloWorld implements Tasklet {
private static final String HELLO = "Hello, %s!";
private static final String WELCOME = "And then we have %s!";
private static final String JOB_NAME = "helloWorldJob";
private JobExplorer explorer;
public RepeatStatus execute(StepContribution stepContribution,
ChunkContext chunkContext) throws Exception {
List
explorer.getJobInstances(JOB_NAME, 0, Integer.MAX_VALUE);
String name = (String) chunkContext.getStepContext()
.getJobParameters()
.get("name");
if (instances != null && instances.size() > 1) {
System.out.println(String.format(WELCOME, name));
} else {
System.out.println(String.format(HELLO, name));
}
return RepeatStatus.FINISHED;
}
public void setExplorer(JobExplorer explorer) {
this.explorer = explorer;
}
}`
清单 5-9 中的代码从获取helloWorldJob
的所有作业实例开始。一旦有了列表,它就确定该作业以前是否运行过。如果有,你就用“欢迎回来”的信息。如果这是作业第一次运行,您可以使用“Hello”消息。
代码和配置就绪后,运行作业两次,这样您就可以看到if
语句执行的两个部分。清单 5-10 显示了每项工作的重要输出。
清单 5-10。两次运行的全球工作输出
`Run 1 executed with the command java -jar metadata-0.0.1-SNAPSHOT.jar name=Michael
2010-12-17 22:42:50,613 DEBUG org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.scope.context.StepContextRepeatCallback] -
Hello, Michael!
2010-12-17 22:42:50,619 DEBUG
org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.step.tasklet.TaskletStep] - <Applying contribution:
[StepContribution: read=0, written=0, filtered=0, readSkips=0, writeSkips=0, processSkips=0,
exitStatus=EXECUTING]>
Run 2 executed with the command java -jar metadata-0.0.1-SNAPSHOT.jar name=John
2010-12-17 22:44:49,960 DEBUG
org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.scope.context.StepContextRepeatCallback] -
And then we have John!
2010-12-17 22:44:49,965 DEBUG
org.springframework.batch.core.launch.support.CommandLineJobRunner.main()
[org.springframework.batch.core.step.tasklet.TaskletStep] - <Applying contribution:
[StepContribution: read=0, written=0, filtered=0, readSkips=0, writeSkips=0, processSkips=0,
exitStatus=EXECUTING]>`
本节讨论了如何通过 JobExplorer 访问作业存储库中的数据。您使用像 JobExplorer 这样的 API 来访问数据,以便以安全的方式使用数据。尽管直接操作作业存储库并不是好的做法,但这并不意味着它维护的数据不受干预。事实上,您可以通过操作作业存储库,以编程方式控制作业中发生的事情。使用 JobOperator 可以做到这一点。
作业员
当您查看方法名称时,JobOperator 与 JobExplorer 非常相似。然而,尽管 JobExplorer 提供了对作业存储库中数据的只读查看,但 JobOperator 只公开对其采取操作所需的内容。org.springframework.batch.core.launch.
JobOperator 接口允许您在作业中以编程方式执行基本的管理任务。
JobOperator 的界面由 11 种方法组成,在表 5-2 中概述。
表 5-2 。作业操作器上可用的方法
| **方法** | **描述** | | :-- | :-- | | `java.util.List到目前为止,这本书一直使用一个基本的 java 命令行命令来启动 Spring Batch 的CommandLineJobRunner
来运行作业。为了查看 JobOperator 的运行情况,您创建了一个 JMX JobRunner,它允许您通过 JMX 控制台执行作业。首先,您必须编写一个main
方法,让 Spring 应用保持运行,而不需要实际做任何事情。清单 5-11 展示了你是如何做到的。
清单 5-11。启动一个 Spring 应用
`package com.apress.springbatch.chapter5;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Batch {
@SuppressWarnings("unused")
public static void main(String[] args) {
try {
ApplicationContext context =
new ClassPathXmlApplicationContext("launch-context.xml");
Object lock = new Object();
synchronized (lock) {
lock.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}`
当您编写了启动 Spring 应用并保持其运行的代码后,您可以编写JMXJobRunner
。为此,您只需编写一个 POJO,它根据传入的作业名启动一个作业。正如你在清单 5-12 中看到的,完成这个的代码只不过是一个 JobOperator 实例的包装器。
清单 5-12。??JMXJobRunner
`package com.apress.springbatch.chapter5;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobInstanceAlreadyExistsException;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.launch.NoSuchJobException;`
`public class JMXJobRunner {
private JobOperator operator;
public void runJob(String name) throws NoSuchJobException,
JobInstanceAlreadyExistsException,
JobParametersInvalidException {
operator.start(name, null);
}
public void setOperator(JobOperator operator) {
this.operator = operator;
}
}`
在清单 5-12 中,您使用JobOperator.start
方法来启动一个带有提供的名称且不带参数的作业;作业在先前加载的ApplicationContext
中进行配置。写完JMXJobRunner
和Batch
类后,剩下的唯一事情就是用 Spring 连接它们。这些元素的所有配置都在launch-context.xml
中,如清单 5-13 所示。
清单 5-13。??launch-context.xml
`
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:bean id="jobOperator"
class="org.springframework.batch.core.launch.support.SimpleJobOperator"
p:jobLauncher-ref="jobLauncher" p:jobExplorer-ref="jobExplorer"
p:jobRepository-ref="jobRepository" p:jobRegistry-ref="jobRegistry" />
<beans:bean id="jobExplorer"
class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"
p:dataSource-ref="dataSource" />
<beans:bean id="taskExecutor"
class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
<beans:bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<beans:property name="jobRepository" ref="jobRepository" />
<beans:property name="taskExecutor" ref="taskExecutor" />
</beans:bean>
` <beans:bean id="jobRegistry"
class="org.springframework.batch.core.configuration.support.MapJobRegistry" />
<beans:bean
class="org.springframework.batch.core.configuration.support.AutomaticJobRegistrar">
<beans:property name="applicationContextFactories">
<beans:bean
class="org.springframework.batch.core.configuration.support.
ClasspathXmlApplicationContextsFactoryBean">
<beans:property name="resources"
value="classpath*:/jobs/helloWorld.xml" />
</beans:bean>
</beans:property>
<beans:property name="jobLoader">
<beans:bean
class="org.springframework.batch.core.configuration.support.DefaultJobLoader">
<beans:property name="jobRegistry" ref="jobRegistry" />
</beans:bean>
</beans:property>
</beans:bean>
<beans:bean id="jobRunner"
class="com.apress.springbatch.chapter5.JMXJobRunnerImpl"
p:operator-ref="jobOperator" />
<beans:bean id="exporter"
class="org.springframework.jmx.export.MBeanExporter"
lazy-init="false">
<beans:property name="beans">
</beans:property>
<beans:property name="assembler" ref="assembler" />
</beans:bean>
<beans:bean id="assembler"
class="org.springframework.jmx.export.assembler.
InterfaceBasedMBeanInfoAssembler">
<beans:property name="managedInterfaces">
</beans:property>
</beans:bean>
<beans:bean id="registry"
class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
<beans:property name="port" value="1099" />
</beans:bean>`
` <beans:bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource">
<beans:property name="driverClassName"
value="\({batch.jdbc.driver}" />
<beans:property name="url" value="\){batch.jdbc.url}" />
<beans:property name="username" value="\({batch.jdbc.user}" />
<beans:property name="password" value="\){batch.jdbc.password}" />
</beans:bean>
<beans:bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
lazy-init="true">
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
<beans:bean id="placeholderProperties"
class="org.springframework.beans.factory.config.
PropertyPlaceholderConfigurer">
<beans:property name="location" value="classpath:batch.properties" />
<beans:property name="systemPropertiesModeName"
value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
<beans:property name="ignoreUnresolvablePlaceholders"
value="true" />
<beans:property name="order" value="1" />
</beans:bean>
</beans:beans>`
这个launch-context.xml
文件有很多内容,我们从头开始吧。为了让JMXJobLauncher
能够启动一个作业,您需要一个对 JobOperator 的引用。该文件中的第一个 bean 是配置。SimpleJobOperator
是 Spring Batch 框架提供的JobOperator
接口的唯一实现。您将其配置为可以访问 JobExplorer、JobLauncher、JobRepository 和 JobRegistry。考虑到 JobOperator 可以做什么,这些依赖项都是需要的。
接下来是 JobExplorer,为该配置中的许多对象提供对 JobRepository 的只读访问。在 JobExplorer 之后是一个 TaskExecutor 配置以及由 JobOperator 使用的 JobLauncher。JobLauncher 执行启动作业的工作,并由您的环境的 JobOperator 管理。接下来配置 JobRepository 如前所述,Spring Batch 使用它来维护作业的状态。
JobRegistry 是配置的下一个 bean。Spring Batch 提供了在 JobRegistry 中注册一组按需执行的作业的能力。JobRegistry 包含有资格在此 JVM 中运行的所有作业。在这种配置的情况下,您使用 Spring Batch 的MapJobRegistry
,它是可运行的作业的Map
。为了在启动时填充 JobRegistry,需要配置一个AutomaticJobRegistrar
的实例。这个类,如清单 5-13 中所配置的,读取/jobs/helloWorldJob.xml
文件中配置的所有作业,并将它们加载到 JobRegistry 中以备将来使用。
这个launch-context.xml
文件中 Spring Batch 透视图的最后一部分是JMXJobLauncher
本身的配置。该文件中的其余配置包括数据源、事务管理器、属性加载器以及将JMXJobLauncher
公开为 MBean 所需的元素。
完成所有配置和编码后,您现在可以运行主类Batch
,并使用 JDK 提供的 JConsole 应用查看通过 JMX 公开的 beans。然而,要启动Batch
程序,您需要做最后一点调整。当你使用 Spring Batch 的simple-cli-archetype
创建一个 shell 项目时, cli 代表命令行界面。这个项目中的项目对象模型 (POM)被预先配置为创建一个 jar 文件,默认情况下CommandLineJobRunner
被定义为主类。对于这个例子,你更新 POM 来使用清单 5-11 中的Batch
类作为你的 jar 的主要方法。清单 5-14 显示了需要更新的代码片段。
清单 5-14。 Maven Jar 插件配置为运行批处理
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <index>false</index> <manifest> <mainClass>com.apress.springbatch.chapter5.Batch</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> <manifestFile> ${project.build.outputDirectory}/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin>
POM 更新后,您可以使用与过去相同的命令来启动程序:java –jar metadata-0.0.1-SNAPSHOT.jar.
请注意,当您运行该程序时,输出完全不同。这一次,没有运行任何作业。相反,您会看到 Spring bootstrap 并从helloWorld.xml
文件中注册作业helloWorldJob
,如清单 5-16 所示。
清单 5-16。helloWorld 工作的注册。
2010-12-16 21:17:41,390 DEBUG com.apress.springbatch.chapter5.Batch.main() [org.springframework.batch.core.configuration.support.DefaultJobLoader] - <Registering job: helloWorldJob1 from context: file:/Users/mminella/Documents/SpringBatch/Chapter5/batch- test/target/classes/jobs/helloWorld.xml>
随着批处理的运行,您可以使用 Java 的 JConsole 查看通过 JMX 公开的 beans。JConsole 是 JDK 提供的免费 Java 监控和管理工具。它允许您以多种方式监控 JVM,包括 CPU 利用率和内存分配,并允许您执行管理任务,包括与 JMX bean 交互。当您启动 JConsole 时,会出现一个屏幕,询问您想要连接到哪个 Java 进程,如图 5-2 所示。
图 5-2。JConsole 主屏幕
在这种情况下,选择名为org.codehaus.classworlds.Launcher "exec:java"
的本地进程(也可以使用 JConsole 来管理远程 Java 进程)。这是您用清单 5-15 中的maven
命令启动的 Java 进程。单击 connect 允许 JConsole 连接到 JVM。
连接后,JConsole 向您显示一个类似于图 5-3 中的屏幕。顶部是六个选项卡:概述、内存、线程、类、虚拟机摘要和 MBeans。在选项卡下面,JConsole 允许您选择显示概览数据的时间范围。最后,有四个象限的数据:JVM 堆内存使用量、正在使用的线程数、JVM 当前加载的类数以及所选时间内的 CPU 使用量。您感兴趣的选项卡是 MBeans。
图 5-3。JVM 概述
当您选择 MBeans 选项卡时,您会在左侧看到一个树导航,在窗口的其余部分看到一个主面板。在清单 5-13 中,您将 bean 配置在名称空间bean
中,并命名为myJobRunner
。果然,图 5-4 显示了树导航中的bean
名称空间和一个可用的 bean myJobRunner
。
图 5-4。 JMXJobRunner
在 JConsole 曝光
对于任何公开的 bean,最多有三个部分:允许您修改公共属性的属性(在本例中没有);操作,它允许您执行公共方法;和通知,向您显示任何 JMX 通知(从 bean 发送到任何通过 JMX 收听的人的消息)。要运行批处理作业,进入操作,如图图 5-5 所示。在这里,您可以看到JMXJobRunner
: runJob
上唯一的公共方法。它是空的,只有一个参数。要调用作业,只需在框中输入作业名,然后单击 Runjob。
图 5-5。JConsole中暴露的JMXJobRunner.runJob
方法
当您单击 Runjob 按钮时,您会在当前运行 Java 进程的控制台中看到作业正在运行,并使用helloWorldJob
给出您期望的输出。
`2010-12-17 17:06:09,444 DEBUG RMI TCP Connection(1)-192.168.1.119
[org.springframework.batch.core.scope.context.StepContextRepeatCallback] -
Hello, World!
2010-12-17 17:06:09,450 DEBUG RMI TCP Connection(1)-192.168.1.119
[org.springframework.batch.core.step.tasklet.TaskletStep] - <Applying contribution:
[StepContribution: read=0, written=0, filtered=0, readSkips=0, writeSkips=0, processSkips=0,
exitStatus=EXECUTING]>`
在这个例子中,您编写了一个可以通过 JMX 公开的 job runner。其他用途是开发一个基于给定条件停止作业的步骤监听器。在第六章的中,您扩展了这个例子以接受参数并使用 JobOperator 以编程方式停止和重启作业。
总结
Spring Batch 管理关于作业的元数据以及在作业运行时维护作业状态以进行错误处理的能力是使用 Spring Batch 进行企业批处理的主要原因之一,如果不是主要原因的话。它不仅提供了健壮的错误处理能力,还允许流程根据作业中其他地方发生的事情来决定要做什么。在下一章中,您将深入了解如何在各种环境中启动、停止和重启作业,从而进一步使用这些元数据以及 JobOperator。
六、运行作业
通常情况下,你不必考虑如何用 Java 运行一个应用。如果您有一个 web 应用,您可以在某种形式的容器中运行它。要运行应用,您需要启动容器,容器会启动应用。如果想运行一个独立的 Java 程序,要么创建一个可执行的 jar 文件,要么直接调用这个类。在这两种情况下,您都可以编写一个 shell 脚本来启动这个过程。
但是,运行批处理作业是不同的。这部分是因为批处理作业既可以作为现有进程中的一个线程运行(到目前为止一直如此),也可以在主执行线程中运行。它可以在容器中运行,也可以作为独立的进程运行。你可以在每次执行时启动一个 JVM,或者你可以加载一个 JVM 并通过类似 JMX 的东西调用它来启动作业(就像你在第五章中所做的那样)。
你也要考虑到当事情出错,你的工作停止时会发生什么。整个作业是否需要重新运行,或者您是否可以从它停止的那一步开始?如果该步骤要处理一百万行,那么它们是否都需要重新处理,或者可以从发生错误的地方重新开始?
在运行批处理作业时要考虑所有这些因素,本章将介绍如何在各种环境中启动作业。它讨论了 Spring Batch framework 提供的不同作业运行器,以及将作业的启动和运行与 Tomcat 之类的容器和 Quartz 之类的调度器集成在一起。
经营一份工作并不是你在这里学到的全部。您还将看到如何以编程方式在作业开始后以允许重新启动的方式停止它。最后,您将通过了解重新启动作业的条件来结束本章。
开始一项工作
在到目前为止的章节中,每次启动 JVM 时,您几乎都在专门运行一个作业。然而,当你像使用SimpleJobLauncher
一样执行一项任务时,事情会比看起来要复杂一些。本节介绍通过SimpleJobLauncher
启动作业时会发生什么。然后,您将详细查看 Spring Batch 提供的所有作业运行器和启动器。您将看到如何在各种环境中执行作业,包括在 servlet 容器中,使用 Spring Batch Admin 管理应用并通过开源调度程序 Quartz。
工作执行
当您考虑在 Spring Batch 中启动批处理作业时,您可能会认为正在发生的事情是 Spring Batch 作为主执行线程的一部分执行作业。当它完成时,该过程结束。然而,事情没那么简单。负责启动作业的接口可以通过多种方式实现,公开任意数量的执行选项(web、JMX、命令行等等)。
因为JobLauncher
接口不保证一个作业是同步运行还是异步运行,SimpleJobLauncher
(Spring Batch 提供的唯一的JobLauncher
实现)通过在 Spring 的TaskExecutor
中运行作业,把它留给开发者。默认情况下,SimpleJobLauncher
使用 Spring 的SyncTaskExecutor
,它在当前线程中执行任务。虽然在许多情况下这是可以的,但是对于在单个 JVM 中运行的作业数量来说,这个选项是一个限制因素。
我们来看看SimpleJobLauncher
是如何配置的。清单 6-1 显示了设置了可选的taskExecutor
属性的配置。如前所述,该属性允许您指定用于启动作业的算法。在这种情况下,您使用 Spring 的SimpleAsyncTaskExecutor
在一个新线程中启动作业。然而,您可以很容易地将其配置为使用ThreadPoolTaskExecutor
来控制可用线程的数量
清单 6-1。 SimpleJobLauncher
配置有任务执行人
`
虽然 JobLauncher 启动了作业并定义了它的运行方式(同步、异步、在线程池中等等),但是当您想要启动作业时,它是您要与之交互的作业运行器(就像您在上一章的 JMX 作业运行器中所做的那样)。接下来您将看到 Spring Batch 提供的两个现成的作业运行器:CommandLineJobRunner
和JobRegistryBackgroundJobRunner
。
Spring 批量作业运行程序
当您查看 Spring Batch API 时,尽管理论上有许多方法可以运行一个作业(通过 servlet、命令行、JMX 等等启动它),但是框架只提供了两个运行器org.springframework.batch.core.launch.support.CommandLineJobRunner
和org.springframework.batch.core.launch.support.JobRegistryBackgroundJobRunner
。所有其他选项——servlet、JMX 等等——必须定制开发。让我们从CommandLineJobRunner
开始,看看CommandLineJobRunner
和JobRegistryBackgroundJobRunner
是怎么用的,为什么。
CommandLineJobRunner
CommandLineJobRunner
是您到目前为止一直使用的跑步者。它通过命令行充当批处理的界面。这对于从终端或更常见的 shell 脚本调用作业非常有用。它提供了四个功能:
- 基于传递的参数加载适当的
ApplicationContext
- 将命令行参数解析成一个
JobParameters
对象 - 根据传递的参数找到请求的作业
- 使用配置的 JobLauncher 执行请求的作业
从命令行或脚本调用CommandLineJobRunner
时,有三个必需参数和四个可选参数,详见表 6-1 。
表 6-1。参数CommandLineJobRunner
参数
尽管到目前为止您几乎只使用了CommandLineJobRunner
来执行任务,但是正如您所看到的,这个小工具可以为您做更多的事情。到您的作业的 XML 文件的路径,以及-next
和作业参数选项应该都很熟悉,因为它们在前面已经介绍过了。
您一直使用的包含在simple-cli
中的项目对象模型(POM)的构建过程构建了一个 jar 文件,其中CommandLineJobRunner
被配置为主类。要使用它,您需要将 jar 文件放在一个目录中,其中有一个包含所有依赖项的lib
文件夹。如果您查看<project_home>/target/
目录,您会看到 jar 文件已经与lib
目录一起构建,并具有所需的依赖关系;不过名字可能不是最直观的(比如spring-batch-simple-cli-2.1.3.RELEASE.jar
)。为了更新构建过程以构建一个更合适的 jar 名称,您可以更改 POM 文件以生成具有合适名称的工件。清单 6-2 显示了 POM 更新,将 jar 的名称改为helloWorld.jar
。
清单 6-2。将 Maven 神器重命名为helloWorld.jar
... <build> <finalName>helloWorld</finalName> ... </build> ...
将 jar 文件命名为对您的应用有意义的名称(在本例中为helloWorld
),您可以使用来自<project_home>/target
目录的 jar 文件本身运行作业。清单 6-3 展示了一个如何使用 Java 命令运行到目前为止一直在运行的HelloWorld
作业的例子。
清单 6-3。基本调用到CommandLineJobRunner
中包含的helloWorld.jar
到
java –jar helloWorld.jar jobs/helloWorld.xml helloWorldJob
您可以传入两个参数:作业配置的路径和要运行的作业的名称。使用–next
参数(来自第四章,如清单 6-4 所示,调用任何已配置的JobParametersIncrementers
。
清单 6-4。使用–next
参数
java –jar helloWorld.jar jobs/helloWorld.xml helloWorldJob -next
虽然表 6-1 显示CommandLineJobRunner
有七个参数可用,但本章只涵盖你目前拥有的四个。在本章的后面,您将再次访问CommandLineJobRunner
,并了解如何使用它来停止、放弃和重启作业。现在,您可以转到 Spring Batch 提供的另一个求职者:JobRegistryBackgroundJobRunner
。
工作注册背景工作流
实际上根本不是一个求职者。您不像CommandLineJobRunner
那样通过这个 runner 执行作业。相反,这个类旨在用于引导 Spring 并构建一个 JobRegistry 供其他人使用。在第五章的中,你为JMXJobRunner
编写了你自己版本的类,该类的功能类似于 bootstrap Spring 和你的作业。
但你没必要自己写。
org.springframework.batch.core.launch.support.JobRegistryBackgroundJobRunner
是命令行界面,和CommandLineJobRunner
一样。但是,这个作业运行器不是运行指定的作业,而是获取一个配置文件列表。一旦配置文件已经被引导,JobRegistryBackgroundJobRunner
暂停直到一个键被按下。
让我们来看一个例子。首先,回到第五章中的JMXJobRunner
示例。到目前为止,您已经将作业 XML 文件(<project_home>/src/main/resources/jobs/helloWorld.xml
)与主或父应用上下文(<project_home>/src/main/resources/launch-context.xml
)链接起来。然而,当使用 JobRegistry 时,通常不是这种情况。原因是当加载 JobRegistry 时,作业不会立即执行。相反,您将所有可能执行的作业加载到一个Map
中,这样它们就可以在任何时候启动。为了正确地配置您的作业,您需要删除一行:在helloWorld.xml
文件中导入launch-context.xml
文件。这样做是为了防止循环引用。如果您在helloWorld.xml
文件中保留对launch-context.xml
文件的引用,JobRegistryBackgroundJobRunner
将加载launch-context.xml
,然后加载helloWorld.xml
中的作业。因为launch-context.xml
已经在该点加载,所以你不想或者不需要再次加载。
您需要做的另一个修改是从launch-context.xml
中移除AutomaticJobRegistrar
bean。JobRegistryBackgroundJobRunner
从命令行接受两个参数:一个基本配置(称为父),包含所有常规组件,如JobRepository
、JobLauncher
等等;以及包含要运行的作业的配置文件列表。JobRegistryBackgroundJobRunner
注册它在你传递的配置中找到的作业,所以不需要AutomaticJobRegistrar
。
注意传入一个已经被引用了作业的配置会导致抛出
DuplicateJobException
。
您使用JobRegistryBackgroundJobRunner
来引导您的作业,而不是使用您在前一章中编写的Batch
类。因为JobRegistryBackgroundJobRunner
包含在spring-batch-core
jar 文件中(主要的 Spring 批处理依赖项),所以要执行带有JobRegistryBackgroundJobRunner
的 jar 文件,您需要做的唯一更改是将 POM 文件改为引用JobRegistryBackgroundJobRunner
作为主类。清单 6-5 突出了这一变化。
清单 6-5。改变pom.xml
中的主类
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <index>false</index> <manifest> <mainClass> **org.springframework.batch.core.launch.support.JobRegistryBackgroundJobRunner** </mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> <manifestFile> ${project.build.outputDirectory}/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin>
为了能够使用JobRegistryBackgroundJobRunner
执行 jar 文件,您只需要对pom.xml
做一点小小的修改。当 jar 文件被重建时,您可以用清单 6-6 中的命令来执行它。
清单 6-6。正在执行JobRegistryBackgroundJobRunner
java -jar helloWorld.jar launch-context.xml jobs/helloWorld.xml
请注意,您没有像过去那样在命令行上指定要运行的作业的名称。原因是您只是用这个命令引导 Spring 和 Spring Batch,而不是启动一个作业的执行。当您执行 jar 时,您会看到正常的 Spring bootstrap 输出,以清单 6-7 所示的内容结束,然后应用等待。等待着。它会继续运行,直到被终止或者以编程方式调用了JobRegistryBackgroundJobRunner.stop()
方法。现在流程已经运行,您可以像在第五章中一样使用 JConsole 来执行任务。
清单 6-7。从JobRegistryBackgroundJobRunner
输出
`2011-04-30 01:36:24,105 DEBUG main
[org.springframework.batch.core.configuration.support.ClassPathXmlApplication
ContextFactory$ResourceXmlApplicationContext] - <Unable to locate
LifecycleProcessor with name 'lifecycleProcessor': using default
[org.springframework.context.support.DefaultLifecycleProcessor@d3ade7]>
2011-04-30 01:36:24,106 DEBUG main
[org.springframework.batch.core.configuration.support.DefaultJobLoader] –
<Registering job: helloWorldJob from context:
org.springframework.batch.core.configuration.support.ClassPathXmlApplicationC
ontextFactory$ResourceXmlApplicationContext@1c6a99d>
Started application. Interrupt (CTRL-C) or call
JobRegistryBackgroundJobRunner.stop() to exit.`
是一个有用的工具,可以从命令行引导 Spring 和你的作业,而不用立即执行它们。这是在类似生产的环境中发现的更典型的场景。然而,大多数生产环境不会启动 Java 进程,让它运行并手动启动作业。相反,作业是有计划的。为了部署的一致性,它们可以在 servlet 容器中运行,并且可能需要由 Spring Admin 项目来管理。下一节将介绍如何做到这一切。
第三方集成
Spring Batch 是开发批处理过程的优秀工具,但是它很少单独管理。企业需要由运营团队管理批处理作业的能力。他们需要能够以一致的方式为企业部署作业。他们需要能够以不需要编程的方式启动、停止和调度作业(通常通过某种企业调度程序)。本节将介绍如何使用开源调度器 Quartz 调度批处理作业,如何在 Tomcat servlet 容器中部署 Spring 批处理作业,以及如何使用管理工具 Spring Batch Admin 启动作业。
用石英调度
许多企业调度程序是可用的。从粗糙但非常有效的 crontab 到价值数百万美元的企业自动化平台。这里使用的调度器是一个开源调度器,名为 Quartz ( [www.quartz-scheduler.org/](http://www.quartz-scheduler.org/)
)。这个调度器通常用于各种规模的 Java 环境中。除了其强大的功能和坚实的社区支持之外,它还拥有 Spring integration 的历史,这有助于执行作业。
考虑到 Quartz 的范围,本书不会在这里涵盖所有内容。但是,有必要简单介绍一下它是如何工作的,以及它是如何与 Spring Integration 的。图 6-1 显示了石英的成分及其相互关系。
图 6-1。石英调度器
如您所见,Quartz 有三个主要组件:一个调度器、一个作业和一个触发器。从SchedulerFactory
获得的调度程序充当JobDetails
(对 Quartz 作业的引用)的注册表,并在相关触发器触发时触发和负责执行作业。一个任务是一个可以被执行的工作单元。一个触发器定义何时运行一个任务。当一个触发器触发时,告诉 Quartz 执行一个任务,一个JobDetails
对象被创建来定义任务的单独执行。
这听起来熟悉吗?应该的。定义作业和JobDetails
对象的模型与 Spring Batch 定义作业和JobInstance
的方式非常相似。为了将 Quartz 集成到 Spring 批处理过程中,您需要执行以下操作:
- 将所需的依赖项添加到您的
pom.xml
文件中。 - 使用 Spring 的
QuartzJobBean
编写您自己的 Quartz 作业来启动您的作业。 - 配置一个由 Spring 提供的
JobDetailBean
来创建一个石英JobDetail
。 - 配置触发器以定义作业应该运行的时间。
为了展示如何使用 Quartz 定期执行一个作业,让我们创建一个新的作业deleteFilesJob
,它在每次运行时清空一个目录。这是批处理作业或任何存储数据并需要定期删除数据的实例(数据库清除等)的常见做法。在这种情况下,您将删除临时目录中的所有文件。
首先将所需的依赖项添加到 POM 文件中。在这种情况下,有三个新的依赖项。首先是石英框架本身。您添加的第二个依赖项是针对spring-context-support
工件的。Spring 的这个包提供了将 Quartz 与 Spring 轻松集成所需的类。最后,为了帮助解决本例中遇到的一些配置问题,您包括了 Apache Commons Collections 库。依赖关系的配置可以在清单 6-8 中找到。
清单 6-8。向 POM 添加与 Quartz 相关的依赖项
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>1.8.3 /version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
清单 6-8 中的配置假设您使用的是 Spring 批处理项目中包含的 POM 文件。如果不是,则需要包含版本号来代替所提供的属性。
有了类路径中现在可用的适当代码,您就可以编写SpringBatchQuartzJobLauncher
。然而,在你写代码之前,让我们先讨论一下它要做什么。在这种情况下,SpringBatchQuartzJobLauncher
取代了第五章中的JMXJobLauncher
等等。JMXJobLauncher
不接受任何作业参数作为作业执行的一部分。对于这个示例,您的作业需要两个参数:您希望清空的目录的路径和您希望删除的文件的期限。为了简单起见,您删除了在给定时间段内没有修改的所有文件。
您的 Quartz 作业运行器不仅可以接受传入的参数,而且为了防止作业无法多次运行,您可以使用 Spring Batch 的参数增量器功能(在第四章的中讨论)为每次运行提供一组唯一的参数。清单 6-9 完整地展示了SpringBatchQuartzJobLauncher
。
清单 6-9。??SpringBatchQuartzJobLauncher
`package com.apress.springbatch.chapter6;
import java.util.List;
import java.util.Map;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersIncrementer;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.JobParametersNotFoundException;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class SpringBatchQuartzJobLauncher extends QuartzJobBean {
private JobLauncher jobLauncher;
private JobLocator jobLocator;
private JobExplorer jobExplorer;
private Map<String, String> jobParameters;
public static final String JOB_NAME = "jobName";
private static final Logger log = LoggerFactory
.getLogger(SpringBatchQuartzJobLauncher.class);
@Override
@SuppressWarnings("unchecked")
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
Map<String, Object> jobDataMap = context.getMergedJobDataMap();
String jobName = (String) jobDataMap.get(JOB_NAME);
try {
Job job = jobLocator.getJob(jobName);
JobParameters allParams = translateParams(job, jobParameters);
jobLauncher.run(job, allParams);
} catch (Exception e) {
log.error("Could not execute job.", e);
}
}
private JobParameters translateParams(Job job,
Map<String, String> params)
throws Exception {
JobParametersBuilder builder = new JobParametersBuilder();
JobParameters incrementedParams = getNextJobParameters(job);
for (Map.Entry<String, JobParameter> param :
incrementedParams.getParameters().entrySet()) {
builder.addParameter(param.getKey(), param.getValue());
}
for (Map.Entry<String, String> param : params.entrySet()) {
builder.addString(param.getKey(), param.getValue());
}
return builder.toJobParameters();
}
private JobParameters getNextJobParameters(Job job)
throws JobParametersNotFoundException {
String jobIdentifier = job.getName();
JobParameters jobParameters;
List
jobExplorer.getJobInstances(jobIdentifier, 0, 1);
JobParametersIncrementer incrementer =
job.getJobParametersIncrementer();
if (incrementer == null) {
throw new JobParametersNotFoundException(
"No job parameters incrementer found for job="
+ jobIdentifier);
}
if (lastInstances.isEmpty()) {
jobParameters = incrementer.getNext(new JobParameters());
if (jobParameters == null) {
throw new JobParametersNotFoundException(
"No bootstrap parameters found from incrementer for job="
+ jobIdentifier);
}
} else {
jobParameters = incrementer.getNext(lastInstances.get(0)
.getJobParameters());
}
return jobParameters;
}
public void setJobLauncher(JobLauncher jobLauncher) {
this.jobLauncher = jobLauncher;
}
public void setJobLocator(JobLocator jobLocator) {
this.jobLocator = jobLocator;
}
public void setJobParameters(Map<String, String> jobParameters) {
this.jobParameters = jobParameters;
}
public void setJobExplorer(JobExplorer jobExplorer) {
this.jobExplorer = jobExplorer;
}
}`
查看这段代码时,请注意 Quartz 和 Spring Batch 之间的类名有很多冲突。为了理解发生了什么,让我们从查看执行环境的结构开始。你有一个扩展 Spring 的QuartzJobBean
的类。Quartz 的org.quartz.Job
接口的这个实现是一个有用的类,它允许您只实现与您的工作相关的逻辑部分,而将调度器等操作留给 Spring。在这种情况下,您将覆盖执行作业的executeInternal
方法。
在executeInternal
方法中,首先获取JobDataMap
,这是一个通过 Spring 配置传递给 Quartz 作业的参数的Map
(在学习完这个类后,您将看到配置)。这个Map
包含了注入到SpringBatchQuartzJobLauncher
类中的所有依赖项,以及您可能想要引用的任何附加参数。在这种情况下,您希望引用另一个参数:作业的名称。
获得作业的名称后,使用JobLocator
从JobRegistry
中检索 Spring 批处理作业。在执行作业之前,您需要将通过 Spring 作为<String, String>
的Map
传递的参数转换成 Spring 批处理JobParameters
集合。完成后,您可以使用JobLauncher
执行作业。
注意,这个类中作业的实际执行并不需要太多代码。这个类的绝大部分致力于作业参数的转换和递增。另外两个方法,translateParams
和getNextJobParameters
用于将从 Spring 接收的参数翻译成JobParameters
,并调用配置的参数增量器。
translateParams
首先创建 Spring Batch 的org.springframework.batch.core.JobParametersBuilder
实例。这个类用于获取键值对,并将它们转换成JobParameter
实例。要开始转换,您需要调用getNextJobParameters
1 方法来增加任何需要增加的参数。因为该流程返回一个JobParameters
实例,所以您可以将这些参数添加到您当前正在使用的JobParametersBuilder
中。添加了递增的参数后,您可以添加 Spring 传递的参数。在这种情况下,您知道它们都是String
s,您可以相应地简化代码。
写完之后,你可以继续写这个工作的基础。在这种情况下,您有一个简单的小任务(类似于您在《??》第二章中编写的HelloWorld
小任务),它删除指定目录中超过给定时间没有修改的所有文件。清单 6-10 显示了实现这一点的代码。
清单 6-10。??DeleteFilesTasklet
`package com.apress.springbatch.chapter6;
import java.io.File;
import java.util.Date;
import java.util.Map;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
public class DeleteFilesTasklet implements Tasklet {
public RepeatStatus execute(StepContribution step, ChunkContext chunk)
throws Exception {
Map<String, Object> params =
chunk.getStepContext().getJobParameters();
String path = (String) params.get("path");
Long age = Long.valueOf((String) params.get("age"));
File tempDirectory = new File(path);
File[] files = tempDirectory.listFiles();
Date now = new Date();
long oldesttime = now.getTime() - age;
for (File file : files) {
if (file.lastModified() < oldesttime) {
file.delete();
}
}
return RepeatStatus.FINISHED;
}
}`
1getNextJobParameters
方法的源代码来自 Spring Batch 的CommandLineJobRunner
。不幸的是,没有标准的方式来启动需要参数和增量参数的作业(JobOperator 调用 incrementer 但不接受参数,JobLauncher 接受参数但不调用 incrementer)。
清单 6-10 中DeleteFilesTasklet
的代码不应该让人感到惊讶。在实现Tasklet
接口时,您实现了execute
方法来完成您的所有工作。对于DeleteFilesTasklet
的工作来说,你需要知道文件从哪里删除,以及它们闲置了多长时间。有了这些信息后,您就可以继续删除文件了。
execute
方法的前三行检索作业参数,这样您就可以获得要删除文件的目录的路径(path
)和文件未被修改的时间(毫秒)(age
)。当您拥有作业参数时,您可以打开一个目录并删除所有符合您要求的文件或目录。处理完成后,您返回RepeatStatus.FINISHED
来告诉 Spring Batch 该步骤已经完成。
要做到这一点,您需要做的就是进行配置。同样,您使用的是JobRegistryBackgroundJobRunner
,所以配置文件是分开的:launch-context.xml
位于<project_home>/src/main/resources
目录中,deleteFilesJob.xml
位于<project_home>/src/main/resources/jobs
目录中。首先看一下deleteFilesJob.xml
文件,在清单 6-11 中,显示了作业本身的配置。
清单 6-11。??deleteFilesJob.xml
`
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:bean id="deleteFilesTasklet"
class="com.apress.springbatch.chapter6.DeleteFilesTasklet" />
<beans:bean id="idIncrementer"
class="org.springframework.batch.core.launch.support.RunIdIncrementer"/>
</beans:beans>`
与您的其他作业一样,您定义了微线程本身,然后有一个使用微线程的步骤,然后以作业的定义结束,这是一个在作业运行时删除文件的单一步骤。这个作业得到的唯一附加配置是添加了RunIdIncrementer
,如第四章中的所述,因此您可以通过 Quartz 多次运行这个作业,而不必更改作业参数。
launch-context.xml
主要由通常的 SpringBatch 嫌疑人组成,如清单 6-12 所示。您只需要添加三个 beans 来使 Quartz 交互工作:jobDetail
、cronTrigger
和schedule
。
清单 6-12。更新为launch-context.xml
`...
...`
在石英示例的launch-context.xml
中,第一个重要的 bean 是jobDetail
bean。这里是你配置SpringBatchQuartzJobLauncher
的地方。注意如何使用 Spring 的JobDetailBean
和SpringBatchQuartzJobLauncher
bean 之间的关系是很重要的。您在launch-context.xml
中将一个JobDetailBean
配置为一个 Spring bean。每次执行 Quartz 作业时,JobDetailBean
都会创建一个新的SpringBatchQuartzJobLauncher
实例。当配置JobDetailBean
、jobDetail
时,您设置了两个属性:
jobClass
:这是每次 Quartz 作业运行时被实例化和执行的类。jobDataAsMap
:这个Map
是注入到SpringBatchQuartzJobLauncher
中的所有对象的集合,以及 Quartz 作业运行所需的任何其他参数(要删除的文件的位置以及它们需要多长时间)。
当 Quartz 作业被触发并且 Spring 创建了一个新的SpringBatchQuartzJobLauncher
实例时,Spring 使用jobDataAsMap
根据需要注入任何需要的依赖项。在这种情况下,您输入要删除的文件的位置以及它们需要多长时间。
在launch-context.xml
中要查看的第二个 bean 是cronTrigger
bean。您之前看到了 Quartz 如何使用触发器来确定何时执行作业。这里使用的触发器根据 cron 字符串决定何时运行。为了配置触发器,您创建一个具有两个依赖项的org.springframework.scheduling.quartz.CronTriggerBean
:jobDetail
引用作业Detail
bean,而cronExpression
是用于确定作业何时运行的 cron 字符串。在这种情况下,您每 10 秒执行一次deleteFilesJob
。
要配置的最后一个 bean 是完成所有工作的 bean:调度程序。使用 Spring 的org.springframework.scheduling.quartz.SchedulerFactoryBean
,向调度程序注册您的触发器。接下来,Spring 和 Quartz 会处理剩下的事情。
为了让事情开始,使用JobRegistryBackgroundJobRunner
来引导这个过程。使用清单 6-13 中的命令启动这个过程,引导 Spring,在 JobRegistry 中注册deleteFilesJob
,并启动 Quartz。在 Quartz 运行的情况下,作业按照配置每 10 秒执行一次。
清单 6-13。通过石英执行工作
java -jar deleteFiles.jar launch-context.xml jobs/deleteFilesJob.xml
通过 Quartz 或其他调度程序运行作业是企业中管理批处理的常见方式。在企业中运行作业的另一个常见方面是它们的部署方式。鉴于许多企业将所有或大多数 Java 应用部署到某种容器中,您应该看看 Spring 批处理是如何在容器中运行的。
在容器中运行
与 web 应用不同,批处理不需要容器来执行。您可以使用数据库连接池、事务管理和 JMS 等现成的框架来构建健壮且完全独立的批处理作业,而不需要应用服务器或 servlet 容器。尽管如此,在容器中运行作业的理由和不在容器中运行作业的理由一样多。
在企业中,围绕基于容器的应用的配置和部署通常有一个更强大的专业基础,其中部署为 jar 文件集合的普通 Java 应用可能会导致一些操作团队暂停。此外,像数据库连接(及其安全性)、JMS 队列等资源可能更容易通过容器中的标准化配置来管理。让我们看看如何从 Tomcat 内部部署和执行作业。
虽然使用 Spring 可以通过应用服务器配置大量资源,但是它们的配置超出了本书的范围。相反,您关注于用所需的作业引导 Spring Batch JobRegistry 并触发它们的执行。
从一个容器中引导作业注册比您到目前为止所做的更容易。Tomcat 不是依赖于CommandLineJobRunner
或JobRegistryBackgroundJobRunner
的main
方法来引导您的进程,而是作为启动 Java 进程的手段,您可以使用标准的 web 技术在您的容器中引导 Spring Batch。
让我们看看如何将deleteFilesJob
部署到 Tomcat。有四个步骤:
- 将 POM 文件作为 war 文件而不是 jar 文件更新到包应用。
- 将所需的 Spring web 相关依赖项添加到 POM 文件中。
- 用
ContextLoaderListener
在新目录<project_home>/src/main/webapp/WEB-INF
中创建web.xml
来引导 Spring。 - 配置
AutomaticJobRegistrar
向 JobRegistry 注册作业。
从更新 POM 文件开始。为了让 Spring 在像 Tomcat 这样的 servlet 容器中引导,您需要添加 Spring 框架的 web 依赖项。清单 6-14 显示了如何配置这项工作所需的附加依赖项,包括 Spring 的 web 模块、SLF4J 和 log4j。
清单 6-14。附加依赖关系为pom.xml
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.5.8</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.14</version> </dependency>
您希望创建一个可以部署到 Tomcat 的 war 文件,而不是创建一个 jar 文件供您独立执行。为此,您需要将 POM 文件中的打包从 jar 更新到 war。您还可以用maven-war-plugin
替换对maven-jar-plugin
的引用。清单 6-15 展示了如何配置 POM 文件,以便你的应用被正确打包。
清单 6-15。修改后的pom.xml
<plugin> <artifactId>maven-war-plugin</artifactId> <version>2.1-beta-1</version> <configuration> <attachClasses>true</attachClasses> <warName>deleteFiles</warName> </configuration> </plugin>
如清单 6-15 所示,配置 Maven 来生成你的 war 文件是非常容易的。不幸的是,在您创建一个web.xml
文件并将其放在正确的位置之前,对pom.xml
文件的更改不会生效。对于web.xml
,您配置了一个监听器,Spring 的org.springframework.web.context.ContextLoaderListener
。它为你启动 Spring 和 JobRegistry。清单 6-16 显示了web.xml
文件。
清单 6-16。web.xml
`
org.springframework.web.context.ContextLoaderListener
`
web.xml
文件存在于<project_home>/src/main/webapp/WEB-INF/
目录中。POM 文件更新后,在正确的位置定义了web.xml
文件,您可以使用标准的mvn clean install
生成一个 war 文件。
现在您已经有了一个工作 war 文件,让我们配置AutomaticJobRegistrar
在启动时在 JobRegistry 中注册作业。正如前面的例子一样,AutomaticJobRegistrar
被配置为注册类路径的/jobs/
目录中列出的所有作业。从那里,您可以像在本章前面所做的那样使用 Quartz 来启动作业。清单 6-17 显示了配置有先前石英配置和AutomaticJobRegistrar
的launch-context.xml
。
清单 6-17。更新为launch-context.xml
... <bean class="org.springframework.batch.core.configuration.support. AutomaticJobRegistrar"> <property name="applicationContextFactories"> <bean class="org.springframework.batch.core.configuration.support. ClasspathXmlApplicationContextsFactoryBean"> <property name="resources" value="classpath*:/jobs/deleteFilesJob.xml" /> </bean> </property> <property name="jobLoader"> <bean class="org.springframework.batch.core.configuration.support. DefaultJobLoader"> <property name="jobRegistry" ref="jobRegistry" /> </bean> </property>
</bean> ...
配置并构建好作业后,您需要做的就是将它部署到 Tomcat 服务器上。当您将 war 文件复制到<TOMCAT_HOME>/webapps
并通过执行<TOMCAT_HOME>/bin
中的./startup.sh
命令来启动服务器时,应用启动,作业通过 Quartz 每 10 秒执行一次。你怎么知道它在跑?您可以确认文件正在按预期删除,并验证<TOMCAT_HOME>/logs/catalina.out
文件中的输出,如清单 6-18 所示。
清单 6-18。catalina.out
中的作业输出
`2011-01-04 21:07:50,103 DEBUG SimpleAsyncTaskExecutor-2
[org.springframework.batch.core.job.AbstractJob] - <Job execution complete:
JobExecution: id=151, startTime=Tue Jan 04 21:07:50 CST 2011, endTime=null,
lastUpdated=Tue Jan 04 21:07:50 CST 2011, status=COMPLETED,
exitStatus=exitCode=COMPLETED;exitDescription=, job=[JobInstance: id=144,
JobParameters=[{age=9000, run.id=49, path=/Users/mminella/temp}],
Job=[deleteFilesJob]]>
2011-01-04 21:07:50,105 INFO SimpleAsyncTaskExecutor-2
[org.springframework.batch.core.launch.support.SimpleJobLauncher] - <Job:
[FlowJob: [name=deleteFilesJob]] completed with the following parameters:
[{age=9000, run.id=49, path=/Users/mminella/temp}] and the following status:
[COMPLETED]>`
在容器中运行 Spring 批处理作业在企业环境中提供了许多优势,包括标准化的打包和部署以及更健壮的管理选项。在企业环境中运行作业的另一个方面是通过操作监控和管理作业的能力。让我们看看如何使用 Spring Batch Admin 项目来启动作业。
使用 Spring Batch Admin 启动
Spring Batch Admin 是 Spring Batch 工作原理的最新补充。与其说它是一个管理工具,不如说它是一个管理框架。在 2010 年初推出 1.0 版本后,它的功能和在 Spring 家族中的角色仍在不断发展。然而,它是一个有用的工具,不仅可以将 web 界面放到 JobRepository 上,还允许通过 web 界面执行作业。
要查看 Spring Batch Admin,请将其添加到现有的deleteFiles
应用中。因为您已经有了通过 Tomcat 部署作业的结构,所以添加 Spring Batch Admin 为您查看 JobRepository 以及管理作业(启动、停止等)提供了一个 web 界面。值得注意的是,您不需要用 Spring Batch Admin 打包作业就可以浏览 Spring Batch 的 job repository——您只需要管理它们的执行。
要将 Spring Batch Admin 添加到您的应用中,请执行以下操作:
- 更新 POM 文件,在 war 文件中包含所需的 jar 文件。
- 更新
web.xml
以包含引导 Spring Batch Admin 所需的元素。 - 移动
launch-context.xml
文件,以便 Spring Batch Admin 使用它来覆盖它的组件。
对于 Spring Batch,如果您还没有注意到的话,您一直在使用的示例项目附带的 POM 文件是用 Spring 2 . 5 . 6 版构建的。显然,这是一个老版本的 Spring。Spring Batch 在较新的 Spring 3.0.x 上运行良好;因为 Spring Batch Admin 需要较新版本的 Spring,所以对 POM 文件的第一次更新是将您正在使用的 Spring 版本更新到 3.0.5.RELEASE,如清单 6-19 所示。
清单 6-19。更新了pom.xml
中的属性
... <properties> **<maven.test.failure.ignore>true</maven.test.failure.ignore><spring.framework.version>3.0.** **5.RELEASE</spring.framework.version>** <spring.batch.version>2.1.2.RELEASE</spring.batch.version> <dependency.locations.enabled>false</dependency.locations.enabled> </properties> ...
您需要对 AspectJ 依赖项进行另一个版本的更改。您需要使用更新的 1.6.6 版本,而不是您的 shell 附带的 1.5.4 依赖项。最后,将依赖项添加到 Spring Batch 管理 jar 文件中。清单 6-20 显示了pom.xml
中新的和更新的依赖关系。
清单 6-20。新增和更新的依赖关系
... <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.6</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-admin-manager</artifactId> <version>1.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-admin-resources</artifactId> <version>1.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-integration</artifactId> <version>1.2.0.RELEASE</version>
</dependency> ...
现在项目已经有了正确的依赖项和版本,您可以更新web.xml
。对于您之前使用的deleteFilesJob
,您有一个监听器,Spring 的ContextLoaderListener
,用于引导 Spring 配置。对于 Spring Batch Admin,您还需要一些东西,如清单 6-21 中的所示。
清单 6-21。更新web.xml
`
classpath*:/org/springframework/batch/admin/web/resources/webapp-config.xml
org.springframework.web.filter.HiddenHttpMethodFilter
org.springframework.web.context.ContextLoaderListener
org.springframework.web.servlet.DispatcherServlet
classpath*:/org/springframework/batch/admin/web/resources/servlet-config.xml
`
从清单 6-21 中的所示的web.xml
文件开始,你就有了应用的显示名称和描述。之后,配置基本 Spring 上下文配置的位置。在本例中,您使用的是包含在spring-batch-admin-resources-1.2.0.RELEASE
jar 文件webapp-config.xml
中的一个。该文件包含 Spring Batch Admin 运行所需的 beans,还为您提供了一个覆盖和扩展任何组件的工具。
接下来的两个配置hiddenHttpMethodFilter
的元素,被用作不支持所有标准 HTTP 方法的浏览器的变通方法。 2 因为 Spring Batch Admin 通过 RESTful API 公开了许多特性,所以它使用这个过滤器来支持一种通用的技术,用于指示通过请求头调用的 HTTP 方法。
像在任何普通的基于 Spring 的 web 应用中一样,在 bootstrap Spring 旁边配置ContextLoaderListener
。最后,您有一个为 Spring Batch 管理应用做繁重工作的 servlet。与大多数基于 Spring 的 web 应用一样,您使用org.springframework.web.servlet.DispatcherServlet
根据配置将请求定向到 beans。在这种情况下,Spring Batch Admin 附带了一个包含所需映射的配置(也可以在spring-batch-admin-resources -1.2.0.RELEASE.jar
) servlet-config.xml
中找到)。
Spring 批处理管理难题的最后一块是移动launch-context.xml
。您可能想知道为什么需要移动它。原因是 Spring Batch Admin 应用有一个针对许多 beanss 的嵌入式配置,这些 bean 是您已经默认配置好的(JobExplorer
、JobRepository
、一个数据源等等)。但是,它还提供了通过将覆盖配置放在 war 文件的<WAR_ROOT>/META-INF/spring/batch/override
目录中来覆盖这些配置的能力。在这种情况下,处理您想要的覆盖的简单方法是将launch-context.xml
从<PROJECT_ROOT>/src/main/resources
目录移动到<PROJECT_ROOT>/src/main/resources/META-INF/spring/batch/override
目录。当 Spring Batch Admin 启动时,它使用您的配置而不是提供的默认配置。
剩下的工作就是构建您的 war 文件,并像以前一样将其部署到 Tomcat。使用mvn clean install
命令,您最终会得到一个 war 文件,您可以将它放入 Tomcat 的webapps
目录并启动 Tomcat。运行 Tomcat,启动浏览器并导航到[
localhost:8080/deleteFiles](http://localhost:8080/deleteFiles)
查看管理应用,如图图 6-2 所示。
2 大多数现代浏览器只支持 HTTP POST
和GET
,但是真正的 RESTful 实现也需要PUT
和DELETE
的支持。
图 6-2。Spring 批处理管理应用的主页
Spring Batch Admin 应用的主页显示了可用的 REST APIs 列表。顶部的选项卡允许您访问已经在 JobRepository 中执行的作业,查看已经执行或当前正在运行的作业,以及上传新的配置文件。
要查看您过去运行的作业以及您可以从 Spring Batch Admin 管理的任何作业,请点按“作业”标签。这样做会将你带到如图图 6-3 所示的页面。
图 6-3。作业库中的作业列表
“作业”页面列出了作业存储库中出现的所有作业。每个作业已经执行的次数,该作业是否可以通过当前配置执行,以及是否配置了JobParametersIncrementer
都显示在该页面上。注意在这个例子中,deleteFilesJob
已经被执行了 2666 次(每 10 秒钟快速累加一次)。deleteFilesJob
也是唯一可启动的作业,所以单击该链接查看如何执行它。
在图 6-4 中显示的deleteFilesJob
页面,从执行作业所需的控件开始,包括一个启动作业的按钮和一个填充有上次运行的作业参数的文本框。在deleteFilesJob
的例子中,因为它配置了一个JobParametersIncrementer
,所以您可以将相同的参数传递给作业;Spring Batch 处理递增的run.id
参数,因此您有一个惟一的 JobInstance。
图 6-4。求职页面为deleteFilesJob
要从该页面执行deleteFilesJob
,您只需点击启动按钮。Spring Batch 执行JobParametersIncrementer
以便一切正常。如果您使用一组重复的参数启动一个作业,则会显示一个错误,并且不会创建任何作业实例或作业执行,如图图 6-5 所示。
图 6-5。运行重复的作业实例
正如您所看到的,有许多方法可以启动 Spring 批处理作业:通过命令行的CommandLineJobRunner
,使用另一个协议,如 JMX 和您在第五章中看到的自定义作业运行器,使用 Quartz 之类的调度程序,甚至使用 Spring Batch Admin web 应用。然而,原谅我的双关语,开始工作只是一个开始。一项工作如何结束会对很多事情产生很大的影响。下一节将介绍 Spring 批处理作业的不同结束方式,以及这些场景如何影响您配置或执行作业的方式。
停止工作
一个作业可能因为许多原因而停止,每一个原因都对接下来发生的事情有自己的影响。它可以自然地运行到完成(到目前为止所有的例子都是如此)。出于某种原因,您可以在处理过程中以编程方式停止作业的执行。您可以从外部停止作业(例如,有人意识到有问题,他们需要停止作业来修复它)。当然,尽管您可能永远不会承认,但是可能会发生导致作业停止执行的错误。这一节将介绍如何使用 Spring Batch 来处理这些场景,以及在每种情况发生时应该做些什么。让我们从最基本的开始:一个运行到自然完成的作业。
自然结束
到目前为止,您的所有作业都已运行到自然完成状态。也就是说,每个作业已经运行了它的所有步骤,直到它们返回了一个COMPLETED
状态,并且作业本身返回了一个退出代码COMPLETED
。这对一份工作意味着什么?
正如您所看到的,一个作业不能使用相同的参数值多次成功执行。这是那句话的成功部分。当作业运行到COMPLETED
退出代码时,不能再次使用相同的作业参数创建新的作业实例。这一点很重要,因为它决定了如何执行作业。您已经使用了JobParametersIncrementer
来增加基于运行的参数,这是一个好主意,尤其是在基于某种调度运行的作业中。例如,如果您有一个每天运行的作业,开发一个将时间戳作为参数递增的JobParametersIncrementer
实现是有意义的。这样,每次通过调度执行作业时,您都可以使用–next
标志来相应地增加作业。如果发生了导致作业无法正常完成的事情,您可以在没有–next
标志的情况下执行作业,提供相同的参数(您将在本章后面看到如何重新启动作业)。
并不是所有的作业每次都能自然结束。有些情况下,您希望根据处理过程中发生的事情停止作业(例如,步骤结束时的完整性检查失败)。在这种情况下,您希望以编程方式停止作业。下一节将介绍这种技术。
纲领性结局
批处理需要一系列的检查和平衡才能有效。当您处理大量数据时,您需要能够验证在处理过程中发生了什么。用户在 web 应用上用错误的地址更新个人资料是一回事。影响一个用户。但是,如果您的工作是导入一个包含 100 万条记录的文件,而导入步骤只导入了 10,000 条记录就完成了,该怎么办呢?出了问题,您需要在工作继续进行之前解决它。本节介绍如何以编程方式停止作业。首先你看一个更真实的例子,使用在第四章中介绍的<stop>
标签;为了重新启动作业,您使用了一些新的属性。您还将了解如何设置结束作业的标志。
使用<停止>标签
首先,让我们看一下构造一个被配置为停止使用
- 导入一个简单的交易文件(
transaction.csv
)。每笔交易都由一个账号、一个时间戳和一个金额组成(正数是贷方,负数是借方)。文件以包含文件中记录数量的单个摘要记录结束。 - 将交易导入交易表后,将它们应用于由帐号和当前帐户余额组成的单独的帐户汇总表。
- 生成一个摘要文件(
summary.csv
),列出每个账户的账号和余额。
从设计的角度来看这些步骤,您希望在将交易应用到每个用户的帐户之前,验证您导入的记录数是否与摘要文件相匹配。在处理大量数据时,这种完整性检查可以为您节省大量的恢复和重新处理时间。
要开始这项工作,让我们看看文件格式和数据模型。此作业的文件格式是简单的逗号分隔值(CSV)文件。这让您无需代码就可以轻松配置适当的读取器和写入器。清单 6-22 显示了您正在使用的两个文件的示例记录格式(分别为transaction.csv
和summary.csv
)。
清单 6-22。两个文件各自的样本记录
`Transaction file:
3985729387,2010-01-08 12:15:26,523.65
3985729387,2010-01-08 1:28:58,-25.93
2
Summary File:
3985729387,497.72`
对于本例,您还保持了简单的数据模型,只包含两个表:Transaction 和 Account_Summary。图 6-6 显示了数据模型。
图 6-6。交易数据模型
要创建这个作业,从 zip 发行版中的 Spring Batch shell 的一个新副本开始,如前几章所述。设置好项目后,配置batch.properties
使用您的 MySQL 实例,就像您到目前为止所做的那样。
当样板 shell 准备就绪时,您就可以配置作业了。在<PROJECT_HOME>/src/main/resources/jobs
目录下创建一个新文件transactionJob.xml
,如清单 6-23 所示配置作业。
清单 6-23。??transactionJob.xml
<?xml version="1.0" encoding="UTF-8"?> <beans:beans
`xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch-2.1.xsd">
<beans:import resource="../launch-context.xml"/>
<beans:bean id="transactionFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[transactionFile]}"/>
</beans:bean>
<beans:bean id="transactionFileReader"
class="com.apress.springbatch.chapter6.TransactionReader">
<beans:property name="fieldSetReader" ref="fileItemReader"/>
</beans:bean>
<beans:bean id="fileItemReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" ref="transactionFile" />
<beans:property name="lineMapper">
<beans:bean
class="org.springframework.batch.item.file.mapping.
DefaultLineMapper">
<beans:property name="lineTokenizer">
<beans:bean
class="org.springframework.batch.item.file.transform.
DelimitedLineTokenizer">
<beans:property name="delimiter" value=","/>
</beans:bean>
</beans:property>
<beans:property name="fieldSetMapper">
<beans:bean
class="org.springframework.batch.item.file.mapping.
PassThroughFieldSetMapper" />
</beans:property>
</beans:bean>
</beans:property>
</beans:bean>
<beans:bean id="transactionWriter"
class="org.springframework.batch.item.database.JdbcBatchItemWriter">
<beans:property name="assertUpdates" value="true" />
<beans:property name="itemSqlParameterSourceProvider">
<beans:bean class="org.springframework.batch.item.database.
BeanPropertyItemSqlParameterSourceProvider" />
</beans:property>
<beans:property name="sql" value="INSERT INTO TRANSACTION
(ACCOUNT_SUMMARY_ID, TIMESTAMP, AMOUNT) VALUES ((SELECT ID FROM
ACCOUNT_SUMMARY WHERE ACCOUNT_NUMBER = :accountNumber), :timestamp, :amount)"
/>
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
...`
这项工作的第一步包括输入文件、两个 ItemReaders(一个完成实际的文件工作,另一个应用一些解释)和一个 ItemWriter。配置从输入文件的定义开始,一个用于读取文件本身的FlatFileItemReader
包装器和相关的FlatFileItemReader
。(第七章和第九章分别介绍了 ItemReaders 和 ItemWriters。)对于这个例子,您需要担心的是这是输入文件的配置以及您读取它的方式。包装的原因有两个。首先,它用于确定记录是常规事务记录还是文件末尾的汇总记录。其次,它被用作一个StepListener
,以确定是否处理了正确的记录数。如果是,则不改变ExitStatus
。如果记录的数量与您的文件的摘要记录不匹配,ExitStatus
改变为返回STOPPED
。本节稍后将介绍这方面的代码。值得注意的是,这个步骤的微线程是用设置为true
的allow-start-if-complete
属性配置的。通过以这种方式配置该步骤,当作业由于任何原因停止时,即使它已经成功完成,作业也可以重新执行该步骤。默认情况下,这个值是false
;如果该步骤成功完成,重试时将被跳过。
transactionJob
的配置在清单 6-24 中继续,在这里您配置第二步(applyTransactionStep
)及其组件。
清单 6-24。applyTransactionStep
及其部件的配置
... <beans:bean id="accountSummaryReader" class="org.springframework.batch.item.database.JdbcCursorItemReader"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="sql" value="select account_number, current_balance from account_summary a where a.id in (select distinct t.account_summary_id from transaction t) order by a.account_number"/> <beans:property name="rowMapper"> <beans:bean class="com.apress.springbatch.chapter6.AccountSummaryRowMapper"/> </beans:property>
`</beans:bean>
<beans:bean id="transactionDao"
class="com.apress.springbatch.chapter6.TransactionDaoImpl">
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
<beans:bean id="transactionApplierProcessor"
class="com.apress.springbatch.chapter6.TransactionApplierProcessor">
<beans:property name="transactionDao" ref="transactionDao"/>
</beans:bean>
<beans:bean id="accountSummaryUpdater"
class="org.springframework.batch.item.database.JdbcBatchItemWriter">
<beans:property name="assertUpdates" value="true" />
<beans:property name="itemSqlParameterSourceProvider">
<beans:bean class="org.springframework.batch.item.database.
BeanPropertyItemSqlParameterSourceProvider" />
</beans:property>
<beans:property name="sql" value="UPDATE ACCOUNT_SUMMARY SET
CURRENT_BALANCE = :currentBalance WHERE ACCOUNT_NUMBER = :accountNumber" />
这项工作的第二步是将交易应用到用户的帐户。这一步的配置从 ItemReader 开始,您使用它从数据库中读取帐户摘要记录。当读取每个项目时,您用在前面的步骤中导入的每个交易在transactionApplierProcessor
中更新 currentBalance 字段。这个 ItemProcessor 使用一个数据访问对象(DAO)在处理交易时查找帐户的交易。最后,使用accountSummaryUpdater
ItemWriter 用新的 currentBalance 值更新帐户。在清单 6-24 的末尾,步骤本身的配置将 ItemReader、ItemProcessor 和 ItemWriter 链接在一起。
作业中的最后一步generateAccountSummaryStep
,由在清单 6-24 中配置的相同的accountSummaryReader
组成,但是它添加了一个新的 ItemWriter 来编写摘要文件。清单 6-25 显示了新 ItemWriter 的配置和相关步骤。
清单 6-25。 generateAccountSummaryStep
配置
`…
<beans:bean id="summaryFile"
class="org.springframework.core.io.FileSystemResource" scope="step">
<beans:constructor-arg value="#{jobParameters[summaryFile]}"/>
</beans:bean>
<beans:bean id="accountSummaryWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter"
scope="step">
<beans:property name="lineAggregator">
<beans:bean
class="org.springframework.batch.item.file.transform.
DelimitedLineAggregator">
<beans:property name="delimiter" value=","/>
<beans:property name="fieldExtractor">
<beans:bean
class="org.springframework.batch.item.file.transform.
BeanWrapperFieldExtractor">
<beans:property name="names"
value="accountNumber,currentBalance"/>
</beans:bean>
</beans:property>
</beans:bean>
</beans:property>
<beans:property name="resource" ref="summaryFile" />
</beans:bean>
配置完所有步骤后,您最终可以配置作业本身。在该作业中,您将按照讨论的方式配置这三个步骤。但是,对于step1
,如果步骤返回STOPPED
,则停止作业。如果作业重新启动,它会重新执行step1
。如果step1
返回任何其他成功的值,则作业继续进行step2
,最后是step3
。清单 6-26 显示了使用<stop>
标签的逻辑配置。
清单 6-26。使用<stop>
标签配置作业
… <job id="transactionJob"> <step id="step1" parent="importTransactionFileStep"> <stop on="STOPPED" restart="step1"/> <next on="*" to="step2"/> </step> <step id="step2" parent="applyTransactionsStep" next="step3"/> <step id="step3" parent="generateAccountSummaryStep"/> </job> </beans:beans>
如何执行检查以确保读入正确的记录数?在这种情况下,您可以开发一个自定义读取器,它也可以用作步骤侦听器。清单 6-27 中的代码展示了阅读器如何读入所有记录,并记录有多少被读入。当该步骤完成时,监听器验证读入的记录数是否与预期的记录数相匹配。如果它们匹配,则返回由常规处理确定的ExitStatus
。如果它们不匹配,您可以通过返回自己的ExitStatus.STOPPED
值来覆盖ExitStatus
。
清单 6-27。TransactionReader.java
`package com.apress.springbatch.chapter6;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.batch.item.file.transform.FieldSet;
public class TransactionReader implements ItemReader