SpringBatch-高级教程-一-

260 阅读1小时+

SpringBatch 高级教程(一)

原文:Pro Spring Batch

协议:CC BY-NC-SA 4.0

一、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 应用使用的相同对象和数据源。

images

***图 1-1。*简化并行处理

最后,您会看到持续或 24/7 处理。在许多使用案例中,系统接收恒定或接近恒定的数据馈送。虽然接受数据的速度对于防止积压是必要的,但是当你查看数据的处理时,将数据分成一次处理的块可能更有效(如图图 1-2 所示)。Spring Batch 提供了一些工具,可以让您以一种可靠的、可伸缩的方式进行这种类型的处理。使用该框架的特性,您可以做一些事情,比如从队列中读取消息,将它们分批成块,并在一个永无止境的循环中一起处理它们。因此,您可以在高容量的情况下增加吞吐量,而不必理解从头开发这样一个解决方案的复杂细微差别。

images

***图 1-2。*批处理 JMS 处理以提高吞吐量

正如您所看到的,Spring Batch 是一个框架,虽然它是为类似大型机的处理而设计的,但可以用来简化各种开发问题。了解了 batch 是什么以及为什么应该使用 Spring Batch 之后,让我们最终开始看看框架本身。

SpringBatch 框架

Spring Batch 框架(Spring Batch)是 Accenture 和 SpringSource 合作开发的,作为一种基于标准的方法来实现常见的批处理模式和范例。

Spring Batch 实现的特性包括数据验证、输出格式化、以可重用方式实现复杂业务规则的能力,以及处理大型数据集的能力。当你仔细阅读本书中的例子时,你会发现,如果你对 Spring 很熟悉,Spring Batch 是有意义的。

让我们从框架的 30000 英尺视图开始,如图图 1-3 所示。

images

***图 1-3。*SpringBatch 建筑

Spring Batch 由分层配置的三层组成。顶部是应用层,它由所有定制代码和配置组成,用于构建您的批处理过程。您的业务逻辑、服务等等,以及如何组织作业的配置,都被视为应用。请注意,应用层并不位于核心层和基础设施层之上,而是包裹着另外两层。原因是,尽管您开发的大部分内容都是由与核心层一起工作的应用层组成的,但有时您会编写定制的基础结构,如定制的读取器和写入器。

应用层大部分时间都在与下一层——核心层——互动。核心层包含定义批处理域的所有部分。核心组件的元素包括作业和步骤接口,以及用于执行作业的接口:JobLauncher 和 JobParameters。

在这一切之下是基础设施层。为了进行任何处理,您需要读取和写入文件、数据库等等。您必须能够处理失败后重试作业时要做的事情。这些部分被认为是公共基础设施,存在于框架的基础设施组件中。

images 注意一个常见的误解是 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 批量作业定义

`<bean id="accountTasklet"   class="com.michaelminella.springbatch.chapter1.AccountTasklet"/>

           `
管理工作

能够编写一个处理一些数据一次就不再运行的 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 所示)以及随着时间的推移监控您的过程的性能的能力。

images

***图 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 步将把所有的借方记入账户。该作业代表将交易应用到用户帐户的整个过程。

images

***图 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。这是一个数据存储(在内存或数据库中),用于保存有关作业和步骤执行的信息。一个作业执行或 步骤执行是关于作业或步骤的单次运行的信息。在本章后面的章节和第五章中,你会看到更多关于执行和存储库的细节。

images

***图 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

images

***图 2-3。*多线程步骤

并行步骤

并行化的下一个方法是并行执行步骤的能力,如图 2-4 所示。假设您有两个步骤,每个步骤将一个输入文件加载到您的数据库中;但是步骤之间没有关系。在加载下一个文件之前必须等待一个文件已经加载,这有意义吗?当然不是,这就是为什么这是一个何时使用并行处理步骤能力的经典例子。

images

***图 2-4。*平行步进加工


1 这是理论上的吞吐量增加。许多因素会阻止进程实现这样的线性并行化。

远程分块

最后两种并行化方法允许您将处理分散到多个 JVM 上。在以前的所有情况下,处理都是在单个 JVM 中执行的,这会严重阻碍可伸缩性选项。当您可以跨多个 JVM 水平扩展流程的任何部分时,满足大量需求的能力就会提高。

第一个远程处理选项是远程分块。在这种方法中,使用主节点中的标准 ItemReader 执行输入;然后,输入通过一种持久通信的形式(例如 JMS)发送到一个远程从属 ItemProcessor,它被配置为消息驱动的 POJO。当处理完成时,从机将更新的项目发送回主机进行写入。因为这种方法在主设备上读取数据,在从设备上处理数据,然后再发送回来,所以需要注意的是,它可能会占用大量网络资源。这种方法适用于 I/O 成本比实际处理成本低的情况。

分区

Spring Batch 中并行化的最后一种方法是分区,如图 2-5 所示。同样,您使用主/从配置;但是这一次您不需要持久的通信方法,主服务器只作为一组从服务器步骤的控制器。在这种情况下,您的每个从属步骤都是独立的,其配置与本地部署的相同。唯一的区别是从属步骤从主节点而不是从作业本身接收工作。当所有的从机都完成了它们的工作,主步骤就被认为完成了。这种配置不需要具有保证交付的持久通信,因为 JobRepository 保证没有重复的工作,并且所有工作都已完成——不像远程分块方法,在远程分块方法中,JobRepository 不知道分布式工作的状态。

images

***图 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 ="www.springframework.org/schema/batc…"        xmlns:beans="www.springframework.org/schema/bean…"        xmlns:xsi="www.w3.org/2001/XMLSch…"        xsi:schemaLocation="www.springframework.org/schema/bean…                     www.springframework.org/schema/bean…                     www.springframework.org/schema/batc…                     www.springframework.org/schema/batc…

    <beans:import resource="../launch-context.xml"/>

    <beans:bean id="helloWorld"                          class="com.apress.springbatch.chapter2.HelloWorld"/>

                  

                   </beans:beans>`

如果这看起来有点眼熟,它应该。这是前面讨论过的高级分解,只是以 XML 的形式。

images 注意尽管大多数 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 做了一些事情,然后发生了其他事情。”在接下来的章节中,您将看到如何处理批处理的故事(假设它们本质上是纯技术性的)。
  • 验收标准:验收标准是可测试的需求,可以用来识别一个故事何时完成。前面陈述中的重要词是可测试的。为了使验收标准有用,它必须能够以某种方式进行验证。这些不是主观的需求,而是开发人员可以用来说“是的,它确实这样做了”或“不,它没有”的硬性要求

让我们看一个通用遥控器的用户故事作为例子:

  • 标题:打开电视
  • *叙述:*作为用户,在关闭电视、接收器和有线电视盒的情况下,我将能够按下我的通用遥控器上的电源按钮。然后,遥控器将打开电视、接收器和有线电视盒,并对它们进行配置以观看电视节目。
  • 验收标准:
    • 在万能遥控器上有一个电源按钮。
    • 当用户按下电源按钮时,将发生以下情况:
      1. 电视机将打开电源。
      2. AV 接收器将通电。
      3. 有线电视盒将通电。
      4. 有线电视盒将被设置到频道 187。
      5. AV 接收器将被设置为 SAT 输入。
      6. 电视机将被设置为视频 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 显示了你期望每个季度从你的股票经纪人那里收到的作为你的经纪账户对账单的邮件。

images

***图 3-1。*经纪声明,格式化并打印在信笺上

如果你分解一下这个陈述是如何产生的,实际上有两个部分。第一张不过是一张漂亮的纸,第二张就印在上面。这是你在本书中创作的第二件作品,如图 3-2 所示。

images

***图 3-2。*纯文本经纪声明

通常,语句创建如下。批处理会创建一个仅包含文本的打印文件。然后,打印文件被发送到打印机,打印机将文本打印到装饰纸上,生成最终报表。打印文件是您使用 Spring Batch 创建的部分。您的批处理将执行以下功能:

  1. 导入客户信息和相关交易的文件。
  2. 从 web 服务中检索数据库中所有股票的收盘价格。
  3. 将先前下载的股票价格导入数据库。
  4. 计算每个账户的定价水平。
  5. 根据上一步计算的水平,计算每笔交易的交易费。
  6. 打印上个月经纪账户的文件。

让我们来看看这些特性都需要什么。为您的工作提供了一个客户事务平面文件,其中包含有关客户及其当月事务的信息。您的作业更新现有的客户信息,并将他们的交易添加到数据库中。当交易被导入后,作业从 web 服务获取数据库中每只股票的最新价格,以便计算每个账户的当前价值。该作业将下载的价格导入数据库。

初始导入完成后,您的工作可以开始计算交易费用。经纪公司通过对每笔交易收取费用来赚钱。这些费用是基于客户一个月的交易量。客户的交易越多,每次交易收取的费用就越少。计算交易费用的第一步是确定用户属于哪个级别或阶层;然后,您可以计算客户交易的价格。当所有的计算都完成后,就可以生成用户的月结单了。

这个特性列表旨在提供一个完整的视图,展示 Spring Batch 在现实问题中是如何使用的。在整本书中,您将了解 Spring Batch 提供的特性,这些特性可以帮助您开发类似于这个场景所需的批处理过程。在第十章中,您实现了批处理作业,以满足以下用户案例中概述的需求:

*导入交易:*作为批处理,我会将客户信息及其相关交易导入到数据库中,以供将来处理。验收标准:

Image批处理作业将预定义的客户/交易文件导入数据库表格。

Image文件导入后,将被删除。

Image客户/交易文件将有两种记录格式。首先是识别后续交易所属的客户。第二个是个人交易记录。

Image客户记录的格式是以下字段的逗号分隔记录:

| **名称** | **必需的** | **格式** | | :-- | :-- | :-- | | 客户税务 ID | 真实的 | \d{9} | | 客户名字 | 错误的 | \w+ | | 客户姓氏 | 错误的 | \w+ | | 客户地址 1 | 错误的 | \w+ | | 客户城市 | 错误的 | \w+ | | 客户状态 | 错误的 | [A-Z]{2} | | 客户邮政编码 | 错误的 | \d{4} | | 客户账号 | 错误的 | \d{16} |

Image客户记录将如下所示:

205866465,Joshua,Thompson,3708 Park,Fairview,LA,58517,3276793917668488

Image交易记录的格式为以下字段的逗号分隔记录:

| **名称** | **必需的** | **格式** | | | :-- | :-- | :-- | :-- | | 客户账号 | 真实的 | \d{16} | | | 股票代码 | 真实的 | \w+ | | | 真实数量 | | \d+ | | | 真实价格 | | \d+\。\d{2} | | | 交易时间戳 | True MM\DD\YYYY | 时:分:秒 |

事务记录如下所示:

3276793917668488,KSS,5767,7074247,2011-04-02 07:00:08

所有交易将作为新交易导入。

将创建一个包含无效客户记录的错误文件。

任何无效的交易记录将与客户记录一起写入错误文件

获取股票收盘价:A 在批处理过程中,在预定的执行时间,我将查询 Yahoo stock web 服务以获取我们的客户在上个月持有的所有股票的收盘价。我将用这些数据构建一个文件,以便将来导入。验收准则

该进程每次运行时都会输出一个文件。

文件将由每个股票代码的一个记录组成。

Image文件中的每条记录都有以下以逗号分隔的字段:

| **名称** | **必需的** | **格式** | | :-- | :-- | :-- | | 股票代码 | 真实的 | \w+ | | 收盘价格 | 真实的 | \d+\。\d{,2} |

Image股票报价文件将从 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).找到关于这项网络服务的更多信息

Image使用 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

*导入股票价格:*作为批处理,当我收到股票价格文件时,我会将该文件导入到数据库中以供将来处理。验收标准:

Image该过程将读取作业中上一步下载的文件。

每只股票的股价将储存在数据库中,供每笔交易参考。

Image文件成功导入后,将被删除。

Image文件的记录格式可以在 story Get 股票收盘价中找到。

任何格式错误的记录都将记录在单独的错误文件中,以供将来分析。

*计算定价等级:*作为批处理,在导入所有输入后,我将计算每个客户所处的定价等级,并将其存储起来以备将来使用。验收标准:

该流程将根据客户在一个月内的交易次数来计算每笔交易的价格。

Image每个等级将由以下阈值决定:

| **层** | **交易** | | :-- | :-- | | 我< = | Ten | | II <= | One hundred | | III <=1, | 000 | | 四> | Ten thousand |

Image将存储与客户相关的等级值,用于未来的费用计算。

*计算每笔交易的费用:*作为批处理,在我完成计算定价层级后,我将计算客户将被收取的每笔交易的经纪费。验收标准:

Image该流程将根据客户所在的层级计算每笔交易的费用(在计算定价层级故事中计算)。

Image计算每笔交易价格的公式如下:

| **梯队** | **公式** | | :-- | :-- | | 我 | 9 美元+购买额的 0.1% | | 二 3 美元 | | | 三 2 美元 | | | 四 1 美元 | |

*打印账户摘要:*作为批处理,所有计算完成后,我会为每个客户打印一份摘要。该摘要将提供客户账户的概述,以及构成其投资组合总价值的细分。验收标准:

该流程将为每个客户生成一个文件。

摘要将以一行文字开始,说明以下内容,并完全对齐

Your Account Summary                  Statement Period:<BEGIN_DATE> to <END_DATE>

Image其中,开始日期是上个月的第一个日历日期,结束日期是上个月的最后一个日历日期。

Image在摘要标题之后,每种证券类型(证券和现金)将有一个单独的行项目,以及该账户的当前价值。

Image在每一个明细项目之后,将打印一个总的账户值。以下是该部分的一个示例:

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

*打印账户明细:*作为批处理,在每个账户汇总后,我将打印每个账户的明细构成。账户详情将为客户提供其账户构成的详细信息,以及他们的投资情况。验收标准:

账户详情将被附加到每个客户的账户摘要中。

详细信息将以标题“账户详细信息”开始,左对齐。

Image在新的一行,将指定账户的现金余额。

在现金余额下方,将显示一个标题,说明“证券”,靠左对齐。

Image对于客户持有的每种股票,将显示以下字段:

| **姓名** | **必需的** | **格式** | | :-- | :-- | :-- | | 股票代码 | 真实的 | \w+ | | 真实数量 | | \d+ | | 真实价格 | | \d+\。\d{2} | | 总值 | 真实的 | 数量*美元格式的价格 |

下面是这部分的一个例子。

`Account Detail Cash                                                             $245,953.23

Securities         SHLD    100    71.98  71.98  7,198.00         CME     50     289.65  14482.50

Total Account Value    $276,633.73`

*打印报表表头:*作为批处理,我会在每页的顶部打印一个表头。这将提供关于账户、客户和经纪公司的一般信息。验收标准:

Image除了客户的地址和账号之外,标题都是静态文本。

Image下面是一个标题示例,其中 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 显示了该流程的批处理作业流程,以下章节描述了这些步骤。

images

***图 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 中提及它。

images

***图 3-4。*样本应用数据模型

对于批处理应用,您有四个表:Customer、Account、Transaction 和 Ticker。当您查看表中的数据时,请注意您没有存储生成语句所需的所有字段。有些字段(如帐户摘要中的总计)需要在处理过程中进行计算。除此之外,数据模型应该看起来相对简单:

  • *客户:*该记录包含所有特定于客户的信息,包括姓名和税务标识号。
  • 账号。每个客户都有一个账户。出于您的目的,每个帐户都有一个号码和一个流动现金余额,根据需要从中扣除费用。客户的交易费用层也存储在这一层。
  • 交易。每笔交易在交易表中都有相应的记录。这里的数据用于确定账户的当前状态(持有多少股份等等)。
  • 跑马灯。对于经纪公司的客户交易的每只股票,该表中都有一条记录,其中包含股票的报价机和最近的收盘价。

总结

本章讨论了敏捷开发过程以及如何将它应用到批量开发中。本章继续沿着这些思路,通过用户故事为你在本书的整个过程中构建的示例应用定义需求。从这一点上,这本书从《SpringBatch》的“是什么”和“为什么”切换到了“如何”

在下一章中,您将深入探究 Spring Batch 的作业和步骤概念,并查看许多其他特定的示例。

四、了解作业和步骤

在第二章中,你创建了自己的第一份工作。您完成了作业和步骤的配置,执行了作业,并配置了一个数据库来存储您的作业存储库。在那个“你好,世界!”例如,您开始对 Spring Batch 中的作业和步骤有所了解。本章继续深入探讨工作和步骤。您首先要学习什么是与 Spring Batch 框架相关的作业和步骤。

从那里,您可以深入了解执行作业或步骤时会发生什么,从加载它们并验证它们的有效性,一直到完成它们。然后,您钻研一些代码,查看您可以配置的作业和步骤的各个部分,并在此过程中学习最佳实践。最后,您将看到批处理难题的不同部分如何通过 Spring 批处理过程中涉及的各种作用域相互传递数据。

尽管您在本章中深入研究了各个步骤,但是一个步骤中最大的部分是它们的读取器和写入器,这里没有涉及。第七章和 9 探讨 Spring Batch 中可用的输入和输出功能。本章尽可能简化每个步骤的 I/O 方面,以便您可以专注于工作中复杂的步骤。

介绍工作

随着 web 应用的激增,您可能已经习惯了将应用分成请求和响应的想法。每个请求包含发生的单个独特处理的数据。请求的结果通常是返回给用户的某种视图。一个 web 应用可以由几十到几百个这样的独特交互组成,每个都以相同的方式构建,如图 4-1 所示。

images

***图 4-1。*网络应用的请求/响应处理

然而,当您考虑批处理作业时,您实际上是在谈论一组操作。术语*1是描述一份工作的好方法。再次使用 web 应用示例,考虑购物车应用的结帐过程是如何工作的。当您点击购物车中的商品结账时,您将经历一系列步骤:注册或登录、确认送货地址、输入账单信息、确认订单、提交订单。这个流程类似于什么是工作。*

*就本书而言,作业被定义为一个独特的、有序的步骤列表,可以从头到尾独立执行。让我们来分解一下这个定义,这样你就能更好地理解你在做什么:

  • *独特:*Spring Batch 中的作业是通过 XML 配置的,类似于使用核心 Spring 框架配置 beans 的方式,因此是可重用的。对于相同的配置,您可以根据需要多次执行作业。因此,没有理由多次定义同一个职务。
  • 有序步骤列表: 2 回到结账流程示例,步骤的顺序很重要。如果您一开始就没有注册,就无法验证您的送货地址。如果您的购物车是空的,您将无法执行结账流程。工作中的步骤顺序很重要。在客户的交易被导入到您的系统之前,您不能生成客户对帐单。在计算完所有费用之前,您无法计算帐户余额。您可以按照允许所有步骤按逻辑顺序执行的顺序来组织作业。
  • 可以从头到尾执行: 第一章将批处理定义为一个不需要与某种形式的完成进行额外交互就可以运行的过程。作业是一系列无需外部依赖即可执行的步骤。您不会构建一个作业,以便第三步是等待,直到文件被发送到一个目录进行处理。相反,当文件到达时,您有一个作业开始。
  • 独立:每个批处理作业应该能够在没有外部依赖影响的情况下执行。这并不意味着一个作业不能有依赖关系。相反,没有多少实际工作(除了“你好,世界”)是没有外部依赖性的。但是,该作业应该能够管理这些依赖关系。如果文件不存在,它会优雅地处理错误。它不等待文件被传递(那是调度程序的责任,等等)。一个作业可以处理它被定义要做的过程的所有元素。

作为比较,图 4-2 显示了批处理过程与图 4-1 中的 web 应用的执行情况。


对于那些熟悉 Spring Web Flow 框架的人来说,工作在结构上与 Web 应用中的流程非常相似。

虽然大多数作业由有序的步骤列表组成,但是 Spring Batch 确实支持并行执行步骤的能力。这个特性将在后面讨论。

images

***图 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

CommandLineJobRunnerJobRegistryBackgroundJobRunner(都位于org.springframework.batch.core.launch.support包中)是框架提供的两个作业运行器。你用第二章里的CommandLineJobRunner来运行“你好,世界!”job,你会在整本书中继续使用它。

虽然 job runner 是用来与 Spring Batch 接口的,但它不是框架的标准部分。没有JobRunner接口,因为每个场景需要不同的实现(尽管 Spring Batch 提供的两个作业运行器都使用 main 方法来启动)。相反,真正进入框架执行的是org.springframework.batch.core.launch.JobLauncher接口的实现。

Spring Batch 提供了单个JobLauncherorg.springframework.batch.core.launch.support.SimpleJobLauncher。这个类使用 Core Spring 的TaskExecutor接口来执行请求的作业。您可以看到这是如何配置的,但是需要注意的是,在 Spring 中有多种方式来配置org.springframework.core.task.TaskExecutor。如果使用了org.springframwork.core.task.SyncTaskExecutor,作业将在与JobLauncher相同的线程中执行。任何其他选项都在自己的线程中执行作业。

images

**图 4-3。**一个JobJobInstanceJobExecution的关系

运行批处理作业时,会创建一个org.springframework.batch.core.JobInstance。一个JobInstance表示作业的一次逻辑运行,由作业名和这次运行传递给作业的参数来标识。作业的运行不同于执行作业的尝试。如果您有一个预期每天运行的作业,那么您应该在 XML 中配置一次(定义蓝图)。每天您都会有一次新的运行或JobInstance,因为您向作业传递了一组新的参数(其中一个是日期)。当每个JobInstance尝试一次或JobExecution成功完成时,它将被视为完成。

images 注意一个JobInstance只能执行一次才能成功完成。因为JobInstance是由作业名和传入的参数标识的,这意味着您只能使用相同的参数运行一次作业。

您可能想知道 Spring Batch 如何从一次尝试到另一次尝试知道一个JobInstance的状态。在第二章的中,您查看了作业存储库,其中有一个batch_job_instance表。此表是所有其他表派生的基础。是batch_job_instancebatch_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,使批处理作业的配置更加简洁。

images 注意一个好的最佳实践是在以任务名称命名的 XML 文件中配置每个任务。

基本作业配置

清单 4-1 显示了一个基本 Spring 批处理作业的外壳。根据记录,这不是一个有效的工作。Spring Batch 中的作业需要至少有一个步骤或被声明为抽象。 3 在任何情况下,这里的重点都是工作而不是步骤,所以你要在本章后面的工作中添加步骤。

你在第二章的“你好,世界!”中使用了这种格式 job,对于以前使用过 Spring 的人来说应该很熟悉。就像 Spring 框架的大多数其他扩展一样,您可以像 Spring 的任何其他用途一样配置 beans,并拥有一个定义特定于域的标记的 XSD。在这种情况下,您在beans标签中包含 Spring Batch 的 XSD。

清单 4-1。??basicJob.xml

` <beans xmlns:batch="www.springframework.org/schema/batc…"

       xmlns:xsi="www.w3.org/2001/XMLSch…"        xsi:schemaLocation="www.springframework.org/schema/bean…            www.springframework.org/schema/bean…            www.springframework.org/schema/batc…            www.springframework.org/schema/batc…

    

    <batch:job id="basicJob">         ...     </batch:job> `

标签之后的第一个文件是 ?? 文件的导入,它位于项目的 ?? 目录中。你在第二章中使用了这个文件,但并没有真正深入进去,所以现在让我们来看看它。清单 4-2 显示了launch-context.xml。请注意,这个launch-context.xml是 zip 文件的一个显著精简版本。本书讨论了文件的其余部分,因为您将在以后的章节中使用它的各个部分。现在,让我们把重点放在让 Spring Batch 工作所需的部件上。

清单 4-2。??launch-context.xml

` <beans        xmlns:p="www.springframework.org/schema/p"        xmlns:xsi="www.w3.org/2001/XMLSch…"        xsi:schemaLocation="www.springframework.org/schema/bean…            www.springframework.org/schema/bean…

                                    

    <bean id="transactionManager"     class="org.springframework.jdbc.datasource.DataSourceTransactionManager"     lazy-init="true">              

    <bean id="placeholderProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigure”

                 <property name="systemPropertiesModeName"                 value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />                       

    

    <bean id="jobLauncher"     class="org.springframework.batch.core.launch.support.SimpleJobLauncher">               `


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.xmljobLauncher豆。如前一节所述,从执行的角度来看,作业启动器是进入 Spring Batch framework 的门户。它被配置为依赖于jobRepository

定义了通用组件后,让我们回到basicJob.xml。关于配置,90%的作业配置是步骤的有序定义,这将在本章后面介绍。关于basicJob请注意,您还没有配置任何对作业存储库或事务管理器的引用。这是因为默认情况下,Spring 使用名为jobRepositoryjobRepository和名为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使用。StringDoublejava.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 中显示的内容。

既然您已经知道了如何将参数放入批处理作业,那么一旦有了参数,如何访问它们呢?如果您快速查看一下ItemReaderItemProcessorItemWriterTasklet接口,您会很快注意到所有感兴趣的方法都没有将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有两个可选依赖:requiredKeysoptionalKeys。两者都是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.namebatch.runDate,您的作业将抛出一个异常。如果需要,可以传入更多的参数,但这两个参数不能为空。另一方面,如果使用optionalParamValidator,如果batch.namebatch.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 是如何使用两个参数执行您的作业的:一个名为nameString,值为迈克尔,另一个名为run.idlongrun.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接口。这个接口有两个方法的结果:beforeJobafterJob。每一个都将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 应用如何存储状态,通常是通过HttpSession6 ExecutionContext本质上是批处理作业的会话。除了简单的key-value对,ExecutionContext提供了一种安全存储作业状态的方法。web 应用的会话和ExecutionContext的一个区别是,在你的工作过程中,你实际上有多个ExecutionContextJobExecution有一个ExecutionContext,每个StepExecution也有一个ExecutionContext(你将在本章后面看到)。这允许在适当的级别确定数据的范围(特定于步骤的数据或整个作业的全局数据)。图 4-4 显示了这些元素之间的关系。

images

***图 4-4。*关系ExecutionContext年代

ExecutionContext提供了一种“安全”的方式来存储数据。存储是安全的,因为进入ExecutionContext的所有内容都保存在作业存储库中。你简要地看了一下第二章中的batch_job_execution_contextbatch_step_execution_context表,但是它们当时并没有包含任何有意义的数据。让我们看看如何向ExecutionContext添加数据和从中检索数据,以及这样做时它在数据库中是什么样子。

操纵执行上下文

如前所述,ExecutionContextJobExecutionStepExecution的一部分。正因为如此,要获得对ExecutionContext的控制,你需要根据你想要使用的JobExecutionStepExecution来获得它。清单 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*的内容

| **作业执行标识** | **SHORT_CONTEXT** | **序列化 _ 上下文** | | :-- | :-- | :-- | | `1` | `{"map":{"entry":{"string":["user.name", "Michael"]}}}` | `NULL` |

表 4-2 由三列组成。第一个是对与这个ExecutionContext相关的JobExecution的引用。第二个是JobExecutionContext的 JSON 表示。该字段随着处理的进行而更新。最后,SERIALIZED_CONTEXT字段包含一个序列化的 Java 对象。SERIALIZED_CONTEXT仅在作业运行或失败时被填充。

本章的这一节介绍了 Spring Batch 中作业的不同部分。然而,为了使一个作业有效,它至少需要一个步骤,这就把您带到了 Spring Batch 框架的下一个主要部分:步骤。

工作步骤

如果一个作业定义了整个过程,那么一个步骤就是一个作业的组成部分。这是一个独立的顺序批处理机。我称之为批处理机是有原因的。一个步骤包含一项工作所需的所有部分。它处理自己的输入。它有自己的处理器。它处理自己的输出。事务在一个步骤中是独立的。从设计上来说,这些步骤是不连贯的。这允许您作为开发人员根据需要自由地组织您的工作。

在本节中,您将采用与上一节中对 jobs 所做的相同的方式深入探讨各个步骤。您将了解 Spring Batch 如何一步一步地将处理分解,以及由于以前版本的框架,这种方式发生了怎样的变化。您还将看到许多关于如何配置工作中的步骤的示例,包括如何控制步骤之间的流程以及有条件的步骤执行。最后,配置语句作业所需的步骤。记住所有这些,让我们通过看步骤如何处理数据来开始看步骤。

组块与项目处理

批处理通常是关于处理数据的。当您考虑要处理的数据单元是什么时,有两种选择:单个项目或一大块项目。单个项由单个对象组成,该对象通常代表数据库或文件中的一行。因此,基于项目的处理是一次一行、一条记录或一个对象地读取、处理然后写入数据,如图 4-5 所示。

images

***图 4-5。*基于项目的处理

可以想象,这种方法会有很大的开销。当您知道将向数据库提交大量行或将它们写入文件时,写入单个行的低效率是巨大的。

当 Spring Batch 1.x 在 2008 年问世时,基于项目的处理是记录的处理方式。从那以后,SpringSource 和 Accenture 的人升级了这个框架,在 Spring Batch 2 中,他们引入了基于组块处理的概念。批处理世界中的是需要处理的记录或行的子集,通常由提交间隔定义。在 Spring Batch 中,当您处理一个数据块时,它是由每次提交之间处理的行数定义的。

图 4-6 显示了为块处理而设计的批处理过程中,数据是如何流动的。在这里,您可以看到,尽管每一行仍然是单独读取和处理的,但是在提交时,对单个块的所有写入都是同时发生的。这种处理上的小调整带来了巨大的性能提升,并为许多其他处理能力开辟了天地。

images

***图 4-6。*基于组块的处理

基于块的处理允许您做的事情之一是远程处理块。当你考虑到像网络开销这样的事情时,远程处理单个项目的成本太高了。但是,如果您可以一次将整个数据块发送到远程处理器,那么它不但不会降低性能,还会显著提高性能。

随着您在本书中对步骤、读取器、写入器和可伸缩性的了解越来越多,请记住 Spring Batch 所基于的基于块的处理。让我们继续深入研究如何配置您的工作的构建块:步骤。

步进配置

到目前为止,您已经认识到工作实际上只不过是要执行的有序步骤列表。因此,通过在作业中列出步骤来配置步骤。让我们来看看如何配置一个步骤以及您可以使用的各种选项。

基本步骤

当您考虑 Spring Batch 中的步骤时,有两种不同的类型:基于块的处理步骤和小任务步骤。虽然您在前面的“Hello,World!”约伯,稍后你会看到更多细节。现在,我们从如何配置基于块的步骤开始。

正如您前面看到的,块是由它们的提交间隔定义的。如果提交间隔设置为 50 项,那么您的作业读入 50 项,处理 50 项,然后一次写出 50 项。因此,事务管理器在基于块的步骤的配置中起着关键作用。清单 4-23 显示了如何为面向块的处理配置一个基本步骤。

清单 4-23。stepJob.xml

` <beans:beans     xmlns:beans="www.springframework.org/schema/bean…"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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>

                                           <chunk reader="inputReader" writer="outputWriter"                        commit-interval="50"/>                            </beans:beans>`

清单 4-23 可能看起来有些吓人,但让我们把注意力放在最后的工作和步骤配置上。文件的其余部分是一个基本的ItemReaderItemWriter的配置,分别在第七章和第九章中介绍。当你浏览清单 4-23 中的作业时,你会看到这个步骤是从step标签开始的。与任何其他 Spring Bean 一样,所需要的只是 id 或名称。在step标签内是一个tasklet标签。org.springframework.batch.core.step.tasklet.Tasklet界面实际上是针对您将要执行的步骤类型的策略界面。在这种情况下,您正在配置org.springframework.batch.core.step.item.ChunkOrientedTasklet<I>。您不必担心在这里专门配置类;请注意,也可以使用其他类型的微线程。示例步骤的最后一部分是chunk标记。在这里,您定义了您的步骤的块是什么。您说使用inputReaderbean(ItemReader接口的实现)作为读取器,使用outputWriterbean(ItemWriter接口的实现)作为写入器,一个块包含 50 个条目。

images 注意当你用 Spring 配置 beans 时,最好使用id属性而不是name属性。为了让 Spring 工作,它们都必须是唯一的,但是使用id属性允许 XML 验证器强制执行。

注意commit-interval属性很重要。在示例中设置为 50。这意味着在读取和处理 50 条记录之前,不会写入任何记录。如果在处理 49 个项目后出现错误,Spring Batch 将回滚当前块(事务)并将作业标记为失败。如果您将 commit-interval 值设置为 1,您的作业将读入一个项目,处理该项目,然后写入该项目。本质上,您将回到基于项目的处理。这样做的问题是,在提交时间间隔内,不仅仅只有一个项目被持久化。作业的状态也在作业存储库中更新。您将在本书的后面实验提交间隔,但是您现在需要知道将commit-interval设置得尽可能高是很重要的。

了解其他类型的微线程

尽管您的大多数步骤都是基于块的处理,因此使用了ChunkOrientedTasklet,但这不是唯一的选择。Spring Batch 提供了另外三个Tasklet接口的实现:CallableTaskletAdapterMethodInvokingTaskletAdapterSystemCommandTasklet。先来看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="www.springframework.org/schema/bean…"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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="www.springframework.org/schema/bean…"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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="www.springframework.org/schema/bean…"              xmlns:xsi=www.w3.org/2001/XMLSch…              xsi:schemaLocation="www.springframework.org/schema/bean…            www.springframework.org/schema/bean…            www.springframework.org/schema/batc…            www.springframework.org/schema/batc…

    <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.CONTINUABLERepeatStatus.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的步骤:carSteptruckStep。每个都使用已经在vehicleStep中配置的相同的读取器和写入器。在每种情况下,他们都添加了一个做不同事情的项目处理器。carStep选择使用继承的 50 个项目的提交间隔,而truckStep覆盖了提交间隔并将其设置为 5 个项目。

***清单 4-32。*在步骤继承中添加属性

`

`

通过声明一个抽象的步骤,就像在 Java 中一样,你可以省略掉原本需要的东西。在一个抽象步骤中,如清单 4-33 ,你可以省略readerwriterprocessortasklet属性。这通常会在 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的步骤中配置所需的元素。您可以在这里指定读取器和写入器。copyStepcommitIntervalStep具有相同的提交间隔 15,无需重复配置。

步骤继承允许您配置可以在各个步骤中重用的公共属性,并以可维护的方式构建 XML 配置。本节的最后一个例子使用了几个特定于块的属性。为了更好地理解它们,让我们回顾一下如何使用 Spring Batch 在基于块的处理中提供的不同特性。

块大小配置

因为基于块的处理是 Spring Batch 2 的基础,所以理解如何配置它的各种选项以充分利用这一重要特性非常重要。本节涵盖了配置块大小的两个选项:静态提交计数和CompletionPolicy实现。所有其他块配置选项都与错误处理相关,将在该部分中讨论。

开始看块配置,清单 4-34 有一个基本的例子,只不过是配置了一个读取器、写入器和提交间隔。读取器是ItemReader接口的实现,编写器是ItemWriter的实现。在本书后面的章节中,每个接口都有自己的专用章节,所以本节不详细介绍它们。所有你需要知道的是他们分别为这个步骤提供输入和输出。commit-interval 定义了一个块中有多少项(在本例中是 50 项)。

***清单 4-34。*一个基本的组块配置

` <beans:beans              xmlns:beans="www.springframework.org/schema/bean…"              xmlns:xsi="www.w3.org/2001/XMLSch…"              xsi:schemaLocation="www.springframework.org/schema/bean…              www.springframework.org/schema/bean…              www.springframework.org/schema/batc…            www.springframework.org/schema/batc…

    <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>

                          <chunk reader="inputReader" writer="outputWriter"                 commit-interval="50"/>              

          </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="www.springframework.org/schema/bean…"     xmlns:util="www.springframework.org/schema/util"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/util         www.springframework.org/schema/util…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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>

                          <chunk reader="inputReader" writer="outputWriter"                     chunk-completion-policy="chunkCompletionPolicy"/>              

                   </beans:beans>`

使用CompletionPolicy接口的实现并不是确定一个块有多大的唯一选择。也可以自己实现。在您查看实现之前,让我们先看一下接口。

CompletionPolicy接口需要四个方法:两个版本的isCompletestartupdate。如果从类的生命周期来看,首先是调用了start方法。该方法初始化策略,以便它知道块正在启动。重要的是要注意到,CompletionPolicy接口的实现是有状态的,并且应该能够通过它自己的内部状态来确定块是否已经完成。start方法在程序块开始时将这个内部状态重置为实现所需的状态。以SimpleCompletionPolicy为例,start方法在块开始时将内部计数器重置为 0。对于每一个已经被处理的条目,调用一次update方法来更新内部状态。回到SimpleCompletionPolicy的例子,update每增加一项,内部计数器就加 1。最后,还有两个isComplete方法。第一个isComplete方法签名接受一个RepeatContext作为它的参数。该实现旨在使用其内部状态来确定块是否已经完成。第二个签名将RepeatContextRepeatStatus作为参数。该实现被期望基于状态来确定块是否已经完成。清单 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.StepExecutionListenerorg.springframework.batch.core.ChunkListener接口,这两个接口分别允许在一个步骤和程序块的开始和结束时处理逻辑。注意,该步骤的侦听器被命名为StepExecutionListener,而不仅仅是StepListener。实际上有一个StepListener接口,不过它只是一个标记接口,所有与步骤相关的侦听器都会扩展它。

StepExecutionListenerChunkListener都提供了类似于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返回FAILEDExitStatus则转到step2b

清单 4-40。 If / Else逻辑步进执行

` <beans:beans     xmlns:beans="www.springframework.org/schema/bean…"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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.ExitStatusorg.springframework.batch.core.BatchStatusBatchStatusJobExecutionStepExecution的一个属性,用于识别作业或步骤的当前状态。ExitStatus是在作业或步骤结束时返回到 Spring Batch 的值。on属性评估ExitStatus的决策。因此,清单 4-40 中的例子相当于这样的 XML 语句:“如果step1的退出代码不等于FAILED,则转到step2a,否则转到step2b

因为ExitStatus的值实际上只是String的值,所以使用通配符的能力可以让事情变得有趣。Spring Batch 允许在on标准中使用两个通配符:

  • *匹配零个或多个字符。比如C*匹配 C完成正确
  • ?匹配单个字符。在这种情况下,?AT匹配,但与不匹配。

虽然评估ExitStatus让你开始决定下一步做什么,但它可能不会带你走完全程。例如,如果您跳过了当前步骤中的任何记录,而不想执行某个步骤,该怎么办?光从ExitStatus你是不会知道的。

images 注意 Spring Batch 在配置转换时会帮到你。它自动将转换从最严格到最不严格排序,并按此顺序应用它们。

Spring Batch 提供了一种确定下一步做什么的编程方式。您可以通过创建一个org.springframework.batch.core.job.flow.JobExecutionDecider接口的实现来做到这一点。这个接口有一个方法decide,它接受JobExecutionStepExecution并返回一个FlowExecutionStatus(一个BatchStatus / ExitStatus对的包装)。有了JobExecutionStepExecution可供评估,你就应该可以获得所有的信息,以便对你的工作下一步该做什么做出适当的决定。清单 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保持在StepExecutionJobExecution并持续在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="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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时,结果会有点不同。这一次,如果step1ExitStatus FAILURE结束,作业在jobRepository中被识别为失败,这允许它以相同的参数重新执行。

当以编程方式结束作业时,作业可以处于的最后一种状态是停止状态。在这种情况下,您可以重新启动作业;当您这样做时,它会在您配置的步骤重新启动。清单 4-45 显示了一个例子。

***清单 4-45。*在停止状态下结束作业

` <beans:beans     xmlns:beans="www.springframework.org/schema/bean…"     xmlns:xsi="www.w3.org/2001/XMLSch…"     xsi:schemaLocation="www.springframework.org/schema/bean…         www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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 `www.springframework.org/schema/bean…         www.springframework.org/schema/batc…         www.springframework.org/schema/batc…

    <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。相反,您定义一个类来从父作业的JobParametersExecutionContext(DefaultJobParameterExtractor检查两个地方)中提取参数,并将这些参数传递给子作业。您的提取器从job.stockFilejob.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 中,你识别出两个独立的流:一个加载两个文件,一个加载一个文件。这些流程中的每一个都是并行执行的。三个步骤(step1step2step3全部完成后,Spring Batch 执行batchStep

就这样。令人惊讶的是,使用 Spring Batch 进行基本并行化是如此简单,如本例所示。鉴于潜在的性能提升, 7 您可以开始明白为什么 Spring Batch 可以成为高性能批处理中非常有效的工具。

后面的章节涵盖了各种错误处理场景,包括从整个作业级别到事务级别的错误处理。但是因为步骤都是关于处理大块的项目,所以下一个主题是处理单个项目时可用的一些错误处理策略。

物品错误处理

Spring Batch 2 基于基于块的处理的概念。因为块是基于事务提交边界的,所以本书在第ItemReaderItemWriter章讨论了如何处理块的错误。然而,个别项目通常是错误的原因,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>

flowJobretryStep中,当步骤(itemReaderitemWriteritemProcessor)中的任何组件抛出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。*使用RetryTemplateRetryCallback

`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>

                     <chunk reader="itemReader" writer="retryItemWriter"                     processor="itemProcessor"                      commit-interval="20"/>               `

重试逻辑的最后一个方面是,像 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 批处理框架的结构组件。它们被用来规划一个过程。从这里开始,本书的大部分内容涵盖了所有不同的东西,这些东西被放入了这些作品的结构中。*