# 配置一个步骤

# 配置Step

XMLJavaBoth

正如领域章节中所讨论的,Step是一个域对象,它封装了批处理作业的一个独立的、连续的阶段,并包含定义和控制实际批处理所需的所有信息。这必然是一个模糊的描述,因为任何给定的Step的内容都是由编写Job的开发人员自行决定的。aStep可以是简单的,也可以是复杂的,正如开发人员所希望的那样。一个简单的Step可能会将文件中的数据加载到数据库中,只需要很少或不需要代码(取决于使用的实现)。更复杂的Step可能具有复杂的业务规则,作为处理的一部分,如下图所示:

Step

图 1.步骤

# 面向块的处理

Spring 批处理在其最常见的实现中使用了一种“面向块”的处理风格。面向块的处理指的是一次读取一个数据,并创建在事务边界内写出的“块”。一旦读取的项数等于提交间隔,ItemWriter就会写出整个块,然后提交事务。下图显示了这个过程:

面向块的处理

图 2.面向块的处理

下面的伪代码以简化的形式显示了相同的概念:

List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
    Object item = itemReader.read();
    if (item != null) {
        items.add(item);
    }
}
itemWriter.write(items);

面向块的步骤还可以配置一个可选的ItemProcessor来处理项,然后将它们传递给ItemWriter。下图显示了在步骤中注册ItemProcessor时的过程:

基于项目处理器的面向块处理

图 3.基于项目处理器的面向块处理

下面的伪代码展示了如何以简化的形式实现这一点:

List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
    Object item = itemReader.read();
    if (item != null) {
        items.add(item);
    }
}

List processedItems = new Arraylist();
for(Object item: items){
    Object processedItem = itemProcessor.process(item);
    if (processedItem != null) {
        processedItems.add(processedItem);
    }
}

itemWriter.write(processedItems);

有关项处理器及其用例的更多详细信息,请参阅项目处理部分。

# 配置Step

尽管Step所需依赖项的列表相对较短,但它是一个非常复杂的类,可能包含许多协作者。

为了简化配置,可以使用 Spring 批 XML 命名空间,如以下示例所示:

XML 配置

<job id="sampleJob" job-repository="jobRepository">
    <step id="step1">
        <tasklet transaction-manager="transactionManager">
            <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
        </tasklet>
    </step>
</job>

在使用 Java 配置时,可以使用 Spring 批处理构建器,如以下示例所示:

Java 配置

/**
 * Note the JobRepository is typically autowired in and not needed to be explicitly
 * configured
 */
@Bean
public Job sampleJob(JobRepository jobRepository, Step sampleStep) {
    return this.jobBuilderFactory.get("sampleJob")
    			.repository(jobRepository)
                .start(sampleStep)
                .build();
}

/**
 * Note the TransactionManager is typically autowired in and not needed to be explicitly
 * configured
 */
@Bean
public Step sampleStep(PlatformTransactionManager transactionManager) {
	return this.stepBuilderFactory.get("sampleStep")
				.transactionManager(transactionManager)
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				.build();
}

上面的配置包括创建面向项的步骤所需的唯一依赖项:

  • reader:提供处理项的ItemReader

  • writer:处理由ItemReader提供的项的ItemWriter

  • transaction-manager: Spring 的PlatformTransactionManager,在处理过程中开始并提交事务。

  • transactionManager: Spring 的PlatformTransactionManager,在处理过程中开始并提交事务。

  • job-repository:JobRepository的特定于 XML 的名称,该名称在处理过程中(就在提交之前)定期存储StepExecutionExecutionContext。对于内联<step/>(在<job/>中定义的),它是<job/>元素上的一个属性。对于独立的<step/>,它被定义为 <tasklet/>的属性。

  • repository:JobRepository的特定于 Java 的名称,该名称在处理过程中(就在提交之前)定期存储StepExecutionExecutionContext

  • commit-interval:在提交事务之前要处理的项数的 XML 特定名称。

  • chunk:依赖项的特定于 Java 的名称,该名称指示这是一个基于项的步骤,以及在提交事务之前要处理的项数。

需要注意的是,job-repository默认为jobRepositorytransaction-manager默认为transactionManager。而且,ItemProcessor是可选的,因为该项可以直接从阅读器传递给编写器。

需要注意的是,repository默认为jobRepositorytransactionManager默认为transactionManager(都是通过@EnableBatchProcessing中的基础设施提供的)。而且,ItemProcessor是可选的,因为该项可以直接从阅读器传递给编写器。

# 从父节点继承Step

如果一组Steps共享类似的配置,那么定义一个“父”Step可能是有帮助的,具体的Steps可以从中继承属性。与 Java 中的类继承类似,“child”Step将其元素和属性与父元素和属性结合在一起。子程序还重写父程序的任何Steps

在下面的示例中,Step,“concreteStep1”,继承自“parentstep”。它用“itemreader”、“itemprocessor”、“itemwriter”、startLimit=5allowStartIfComplete=true实例化。此外,commitInterval是“5”,因为它被“concreteStep1”Step覆盖,如以下示例所示:

<step id="parentStep">
    <tasklet allow-start-if-complete="true">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

<step id="concreteStep1" parent="parentStep">
    <tasklet start-limit="5">
        <chunk processor="itemProcessor" commit-interval="5"/>
    </tasklet>
</step>

在 Job 元素中的 Step 上仍然需要id属性。这有两个原因:

  • 在持久化StepExecution时,使用id作为步骤名。如果在作业中的多个步骤中引用了相同的独立步骤,则会发生错误。

  • 在创建工作流时,如本章后面所述,next属性应该指代工作流中的步骤,而不是独立的步骤。

# 摘要Step

有时,可能需要定义不是完整的Step配置的父Step。例如,如果readerwritertasklet属性在Step配置中被保留,则初始化失败。如果必须在没有这些属性的情况下定义父属性,那么应该使用abstract属性。abstract``Step只是扩展,不是实例化。

在下面的示例中,如果不声明Step``abstractParentStep为抽象,则不会对其进行实例化。Step、“ConcreteStep2”有“itemreader”、“itemwriter”和 commit-interval=10。

<step id="abstractParentStep" abstract="true">
    <tasklet>
        <chunk commit-interval="10"/>
    </tasklet>
</step>

<step id="concreteStep2" parent="abstractParentStep">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter"/>
    </tasklet>
</step>
# 合并列表

Steps上的一些可配置元素是列表,例如<listeners/>元素。如果父元素和子元素Steps都声明一个<listeners/>元素,那么子元素的列表将覆盖父元素的列表。为了允许子元素向父元素定义的列表中添加额外的侦听器,每个 List 元素都具有merge属性。如果元素指定merge="true",那么子元素的列表将与父元素的列表合并,而不是覆盖它。

在下面的示例中,使用两个侦听器创建Step“concreteStep3”:listenerOnelistenerTwo:

<step id="listenersParentStep" abstract="true">
    <listeners>
        <listener ref="listenerOne"/>
    <listeners>
</step>

<step id="concreteStep3" parent="listenersParentStep">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
    </tasklet>
    <listeners merge="true">
        <listener ref="listenerTwo"/>
    <listeners>
</step>

# 提交间隔

如前所述,一个步骤读入并写出项,并使用提供的PlatformTransactionManager定期提交。如果commit-interval为 1,则在写入每个单独的项后提交。在许多情况下,这是不理想的,因为开始和提交事务是昂贵的。理想情况下,最好是在每个事务中处理尽可能多的项,这完全取决于所处理的数据类型以及与该步骤交互的资源。因此,可以配置在提交中处理的项数。

下面的示例显示了一个step,其taskletcommit-interval值为 10,因为它将在 XML 中定义:

XML 配置

<job id="sampleJob">
    <step id="step1">
        <tasklet>
            <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
        </tasklet>
    </step>
</job>

下面的示例显示了一个step,其tasklet的值commit-interval为 10,这将在 Java 中定义:

Java 配置

@Bean
public Job sampleJob() {
    return this.jobBuilderFactory.get("sampleJob")
                     .start(step1())
                     .build();
}

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				.build();
}

在前面的示例中,在每个事务中处理 10 个项目。在处理的开始,事务就开始了。此外,每次在read上调用ItemReader时,计数器都会递增。当它达到 10 时,聚合项的列表被传递给ItemWriter,事务被提交。

# 配置用于重新启动的Step

在“配置和运行作业”小节中,讨论了重新启动Job。重启对步骤有很多影响,因此,可能需要一些特定的配置。

# 设置启动限制

在许多情况下,你可能希望控制Step可以启动的次数。例如,可能需要对特定的Step进行配置,使其仅运行一次,因为它会使一些必须手动修复的资源失效,然后才能再次运行。这是在步骤级别上可配置的,因为不同的步骤可能有不同的需求。可以只执行一次的Step可以作为同一Job的一部分存在,也可以作为可以无限运行的Step的一部分存在。

下面的代码片段展示了一个 XML 中的 Start Limit 配置示例:

XML 配置

<step id="step1">
    <tasklet start-limit="1">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

下面的代码片段展示了一个 Java 中的 Start Limit 配置示例:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				.startLimit(1)
				.build();
}

前面示例中所示的步骤只能运行一次。试图再次运行它将导致抛出StartLimitExceededException。请注意,start-limit 的默认值是Integer.MAX_VALUE

# 重新启动已完成的Step

在可重启作业的情况下,无论第一次是否成功,都可能有一个或多个应该始终运行的步骤。例如,验证步骤或Step在处理前清理资源。在对重新启动的作业进行正常处理期间,跳过状态为“已完成”的任何步骤,这意味着该步骤已成功完成。将allow-start-if-complete设置为“true”会重写此项,以便该步骤始终运行。

下面的代码片段展示了如何在 XML 中定义一个可重启作业:

XML 配置

<step id="step1">
    <tasklet allow-start-if-complete="true">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

下面的代码片段展示了如何在 Java 中定义一个可重启作业:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				.allowStartIfComplete(true)
				.build();
}
# Step重新启动配置示例

下面的 XML 示例展示了如何将作业配置为具有可以重新启动的步骤:

XML 配置

<job id="footballJob" restartable="true">
    <step id="playerload" next="gameLoad">
        <tasklet>
            <chunk reader="playerFileItemReader" writer="playerWriter"
                   commit-interval="10" />
        </tasklet>
    </step>
    <step id="gameLoad" next="playerSummarization">
        <tasklet allow-start-if-complete="true">
            <chunk reader="gameFileItemReader" writer="gameWriter"
                   commit-interval="10"/>
        </tasklet>
    </step>
    <step id="playerSummarization">
        <tasklet start-limit="2">
            <chunk reader="playerSummarizationSource" writer="summaryWriter"
                   commit-interval="10"/>
        </tasklet>
    </step>
</job>

下面的 Java 示例展示了如何将作业配置为具有可以重新启动的步骤:

Java 配置

@Bean
public Job footballJob() {
	return this.jobBuilderFactory.get("footballJob")
				.start(playerLoad())
				.next(gameLoad())
				.next(playerSummarization())
				.build();
}

@Bean
public Step playerLoad() {
	return this.stepBuilderFactory.get("playerLoad")
			.<String, String>chunk(10)
			.reader(playerFileItemReader())
			.writer(playerWriter())
			.build();
}

@Bean
public Step gameLoad() {
	return this.stepBuilderFactory.get("gameLoad")
			.allowStartIfComplete(true)
			.<String, String>chunk(10)
			.reader(gameFileItemReader())
			.writer(gameWriter())
			.build();
}

@Bean
public Step playerSummarization() {
	return this.stepBuilderFactory.get("playerSummarization")
			.startLimit(2)
			.<String, String>chunk(10)
			.reader(playerSummarizationSource())
			.writer(summaryWriter())
			.build();
}

前面的示例配置用于加载有关足球比赛的信息并对其进行总结的作业。它包含三个步骤:playerLoadgameLoadplayerSummarizationplayerLoad步骤从平面文件加载玩家信息,而gameLoad步骤对游戏也是如此。最后一步,playerSummarization,然后根据提供的游戏总结每个玩家的统计数据。假设playerLoad加载的文件必须只加载一次,但是gameLoad可以加载特定目录中的任何游戏,并在它们成功加载到数据库后将其删除。因此,playerLoad步骤不包含额外的配置。它可以启动任意次数,如果完成,则跳过。但是,每次都需要运行gameLoad步骤,以防自上次运行以来添加了额外的文件。它有“允许-启动-如果-完成”设置为“真”,以便始终被启动。(假设游戏加载到的数据库表上有一个进程指示器,以确保新的游戏可以通过摘要步骤正确地找到)。摘要步骤是作业中最重要的一步,它的起始限制为 2.这是有用的,因为如果该步骤持续失败,新的退出代码将返回给控制作业执行的操作符,并且在手动干预发生之前,它不能再次启动。

此作业为该文档提供了一个示例,它与示例项目中的footballJob不同。

本节的其余部分描述了footballJob示例的三次运行中的每一次运行的情况。

运行 1:

  1. playerLoad运行并成功完成,将 400 名玩家添加到“玩家”表中。

  2. gameLoad运行和处理 11 个游戏数据文件,并将其内容加载到“游戏”表中。

  3. playerSummarization开始处理,5 分钟后失败。

运行 2:

  1. playerLoad不运行,因为它已经成功地完成了,并且allow-start-if-complete是’false’(默认值)。

  2. gameLoad再次运行并处理另外 2 个文件,并将其内容加载到“Games”表中(进程指示器指示它们尚未被处理)。

  3. playerSummarization开始处理所有剩余的游戏数据(使用进程指示器进行过滤),并在 30 分钟后再次失败。

运行 3:

  1. playerLoad不运行,因为它已经成功地完成了,并且allow-start-if-complete是’false’(默认值)。

  2. gameLoad再次运行并处理另外 2 个文件,并将其内容加载到“Games”表中(进程指示器指示它们尚未被处理)。

  3. 由于这是playerSummarization的第三次执行,因此playerSummarization未启动并立即终止作业,并且其限制仅为 2.要么必须提高限制,要么必须执行Job作为新的JobInstance

# 配置跳过逻辑

在许多情况下,在处理过程中遇到的错误不会导致Step失败,而是应该跳过。这通常是一个必须由了解数据本身及其含义的人做出的决定。例如,财务数据可能不会被跳过,因为它会导致资金转移,而这需要完全准确。另一方面,加载供应商列表可能会允许跳过。如果某个供应商由于格式化不正确或缺少必要的信息而未加载,那么很可能就不存在问题。通常,这些不良记录也会被记录下来,稍后在讨论听众时会对此进行讨论。

下面的 XML 示例展示了使用跳过限制的示例:

XML 配置

<step id="step1">
   <tasklet>
      <chunk reader="flatFileItemReader" writer="itemWriter"
             commit-interval="10" skip-limit="10">
         <skippable-exception-classes>
            <include class="org.springframework.batch.item.file.FlatFileParseException"/>
         </skippable-exception-classes>
      </chunk>
   </tasklet>
</step>

下面的 Java 示例展示了一个使用跳过限制的示例:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(flatFileItemReader())
				.writer(itemWriter())
				.faultTolerant()
				.skipLimit(10)
				.skip(FlatFileParseException.class)
				.build();
}

在前面的示例中,使用了FlatFileItemReader。如果在任何时候抛出一个FlatFileParseException,则跳过该项并将其计入总跳过限制 10.声明的异常(及其子类)可能会在块处理的任何阶段(读、进程、写)抛出,但是在步骤执行中,读、进程和写的跳过是单独的计数,但是该限制适用于所有跳过。一旦达到跳过限制,发现的下一个异常将导致该步骤失败。换句话说,第 11 跳会触发异常,而不是第 10 跳会触发异常。

上述示例的一个问题是,除了FlatFileParseException之外的任何其他异常都会导致Job失败。在某些情况下,这可能是正确的行为。然而,在其他情况下,可能更容易确定哪些异常应该导致失败,并跳过其他所有情况。

下面的 XML 示例展示了一个排除特定异常的示例:

XML 配置

<step id="step1">
    <tasklet>
        <chunk reader="flatFileItemReader" writer="itemWriter"
               commit-interval="10" skip-limit="10">
            <skippable-exception-classes>
                <include class="java.lang.Exception"/>
                <exclude class="java.io.FileNotFoundException"/>
            </skippable-exception-classes>
        </chunk>
    </tasklet>
</step>

下面的 Java 示例展示了一个排除特定异常的示例:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(flatFileItemReader())
				.writer(itemWriter())
				.faultTolerant()
				.skipLimit(10)
				.skip(Exception.class)
				.noSkip(FileNotFoundException.class)
				.build();
}

通过将java.lang.Exception标识为可跳过的异常类,配置指示所有Exceptions都是可跳过的。但是,通过“排除”java.io.FileNotFoundException,该配置将可跳过的异常类的列表细化为所有Exceptions除了FileNotFoundException。任何被排除的异常类如果遇到都是致命的(也就是说,它们不会被跳过)。

对于遇到的任何异常,可跳跃性由类层次结构中最近的超类决定。任何未分类的例外情况都被视为“致命的”。

<include/><exclude/>元素的顺序并不重要。

skipnoSkip方法调用的顺序并不重要。

# 配置重试逻辑

在大多数情况下,你希望异常导致跳过或Step失败。然而,并非所有的例外都是确定性的。如果在读取时遇到FlatFileParseException,则总是为该记录抛出该记录。重置ItemReader不会有帮助。但是,对于其他异常,例如DeadlockLoserDataAccessException,它表示当前进程试图更新另一个进程持有锁定的记录。等待并再次尝试可能会取得成功。

在 XML 中,重试应该配置如下:

<step id="step1">
   <tasklet>
      <chunk reader="itemReader" writer="itemWriter"
             commit-interval="2" retry-limit="3">
         <retryable-exception-classes>
            <include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
         </retryable-exception-classes>
      </chunk>
   </tasklet>
</step>

在 Java 中,重试应该配置如下:

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.faultTolerant()
				.retryLimit(3)
				.retry(DeadlockLoserDataAccessException.class)
				.build();
}

Step允许对单个项目的重试次数进行限制,并提供“可重试”的异常列表。有关重试工作原理的更多详细信息,请参见retry

# 控制回滚

默认情况下,不管是重试还是跳过,从ItemWriter抛出的任何异常都会导致由Step控制的事务回滚。如果按照前面描述的方式配置了 Skip,则从ItemReader抛出的异常不会导致回滚。但是,在许多情况下,从ItemWriter抛出的异常不应该导致回滚,因为没有发生任何使事务无效的操作。出于这个原因,Step可以配置一个不应导致回滚的异常列表。

在 XML 中,你可以按以下方式控制回滚:

XML 配置

<step id="step1">
   <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
      <no-rollback-exception-classes>
         <include class="org.springframework.batch.item.validator.ValidationException"/>
      </no-rollback-exception-classes>
   </tasklet>
</step>

在 Java 中,你可以按以下方式控制回滚:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.faultTolerant()
				.noRollback(ValidationException.class)
				.build();
}
# 事务读取器

ItemReader的基本契约是,它只是远期的。该步骤缓冲读写器的输入,以便在回滚的情况下,不需要从读写器重新读取项目。然而,在某些情况下,读取器是建立在事务性资源之上的,例如 JMS 队列。在这种情况下,由于队列与回滚的事务绑定在一起,因此从队列中拉出的消息将被放回。出于这个原因,可以将该步骤配置为不缓冲项。

下面的示例展示了如何创建不使用 XML 缓冲项的读取器:

XML 配置

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="2"
               is-reader-transactional-queue="true"/>
    </tasklet>
</step>

下面的示例展示了如何创建不在 Java 中缓冲项的读取器:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.readerIsTransactionalQueue()
				.build();
}

# 事务属性

事务属性可用于控制isolationpropagationtimeout设置。有关设置事务属性的更多信息,请参见Spring core documentation (opens new window)

以下示例在 XML 中设置isolationpropagationtimeout事务属性:

XML 配置

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
        <transaction-attributes isolation="DEFAULT"
                                propagation="REQUIRED"
                                timeout="30"/>
    </tasklet>
</step>

下面的示例在 Java 中设置isolationpropagationtimeout事务属性:

Java 配置

@Bean
public Step step1() {
	DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
	attribute.setPropagationBehavior(Propagation.REQUIRED.value());
	attribute.setIsolationLevel(Isolation.DEFAULT.value());
	attribute.setTimeout(30);

	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(itemWriter())
				.transactionAttribute(attribute)
				.build();
}

#Step注册ItemStream

该步骤必须在其生命周期中的必要点处理ItemStream回调(有关ItemStream接口的更多信息,请参见ItemStream)。如果一个步骤失败并且可能需要重新启动,这是至关重要的,因为ItemStream接口是该步骤获取所需的关于两次执行之间的持久状态的信息的地方。

如果ItemReaderItemProcessorItemWriter本身实现了ItemStream接口,那么这些接口将被自动注册。任何其他流都需要单独注册。在将委托等间接依赖注入到 Reader 和 Writer 中时,通常会出现这种情况。可以通过“流”元素在step上注册流。

下面的示例显示了如何在 XML 中的step上注册stream:

XML 配置

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="compositeWriter" commit-interval="2">
            <streams>
                <stream ref="fileItemWriter1"/>
                <stream ref="fileItemWriter2"/>
            </streams>
        </chunk>
    </tasklet>
</step>

<beans:bean id="compositeWriter"
            class="org.springframework.batch.item.support.CompositeItemWriter">
    <beans:property name="delegates">
        <beans:list>
            <beans:ref bean="fileItemWriter1" />
            <beans:ref bean="fileItemWriter2" />
        </beans:list>
    </beans:property>
</beans:bean>

下面的示例展示了如何在 Java 中的step上注册stream:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(2)
				.reader(itemReader())
				.writer(compositeItemWriter())
				.stream(fileItemWriter1())
				.stream(fileItemWriter2())
				.build();
}

/**
 * In Spring Batch 4, the CompositeItemWriter implements ItemStream so this isn't
 * necessary, but used for an example.
 */
@Bean
public CompositeItemWriter compositeItemWriter() {
	List<ItemWriter> writers = new ArrayList<>(2);
	writers.add(fileItemWriter1());
	writers.add(fileItemWriter2());

	CompositeItemWriter itemWriter = new CompositeItemWriter();

	itemWriter.setDelegates(writers);

	return itemWriter;
}

在上面的示例中,CompositeItemWriter不是ItemStream,但它的两个委托都是。因此,为了使框架能够正确地处理这两个委托编写器,必须将这两个委托编写器显式地注册为流。ItemReader不需要显式地注册为流,因为它是Step的直接属性。该步骤现在可以重新启动,并且在发生故障时,Reader 和 Writer 的状态被正确地持久化。

# 拦截Step执行

就像Job一样,在执行Step的过程中有许多事件,其中用户可能需要执行某些功能。例如,为了写出到需要页脚的平面文件,需要在ItemWriter已完成时通知Step,以便可以写出页脚。这可以通过使用许多Step范围的侦听器中的一个来实现。

实现StepListener扩展之一的任何类(但不包括接口本身,因为它是空的)都可以通过listeners元素应用到一个步骤。listeners元素在步骤、任务 let 或块声明中是有效的。建议你在其函数应用的级别上声明侦听器,或者,如果它是多功能的(例如StepExecutionListenerItemReadListener),则在其应用的最细粒度级别上声明它。

下面的示例展示了一个应用于 XML 块级别的侦听器:

XML 配置

<step id="step1">
    <tasklet>
        <chunk reader="reader" writer="writer" commit-interval="10"/>
        <listeners>
            <listener ref="chunkListener"/>
        </listeners>
    </tasklet>
</step>

下面的示例展示了在 Java 中应用于块级别的侦听器:

Java 配置

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(reader())
				.writer(writer())
				.listener(chunkListener())
				.build();
}

一个ItemReaderItemWriterItemProcessor本身实现了StepListener接口之一的Step如果使用名称空间<step>元素或*StepFactoryBean工厂之一,则自动在Step中注册。这仅适用于直接注入Step的组件。如果侦听器嵌套在另一个组件中,则需要显式地对其进行注册(如前面在[用Step注册ItemStream](#registeringitemStreams)中所述)。

除了StepListener接口外,还提供了注释来解决相同的问题。普通的旧 Java 对象可以具有带有这些注释的方法,然后将这些方法转换为相应的StepListener类型。对组块组件的定制实现进行注释也是常见的,例如ItemReaderItemWriterTasklet。XML 解析器分析<listener/>元素的注释,并在构建器中使用listener方法注册注释,因此你所需要做的就是使用 XML 名称空间或构建器通过一个步骤注册侦听器。

# StepExecutionListener

StepExecutionListener表示用于Step执行的最通用的侦听器。它允许在Step开始之前和结束之后发出通知,无论是正常结束还是失败,如下例所示:

public interface StepExecutionListener extends StepListener {

    void beforeStep(StepExecution stepExecution);

    ExitStatus afterStep(StepExecution stepExecution);

}

ExitStatusafterStep的返回类型,以便使侦听器有机会修改在完成Step时返回的退出代码。

与此接口对应的注释是:

  • @BeforeStep

  • @AfterStep

# ChunkListener

块被定义为在事务范围内处理的项。在每个提交间隔时间提交一个事务,提交一个“块”。aChunkListener可用于在块开始处理之前或在块成功完成之后执行逻辑,如以下接口定义所示:

public interface ChunkListener extends StepListener {

    void beforeChunk(ChunkContext context);
    void afterChunk(ChunkContext context);
    void afterChunkError(ChunkContext context);

}

在事务启动后但在ItemReader上调用读之前调用 BeforeChunk 方法。相反,afterChunk是在提交了块之后调用的(如果有回滚,则根本不调用)。

与此接口对应的注释是:

  • @BeforeChunk

  • @AfterChunk

  • @AfterChunkError

当没有块声明时,可以应用ChunkListenerTaskletStep负责调用ChunkListener,因此它也适用于非面向项目的任务小程序(在任务小程序之前和之后调用它)。

# ItemReadListener

在前面讨论跳过逻辑时,提到了记录跳过的记录可能是有益的,这样可以在以后处理它们。在读取错误的情况下,可以使用ItemReaderListener完成此操作,如下面的接口定义所示:

public interface ItemReadListener<T> extends StepListener {

    void beforeRead();
    void afterRead(T item);
    void onReadError(Exception ex);

}

在每次调用之前调用beforeRead方法来读取ItemReader。在每次成功调用 read 之后,都会调用afterRead方法,并传递被读取的项。如果在读取时出现错误,则调用onReadError方法。提供了所遇到的异常,以便可以对其进行记录。

与此接口对应的注释是:

  • @BeforeRead

  • @AfterRead

  • @OnReadError

# ItemProcessListener

就像ItemReadListener一样,可以“监听”项目的处理,如以下接口定义所示:

public interface ItemProcessListener<T, S> extends StepListener {

    void beforeProcess(T item);
    void afterProcess(T item, S result);
    void onProcessError(T item, Exception e);

}

ItemProcessor上,在process之前调用beforeProcess方法,并将其交给要处理的项。在成功处理该项后,将调用afterProcess方法。如果在处理过程中出现错误,则调用onProcessError方法。提供了遇到的异常和试图处理的项,以便可以对它们进行记录。

与此接口对应的注释是:

  • @BeforeProcess

  • @AfterProcess

  • @OnProcessError

# ItemWriteListener

可以使用ItemWriteListener“监听”项目的写入,如以下接口定义所示:

public interface ItemWriteListener<S> extends StepListener {

    void beforeWrite(List<? extends S> items);
    void afterWrite(List<? extends S> items);
    void onWriteError(Exception exception, List<? extends S> items);

}

ItemWriter上的write之前调用beforeWrite方法,并将所写的项列表交给该方法。在成功地写入项目之后,将调用afterWrite方法。如果写入时出现错误,则调用onWriteError方法。提供了遇到的异常和试图写入的项,以便可以对它们进行记录。

与此接口对应的注释是:

  • @BeforeWrite

  • @AfterWrite

  • @OnWriteError

# SkipListener

ItemReadListenerItemProcessListenerItemWriteListener都提供了通知错误的机制,但没有一个通知你记录实际上已被跳过。例如,onWriteError即使一个项目被重试并成功,也会被调用。出于这个原因,有一个单独的接口用于跟踪跳过的项目,如以下接口定义所示:

public interface SkipListener<T,S> extends StepListener {

    void onSkipInRead(Throwable t);
    void onSkipInProcess(T item, Throwable t);
    void onSkipInWrite(S item, Throwable t);

}

onSkipInRead是在读取时跳过项时调用的。需要注意的是,回滚可能会导致同一项被多次注册为跳过一次。onSkipInWrite是在写入时跳过一项时调用的。因为该项已被成功读取(而不是跳过),所以还将该项本身作为参数提供给它。

与此接口对应的注释是:

  • @OnSkipInRead

  • @OnSkipInWrite

  • @OnSkipInProcess

# 跳过侦听器和事务

SkipListener最常见的用例之一是注销一个跳过的项,这样就可以使用另一个批处理过程甚至人工过程来评估和修复导致跳过的问题。因为在许多情况下原始事务可能会被回滚, Spring Batch 提供了两个保证:

  1. 每个项目只调用一次适当的 Skip 方法(取决于错误发生的时间)。

  2. 总是在事务提交之前调用SkipListener。这是为了确保侦听器调用的任何事务资源不会因ItemWriter中的故障而回滚。

# TaskletStep

面向块的处理并不是在Step中进行处理的唯一方法。如果Step必须包含一个简单的存储过程调用怎么办?你可以将调用实现为ItemReader,并在过程完成后返回 null。然而,这样做有点不自然,因为需要有一个 no-opItemWriter。 Spring Batch 为此场景提供了TaskletStep

Tasklet是一个简单的接口,它有一个方法execute,它被TaskletStep反复调用,直到它返回RepeatStatus.FINISHED或抛出异常来表示失败。对Tasklet的每个调用都包装在一个事务中。Tasklet实现器可以调用一个存储过程、一个脚本或一个简单的 SQL 更新语句。

要在 XML 中创建TaskletStep<tasklet/>元素的’ref’属性应该引用定义Tasklet对象的 Bean。在<tasklet/>中不应该使用<chunk/>元素。下面的示例展示了一个简单的任务:

<step id="step1">
    <tasklet ref="myTasklet"/>
</step>

要在 Java 中创建TaskletStep,传递给构建器的tasklet方法的 Bean 应该实现Tasklet接口。在构建TaskletStep时,不应调用chunk。下面的示例展示了一个简单的任务:

@Bean
public Step step1() {
    return this.stepBuilderFactory.get("step1")
    			.tasklet(myTasklet())
    			.build();
}
TaskletStep如果实现StepListener接口,则自动将
任务集注册为StepListener

# TaskletAdapter

ItemReaderItemWriter接口的其他适配器一样,Tasklet接口包含一个允许自适应到任何预先存在的类的实现:TaskletAdapter。这可能有用的一个例子是现有的 DAO,该 DAO 用于更新一组记录上的标志。TaskletAdapter可以用来调用这个类,而不必为Tasklet接口编写适配器。

下面的示例展示了如何在 XML 中定义TaskletAdapter:

XML 配置

<bean id="myTasklet" class="o.s.b.core.step.tasklet.MethodInvokingTaskletAdapter">
    <property name="targetObject">
        <bean class="org.mycompany.FooDao"/>
    </property>
    <property name="targetMethod" value="updateFoo" />
</bean>

下面的示例展示了如何在 Java 中定义TaskletAdapter:

Java 配置

@Bean
public MethodInvokingTaskletAdapter myTasklet() {
	MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();

	adapter.setTargetObject(fooDao());
	adapter.setTargetMethod("updateFoo");

	return adapter;
}

# 示例Tasklet实现

许多批处理作业包含一些步骤,这些步骤必须在主处理开始之前完成,以便设置各种资源,或者在处理完成之后清理这些资源。如果作业中的文件很多,那么在成功地将某些文件上传到另一个位置后,通常需要在本地删除这些文件。下面的示例(取自Spring Batch samples project (opens new window))是一个带有这样的职责的Tasklet实现:

public class FileDeletingTasklet implements Tasklet, InitializingBean {

    private Resource directory;

    public RepeatStatus execute(StepContribution contribution,
                                ChunkContext chunkContext) throws Exception {
        File dir = directory.getFile();
        Assert.state(dir.isDirectory());

        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
            boolean deleted = files[i].delete();
            if (!deleted) {
                throw new UnexpectedJobExecutionException("Could not delete file " +
                                                          files[i].getPath());
            }
        }
        return RepeatStatus.FINISHED;
    }

    public void setDirectoryResource(Resource directory) {
        this.directory = directory;
    }

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(directory, "directory must be set");
    }
}

前面的tasklet实现将删除给定目录中的所有文件。需要注意的是,execute方法只被调用一次。剩下的就是引用来自steptasklet

下面的示例展示了如何在 XML 中引用来自steptasklet:

XML 配置

<job id="taskletJob">
    <step id="deleteFilesInDir">
       <tasklet ref="fileDeletingTasklet"/>
    </step>
</job>

<beans:bean id="fileDeletingTasklet"
            class="org.springframework.batch.sample.tasklet.FileDeletingTasklet">
    <beans:property name="directoryResource">
        <beans:bean id="directory"
                    class="org.springframework.core.io.FileSystemResource">
            <beans:constructor-arg value="target/test-outputs/test-dir" />
        </beans:bean>
    </beans:property>
</beans:bean>

下面的示例展示了如何在 Java 中引用来自steptasklet:

Java 配置

@Bean
public Job taskletJob() {
	return this.jobBuilderFactory.get("taskletJob")
				.start(deleteFilesInDir())
				.build();
}

@Bean
public Step deleteFilesInDir() {
	return this.stepBuilderFactory.get("deleteFilesInDir")
				.tasklet(fileDeletingTasklet())
				.build();
}

@Bean
public FileDeletingTasklet fileDeletingTasklet() {
	FileDeletingTasklet tasklet = new FileDeletingTasklet();

	tasklet.setDirectoryResource(new FileSystemResource("target/test-outputs/test-dir"));

	return tasklet;
}

# 控制阶跃流

在拥有一份工作的过程中,有了将步骤组合在一起的能力,就需要能够控制工作如何从一个步骤“流动”到另一个步骤。aStep失败并不一定意味着Job应该失败。此外,可能有不止一种类型的“成功”来决定下一步应该执行哪个Step。根据Steps组的配置方式,某些步骤甚至可能根本不会被处理。

# 序贯流

最简单的流程场景是所有步骤都按顺序执行的作业,如下图所示:

顺序流动

图 4.顺序流动

这可以通过使用step中的“next”来实现。

下面的示例展示了如何在 XML 中使用next属性:

XML 配置

<job id="job">
    <step id="stepA" parent="s1" next="stepB" />
    <step id="stepB" parent="s2" next="stepC"/>
    <step id="stepC" parent="s3" />
</job>

下面的示例展示了如何在 Java 中使用next()方法:

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(stepA())
				.next(stepB())
				.next(stepC())
				.build();
}

在上面的场景中,“步骤 A”首先运行,因为它是列出的第一个Step。如果“步骤 A”正常完成,那么“步骤 B”运行,依此类推。但是,如果“步骤 A”失败,则整个Job失败,并且“步骤 B”不执行。

对于 Spring 批处理 XML 命名空间,配置中列出的第一步是 alwaysJob运行的第一步。其他步骤元素的顺序并不是
重要的,但是第一步必须始终首先出现在 XML 中。

# 条件流

在上面的例子中,只有两种可能性:

  1. step成功,应该执行下一个step

  2. step失败,因此,job应该失败。

在许多情况下,这可能就足够了。但是,如果step的失败应该触发不同的step,而不是导致失败,那么在这种情况下该怎么办?下图显示了这样的流程:

条件流

图 5.条件流

为了处理更复杂的场景, Spring 批 XML 命名空间允许在 Step 元素中定义转换元素。一个这样的转换是next元素。与next属性类似,next元素告诉Job下一个执行的是Step。然而,与属性不同的是,在给定的Step上允许任意数量的next元素,并且在失败的情况下没有默认行为。这意味着,如果使用了转换元素,则必须显式地定义Step转换的所有行为。还请注意,单个步骤不能同时具有next属性和transition元素。

next元素指定要匹配的模式和接下来要执行的步骤,如以下示例所示:

XML 配置

<job id="job">
    <step id="stepA" parent="s1">
        <next on="*" to="stepB" />
        <next on="FAILED" to="stepC" />
    </step>
    <step id="stepB" parent="s2" next="stepC" />
    <step id="stepC" parent="s3" />
</job>

Java API 提供了一组流畅的方法,允许你指定流程以及当步骤失败时要做什么。下面的示例显示了如何指定一个步骤(stepA),然后继续执行两个不同步骤中的任何一个(stepBstepC),这取决于stepA是否成功:

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(stepA())
				.on("*").to(stepB())
				.from(stepA()).on("FAILED").to(stepC())
				.end()
				.build();
}

当使用 XML 配置时,转换元素的on属性使用一个简单的模式匹配方案来匹配执行ExitStatus所产生的ExitStatus

当使用 Java 配置时,on()方法使用一个简单的模式匹配方案来匹配执行ExitStatus所产生的ExitStatus

模式中只允许使用两个特殊字符:

  • “*”匹配零个或多个字符

  • “?”正好与一个字符匹配。

例如,“C*t”匹配“cat”和“count”,而“C?t”匹配“cat”,但不匹配“count”。

虽然对Step上的转换元素的数量没有限制,但是如果Step执行导致元素不覆盖的ExitStatus,那么框架将抛出一个异常,而Job将失败。该框架自动命令从最特定到最不特定的转换。这意味着,即使在上面的示例中将顺序交换为“stepa”,ExitStatus的“failed”仍将转到“stepc”。

# 批处理状态与退出状态

在为条件流配置Job时,重要的是要理解BatchStatusExitStatus之间的区别。BatchStatus是一个枚举,它是JobExecutionStepExecution的属性,框架使用它来记录JobStep的状态。它可以是以下值之一:COMPLETEDSTARTINGSTARTEDSTOPPINGSTOPPEDFAILEDABANDONED,或UNKNOWN。其中大多数是不言自明的:COMPLETED是当一个步骤或作业成功完成时设置的状态,FAILED是当它失败时设置的状态,依此类推。

使用 XML 配置时,下面的示例包含“next”元素:

<next on="FAILED" to="stepB" />

使用 Java 配置时,下面的示例包含“on”元素:

...
.from(stepA()).on("FAILED").to(stepB())
...

乍一看,“on”似乎引用了它所属的StepBatchStatus。然而,它实际上引用了ExitStatusStep。顾名思义,ExitStatus表示一个Step在完成执行后的状态。

更具体地说,当使用 XML 配置时,前面的 XML 配置示例中显示的“next”元素引用ExitStatus的退出代码。

当使用 Java 配置时,前面的 Java 配置示例中显示的“on()”方法引用ExitStatus的退出代码。

在英语中,它写着:“如果退出代码是FAILED,则转到 STEPB。”默认情况下,对于Step,退出代码始终与BatchStatus相同,这就是上面的条目有效的原因。但是,如果退出代码需要不同,该怎么办?一个很好的例子来自于 Samples 项目中的 Skip Sample 作业:

下面的示例展示了如何使用 XML 中的不同退出代码:

XML 配置

<step id="step1" parent="s1">
    <end on="FAILED" />
    <next on="COMPLETED WITH SKIPS" to="errorPrint1" />
    <next on="*" to="step2" />
</step>

下面的示例展示了如何使用 Java 中的不同退出代码:

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1()).on("FAILED").end()
			.from(step1()).on("COMPLETED WITH SKIPS").to(errorPrint1())
			.from(step1()).on("*").to(step2())
			.end()
			.build();
}

step1有三种可能性:

  1. Step失败,在这种情况下,作业应该失败。

  2. Step成功完成。

  3. Step已成功完成,但退出代码为“与跳过一起完成”。在这种情况下,应该运行一个不同的步骤来处理错误。

前面的配置可以正常工作。但是,需要根据跳过记录的执行情况更改退出代码,如以下示例所示:

public class SkipCheckingListener extends StepExecutionListenerSupport {
    public ExitStatus afterStep(StepExecution stepExecution) {
        String exitCode = stepExecution.getExitStatus().getExitCode();
        if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) &&
              stepExecution.getSkipCount() > 0) {
            return new ExitStatus("COMPLETED WITH SKIPS");
        }
        else {
            return null;
        }
    }
}

上面的代码是StepExecutionListener,该代码首先检查以确保Step成功,然后检查StepExecution上的跳过计数是否高于 0. 如果这两个条件都满足,则返回一个新的ExitStatus,其退出代码为COMPLETED WITH SKIPS

# 配置停止

在讨论了batchstatus 和 exitstatus之后,人们可能想知道如何确定BatchStatusExitStatusJob。虽然这些状态是由执行的代码为Step确定的,但Job的状态是基于配置确定的。

到目前为止,讨论的所有作业配置都至少有一个没有转换的最终Step

在下面的 XML 示例中,在step执行之后,Job结束:

<step id="stepC" parent="s3"/>

在下面的 Java 示例中,在step执行之后,Job结束:

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(step1())
				.build();
}

如果没有为Step定义转换,则Job的状态定义如下:

  • 如果StepExitStatus结尾失败,则BatchStatusExitStatus中的Job都是FAILED

  • 否则,BatchStatusExitStatusJob都是COMPLETED

虽然这种终止批处理作业的方法对于某些批处理作业(例如简单的连续步骤作业)来说已经足够了,但可能需要自定义的作业停止场景。为此, Spring Batch 提供了三个转换元素来停止Job(除了我们前面讨论的[next元素](#NextElement))。这些停止元素中的每一个都以特定的BatchStatus停止Job。重要的是要注意,停止转换元件对BatchStatus中的任何StepsExitStatusExitStatus都没有影响。这些元素只影响Job的最终状态。例如,对于作业中的每一步,都可能具有FAILED的状态,但是对于作业,则可能具有COMPLETED的状态。

# 以一步结尾

配置步骤结束指示JobBatchStatusCOMPLETED停止。已经完成了 statusCOMPLETEDJob不能重新启动(框架抛出一个JobInstanceAlreadyCompleteException)。

在使用 XML 配置时,此任务使用“end”元素。end元素还允许一个可选的’exit-code’属性,该属性可用于自定义JobExitStatus。如果没有给出“exit-code”属性,则ExitStatus默认为COMPLETED,以匹配BatchStatus

当使用 Java 配置时,此任务使用“end”方法。end方法还允许一个可选的’exitstatus’参数,该参数可用于自定义Job中的ExitStatus。如果不提供“exitstatus”值,则ExitStatus默认为COMPLETED,以匹配BatchStatus

考虑以下场景:如果step2失败,则Job停止,BatchStatusCOMPLETEDExitStatusCOMPLETEDstep3不运行。否则,执行移动到step3。请注意,如果step2失败,则Job不可重启(因为状态是COMPLETED)。

下面的示例以 XML 形式展示了该场景:

<step id="step1" parent="s1" next="step2">

<step id="step2" parent="s2">
    <end on="FAILED"/>
    <next on="*" to="step3"/>
</step>

<step id="step3" parent="s3">

下面的示例展示了 Java 中的场景:

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(step1())
				.next(step2())
				.on("FAILED").end()
				.from(step2()).on("*").to(step3())
				.end()
				.build();
}
# 失败的步骤

配置在给定点失败的步骤指示JobBatchStatusFAILED停止。与 END 不同的是,Job的失败并不会阻止Job被重新启动。

在使用 XML 配置时,“fail”元素还允许一个可选的“exit-code”属性,该属性可用于自定义Job中的ExitStatus。如果没有给出“exit-code”属性,则ExitStatus默认为FAILED,以匹配BatchStatus

考虑下面的场景,如果step2失败,则Job停止,BatchStatusFAILEDExitStatusEARLY TERMINATIONstep3不执行。否则,执行移动到step3。此外,如果step2失败,并且Job被重新启动,那么在step2上再次开始执行。

下面的示例以 XML 形式展示了该场景:

XML 配置

<step id="step1" parent="s1" next="step2">

<step id="step2" parent="s2">
    <fail on="FAILED" exit-code="EARLY TERMINATION"/>
    <next on="*" to="step3"/>
</step>

<step id="step3" parent="s3">

下面的示例展示了 Java 中的场景:

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1())
			.next(step2()).on("FAILED").fail()
			.from(step2()).on("*").to(step3())
			.end()
			.build();
}
# 在给定的步骤停止作业

将作业配置为在特定的步骤停止,将指示Job使用BatchStatusSTOPPED停止作业。停止Job可以在处理中提供临时中断,以便操作员可以在重新启动Job之前采取一些操作。

在使用 XML 配置时,“stop”元素需要一个“restart”属性,该属性指定了重新启动作业时执行应该在哪里进行的步骤。

当使用 Java 配置时,stopAndRestart方法需要一个“restart”属性,该属性指定作业重新启动时执行应该在哪里进行的步骤。

考虑以下场景:如果step1COMPLETE结束,那么作业将停止。一旦重新启动,执行就开始于step2

以下清单以 XML 形式展示了该场景:

<step id="step1" parent="s1">
    <stop on="COMPLETED" restart="step2"/>
</step>

<step id="step2" parent="s2"/>

下面的示例展示了 Java 中的场景:

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1()).on("COMPLETED").stopAndRestart(step2())
			.end()
			.build();
}

# 程序化流程决策

在某些情况下,可能需要比ExitStatus更多的信息来决定下一步执行哪个步骤。在这种情况下,可以使用JobExecutionDecider来辅助决策,如以下示例所示:

public class MyDecider implements JobExecutionDecider {
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        String status;
        if (someCondition()) {
            status = "FAILED";
        }
        else {
            status = "COMPLETED";
        }
        return new FlowExecutionStatus(status);
    }
}

在下面的示例作业配置中,decision指定了要使用的决策器以及所有转换:

XML 配置

<job id="job">
    <step id="step1" parent="s1" next="decision" />

    <decision id="decision" decider="decider">
        <next on="FAILED" to="step2" />
        <next on="COMPLETED" to="step3" />
    </decision>

    <step id="step2" parent="s2" next="step3"/>
    <step id="step3" parent="s3" />
</job>

<beans:bean id="decider" class="com.MyDecider"/>

在下面的示例中,在使用 Java 配置时,实现JobExecutionDecider的 Bean 被直接传递到next调用。

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1())
			.next(decider()).on("FAILED").to(step2())
			.from(decider()).on("COMPLETED").to(step3())
			.end()
			.build();
}

# 拆分流

到目前为止描述的每个场景都涉及一个Job,它以线性方式一次执行一个步骤。 Spring 除了这种典型的样式之外,批处理还允许使用并行的流来配置作业。

XML 命名空间允许你使用“split”元素。正如下面的示例所示,“split”元素包含一个或多个“flow”元素,可以在其中定义整个单独的流。“拆分”元素还可以包含前面讨论过的任何转换元素,例如“next”属性或“next”、“end”或“fail”元素。

<split id="split1" next="step4">
    <flow>
        <step id="step1" parent="s1" next="step2"/>
        <step id="step2" parent="s2"/>
    </flow>
    <flow>
        <step id="step3" parent="s3"/>
    </flow>
</split>
<step id="step4" parent="s4"/>

基于 Java 的配置允许你通过提供的构建器配置分割。正如下面的示例所示,“split”元素包含一个或多个“flow”元素,可以在其中定义整个单独的流。“拆分”元素还可以包含前面讨论过的任何转换元素,例如“next”属性或“next”、“end”或“fail”元素。

@Bean
public Flow flow1() {
	return new FlowBuilder<SimpleFlow>("flow1")
			.start(step1())
			.next(step2())
			.build();
}

@Bean
public Flow flow2() {
	return new FlowBuilder<SimpleFlow>("flow2")
			.start(step3())
			.build();
}

@Bean
public Job job(Flow flow1, Flow flow2) {
	return this.jobBuilderFactory.get("job")
				.start(flow1)
				.split(new SimpleAsyncTaskExecutor())
				.add(flow2)
				.next(step4())
				.end()
				.build();
}

# 外部化作业之间的流定义和依赖关系

作业中的部分流可以作为单独的 Bean 定义外部化,然后重新使用。有两种方法可以做到这一点。第一种方法是简单地将流声明为对别处定义的流的引用。

下面的示例展示了如何将流声明为对 XML 中其他地方定义的流的引用:

XML 配置

<job id="job">
    <flow id="job1.flow1" parent="flow1" next="step3"/>
    <step id="step3" parent="s3"/>
</job>

<flow id="flow1">
    <step id="step1" parent="s1" next="step2"/>
    <step id="step2" parent="s2"/>
</flow>

下面的示例展示了如何将流声明为对 Java 中其他地方定义的流的引用:

Java 配置

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(flow1())
				.next(step3())
				.end()
				.build();
}

@Bean
public Flow flow1() {
	return new FlowBuilder<SimpleFlow>("flow1")
			.start(step1())
			.next(step2())
			.build();
}

如前面的示例所示,定义外部流的效果是将外部流中的步骤插入到作业中,就好像这些步骤是内联声明的一样。通过这种方式,许多作业可以引用相同的模板流,并将这样的模板组合成不同的逻辑流。这也是分离单个流的集成测试的一种好方法。

外部化流程的另一种形式是使用JobStep。aJobStep类似于 aFlowStep,但实际上是为指定的流程中的步骤创建并启动一个单独的作业执行。

下面的示例是 XML 中JobStep的示例:

XML 配置

<job id="jobStepJob" restartable="true">
   <step id="jobStepJob.step1">
      <job ref="job" job-launcher="jobLauncher"
          job-parameters-extractor="jobParametersExtractor"/>
   </step>
</job>

<job id="job" restartable="true">...</job>

<bean id="jobParametersExtractor" class="org.spr...DefaultJobParametersExtractor">
   <property name="keys" value="input.file"/>
</bean>

下面的示例显示了 Java 中JobStep的示例:

Java 配置

@Bean
public Job jobStepJob() {
	return this.jobBuilderFactory.get("jobStepJob")
				.start(jobStepJobStep1(null))
				.build();
}

@Bean
public Step jobStepJobStep1(JobLauncher jobLauncher) {
	return this.stepBuilderFactory.get("jobStepJobStep1")
				.job(job())
				.launcher(jobLauncher)
				.parametersExtractor(jobParametersExtractor())
				.build();
}

@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(step1())
				.build();
}

@Bean
public DefaultJobParametersExtractor jobParametersExtractor() {
	DefaultJobParametersExtractor extractor = new DefaultJobParametersExtractor();

	extractor.setKeys(new String[]{"input.file"});

	return extractor;
}

作业参数提取器是一种策略,它确定如何将StepExecutionContext转换为正在运行的JobParametersJobParameters。当你希望有一些更细粒度的选项来监视和报告作业和步骤时,JobStep非常有用。使用JobStep通常也是对这个问题的一个很好的回答:“我如何在工作之间创建依赖关系?”这是一种很好的方法,可以将一个大型系统分解成更小的模块,并控制工作流程。

# JobStep属性的后期绑定

前面显示的 XML 和平面文件示例都使用 Spring Resource抽象来获取文件。这是因为Resource有一个getFile方法,它返回一个java.io.File。XML 和平面文件资源都可以使用标准的 Spring 构造进行配置:

下面的示例展示了 XML 中的后期绑定:

XML 配置

<bean id="flatFileItemReader"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource"
              value="file://outputs/file.txt" />
</bean>

下面的示例展示了 Java 中的后期绑定:

Java 配置

@Bean
public FlatFileItemReader flatFileItemReader() {
	FlatFileItemReader<Foo> reader = new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource("file://outputs/file.txt"))
			...
}

前面的Resource从指定的文件系统位置加载文件。请注意,绝对位置必须以双斜杠(//)开始。在大多数 Spring 应用程序中,这种解决方案已经足够好了,因为这些资源的名称在编译时是已知的。然而,在批处理场景中,可能需要在运行时确定文件名作为作业的参数。这可以通过使用“-D”参数读取系统属性来解决。

下面的示例展示了如何从 XML 中的属性读取文件名:

XML 配置

<bean id="flatFileItemReader"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="${input.file.name}" />
</bean>

下面展示了如何从 Java 中的属性读取文件名:

Java 配置

@Bean
public FlatFileItemReader flatFileItemReader(@Value("${input.file.name}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

要使这个解决方案起作用,所需要的只是一个系统参数(例如-Dinput.file.name="file://outputs/file.txt")。

虽然在这里可以使用PropertyPlaceholderConfigurer,但是如果系统属性始终设置,则不需要
,因为 Spring ResourceEditor中的
已经对系统属性进行了筛选和占位符替换。

通常,在批处理设置中,最好是在作业的JobParameters中参数化文件名,而不是通过系统属性,并以这种方式访问它们。为了实现这一点, Spring 批处理允许各种JobStep属性的后期绑定。

下面的示例展示了如何用 XML 参数化一个文件名:

XML 配置

<bean id="flatFileItemReader" scope="step"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobParameters['input.file.name']}" />
</bean>

下面的示例展示了如何在 Java 中参数化一个文件名:

Java 配置

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['input.file.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

JobExecutionStepExecution级别ExecutionContext都可以以相同的方式访问。

下面的示例展示了如何访问 XML 中的ExecutionContext:

XML 配置

<bean id="flatFileItemReader" scope="step"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobExecutionContext['input.file.name']}" />
</bean>

XML 配置

<bean id="flatFileItemReader" scope="step"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{stepExecutionContext['input.file.name']}" />
</bean>

下面的示例展示了如何在 Java 中访问ExecutionContext:

Java 配置

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.file.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

Java 配置

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{stepExecutionContext['input.file.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}
任何使用 late-binding 的 Bean 都必须用 scope=“step”声明。有关更多信息,请参见Step Scope。应该注意的是
aStep Bean 不应该是步骤作用域。如果在
定义的步骤中需要进行后期绑定,则该步骤的组件(即 tasklet、Item Reader/Writer 等)
是应该被限定范围的组件。
如果你正在使用 Spring 3.0(或更高版本),则步骤作用域 bean 中的表达式使用
Spring 表达式语言,这是一种功能强大的通用语言,具有许多有趣的
特性。为了提供向后兼容性,如果 Spring 批检测到
Spring 的旧版本的存在,则它使用一种功能不那么强大的原生表达式语言和具有略有不同的解析规则的
。主要的区别在于,在
上面的示例中的 MAP 键不需要引用 Spring 2.5,但是在 Spring 3.0 中的引用是强制性的

# 步骤作用域

前面显示的所有延迟绑定示例都在 Bean 定义中声明了“步骤”的范围。

下面的示例展示了在 XML 中绑定到 STEP 作用域的示例:

XML 配置

<bean id="flatFileItemReader" scope="step"
      class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobParameters[input.file.name]}" />
</bean>

下面的示例展示了在 Java 中绑定到 STEP 作用域的示例:

Java 配置

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input.file.name]}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

使用 late binding 需要使用Step的作用域,因为在Step开始之前,实际上不能实例化 Bean,以允许找到属性。因为默认情况下它不是 Spring 容器的一部分,所以必须通过使用batch名称空间,或者通过显式地为StepScope包含一个 Bean 定义,或者通过使用@EnableBatchProcessing注释,显式地添加作用域。只使用其中一种方法。下面的示例使用batch名称空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:batch="http://www.springframework.org/schema/batch"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="...">
<batch:job .../>
...
</beans>

下面的示例明确地包括 Bean 定义:

<bean class="org.springframework.batch.core.scope.StepScope" />

# 工作范围

在 Spring 批 3.0 中引入的Job作用域在配置中类似于Step作用域,但它是Job上下文的作用域,因此每个运行的作业只有一个这样的 Bean 实例。此外,还支持使用#{..}占位符从JobContext访问的引用的后期绑定。使用此特性, Bean 可以从作业或作业执行上下文和作业参数中提取属性。

下面的示例展示了在 XML 中绑定到作业范围的示例:

XML 配置

<bean id="..." class="..." scope="job">
    <property name="name" value="#{jobParameters[input]}" />
</bean>

XML 配置

<bean id="..." class="..." scope="job">
    <property name="name" value="#{jobExecutionContext['input.name']}.txt" />
</bean>

下面的示例展示了在 Java 中绑定到作业范围的示例:

Java 配置

@JobScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input]}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

Java 配置

@JobScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}

因为默认情况下它不是 Spring 容器的一部分,所以必须通过使用batch命名空间,通过显式地为 JobScope 包括一个 Bean 定义,或者使用@EnableBatchProcessing注释(但不是所有的),显式地添加范围。下面的示例使用batch名称空间:

<beans xmlns="http://www.springframework.org/schema/beans"
		  xmlns:batch="http://www.springframework.org/schema/batch"
		  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		  xsi:schemaLocation="...">

<batch:job .../>
...
</beans>

下面的示例包括显式定义JobScope的 Bean:

<bean class="org.springframework.batch.core.scope.JobScope" />
在多线程
或分区步骤中使用作业范围的 bean 有一些实际的限制。 Spring 批处理不控制在这些
用例中产生的线程,因此不可能正确地设置它们以使用这样的 bean。因此,
不建议在多线程或分区步骤中使用作业范围的 bean。