当前版本仍在开发中,尚不被视为稳定版本。最新稳定版请使用 Spring Batch 文档 6.0.2!

控制 Step 流程

当你能够在一个 job 中把多个 step 组织起来之后,就自然需要去控制这个 job 如何从一个 step “流转”到下一个 step。某个 Step 失败,并不一定意味着整个 Job 就必须失败。 此外,“成功”也未必只有一种含义,不同的成功结果可能决定下一步应执行不同的 Step。 根据一组 Step 的配置方式不同,某些 step 甚至可能完全不会被执行。

流程定义中的 Step Bean 方法代理

在一个流程定义中,step 实例必须是唯一的。当一个 step 在流程定义中存在多个走向时, 务必确保传递给流程定义方法(如 startfrom 等)的始终是同一个 step 实例。 否则,流程执行的行为可能会出现意外。

在下面的示例中,step 都是以参数形式注入到 flow 或 job 的 bean 定义方法中的。 这种依赖注入风格可以保证流程定义中的 step 唯一性。 但如果你是通过直接调用带有 @Bean 注解的 step 定义方法来定义流程, 那么在禁用 bean 方法代理的情况下(例如 @Configuration(proxyBeanMethods = false)), 这些 step 可能就不再唯一。如果你偏好这种 bean 间直接调用的写法,那么就必须启用 bean 方法代理。

关于 Spring Framework 中 bean 方法代理的更多细节,可参见 Using the @Configuration annotation 一节。

顺序流程

最简单的流程场景,就是一个 job 中所有 step 按顺序依次执行,如下图所示:

顺序流程
图 1. 顺序流程

这可以通过在 step 中使用 next 来实现。

  • Java

  • XML

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

Java Configuration
@Bean
public Job job(JobRepository jobRepository, Step stepA, Step stepB, Step stepC) {
	return new JobBuilder("job", jobRepository)
				.start(stepA)
				.next(stepB)
				.next(stepC)
				.build();
}

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

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

在上述场景中,stepA 会最先运行,因为它是列出的第一个 Step。 如果 stepA 正常完成,则继续运行 stepB,以此类推。 但如果 stepA 失败,那么整个 Job 就会失败,stepB 也不会执行。

在 Spring Batch 的 XML 命名空间中,配置里列出的第一个 step 总是Job 最先执行的 step。其余 step 元素的顺序无关紧要, 但第一个 step 必须始终排在 XML 的最前面。

条件流程

在前面的示例中,只有两种可能:

  1. step 成功,然后执行下一个 step

  2. step 失败,因此整个 job 也应失败。

在很多场景下,这已经够用了。但如果某个 step 失败后, 你希望触发另一个不同的 step,而不是让作业直接失败,又该怎么办? 下图展示了这样一种流程:

条件流程
图 2. 条件流程
  • Java

  • XML

Java API 提供了一组链式方法,用来定义流程以及某个 step 失败时应采取的处理方式。下面的示例展示了如何先执行一个 step(stepA),然后根据 stepA 是否成功,继续进入两个不同的 step(stepBstepC)之一:

Java Configuration
@Bean
public Job job(JobRepository jobRepository, Step stepA, Step stepB, Step stepC) {
	return new JobBuilder("job", jobRepository)
				.start(stepA)
				.on("*").to(stepB)
				.from(stepA).on("FAILED").to(stepC)
				.end()
				.build();
}

为了处理更复杂的场景,Spring Batch 的 XML 命名空间允许你在 step 元素内部定义转换元素。其中一种转换就是 next 元素。和 next 属性一样,next 元素用于告诉 Job 下一步要执行哪个 Step。但与属性不同的是,一个给定的 Step 上可以声明任意多个 next 元素,而且在失败场景下没有默认行为。这意味着,一旦使用了转换元素,就必须把该 Step 的所有流转行为都显式定义出来。还要注意,单个 step 不能同时声明 next 属性和 transition 元素。

next 元素用来指定匹配模式,以及匹配成功后要执行的下一个 step,如下例所示:

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

  • XML

使用 Java 配置时,on() 方法会通过一种简单的模式匹配机制,去匹配该 Step 执行完成后产生的 ExitStatus

使用 XML 配置时,转换元素上的 on 属性同样采用简单的模式匹配机制,用来匹配该 Step 执行后产生的 ExitStatus

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

  • * 匹配零个或多个字符

  • ? 恰好匹配一个字符

例如,c*t 可以匹配 catcount,而 c?t 可以匹配 cat,但不能匹配 count

虽然一个 Step 上声明的转换元素数量没有上限,但如果该 Step 执行后得到的 ExitStatus 没有被任何元素覆盖, 框架就会抛出异常,导致 Job 失败。框架会自动按“从最具体到最宽泛”的顺序排序这些转换。因此,即便把前面示例中 stepA 的声明顺序调换,FAILED 这个 ExitStatus 仍然会进入 stepC

BatchStatusExitStatus

Job 配置条件流转时,理解 BatchStatusExitStatus 的区别非常重要。BatchStatus 是一个枚举, 同时作为 JobExecutionStepExecution 的属性存在,框架用它来记录 JobStep 的状态。它可能的取值包括: COMPLETEDSTARTINGSTARTEDSTOPPINGSTOPPEDFAILEDABANDONEDUNKNOWN。这些值大都顾名思义:比如 COMPLETED 表示 step 或 job 已成功完成,FAILED 表示执行失败,等等。

  • Java

  • XML

下面的示例展示了在 Java 配置中使用 on 的写法:

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

下面的示例展示了在 XML 配置中使用 next 元素的写法:

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

乍看之下,on 似乎引用的是所属 StepBatchStatus。但实际上,它引用的是该 StepExitStatus。顾名思义,ExitStatus 表示的是 Step 执行结束之后的状态。

  • Java

  • XML

使用 Java 配置时,前面示例中的 on() 方法匹配的是 ExitStatus 的退出码。

更具体地说,在 XML 配置中,前面示例里的 next 元素匹配的也是 ExitStatus 的退出码。

用自然语言来表达,就是:“如果退出码是 FAILED,就转到 stepB”。默认情况下,退出码总是与该 StepBatchStatus 保持一致,所以前面的配置能够正常工作。但如果退出码需要与默认值不同,该怎么办?samples 项目中的 skip 示例 job 就是一个很好的例子:

  • Java

  • XML

下面的示例展示了在 Java 中如何处理自定义退出码:

Java Configuration
@Bean
public Job job(JobRepository jobRepository, Step step1, Step step2, Step errorPrint1) {
	return new JobBuilder("job", jobRepository)
			.start(step1).on("FAILED").end()
			.from(step1).on("COMPLETED WITH SKIPS").to(errorPrint1)
			.from(step1).on("*").to(step2)
			.end()
			.build();
}

下面的示例展示了在 XML 中如何处理自定义退出码:

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

step1 有三种可能结果:

  • Step 失败,此时整个 job 也应该失败。

  • Step 成功完成。

  • Step 成功完成,但退出码是 COMPLETED WITH SKIPS。此时应执行另一个 step 来处理错误。

上述配置本身没有问题。不过,还需要有某种机制根据执行过程中是否发生记录跳过来修改退出码,如下例所示:

public class SkipCheckingListener implements StepExecutionListener {
    @Override
    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 的 skip 计数是否大于 0。如果两个条件都满足,就返回一个新的 ExitStatus,其退出码为 COMPLETED WITH SKIPS

停止配置

在理解了 BatchStatusExitStatus 之后,一个自然的问题是: JobBatchStatusExitStatus 是如何确定的?对于 Step 来说,这些状态由实际执行的代码决定; 而对于 Job,它们则由配置来决定。

到目前为止,前面讨论过的所有 job 配置中,至少都会有一个不再向后流转的最终 Step

  • Java

  • XML

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

@Bean
public Job job(JobRepository jobRepository, Step step1) {
	return new JobBuilder("job", jobRepository)
				.start(step1)
				.build();
}

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

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

如果一个 Step 没有定义任何转换,那么 Job 的状态按以下规则确定:

  • 如果该 StepFAILEDExitStatus 结束,那么 JobBatchStatusExitStatus 都是 FAILED

  • 否则,JobBatchStatusExitStatus 都是 COMPLETED

这种结束批处理 job 的方式,对于某些场景已经足够,例如简单的顺序 step job。但在一些情况下,你可能需要自定义 job 的停止方式。 为此,Spring Batch 除了前面提到的 next 元素 之外,还提供了三个用于停止 Job 的转换元素。 这些停止元素会以特定的 BatchStatus 结束一个 Job。需要特别注意的是,这些停止转换元素不会影响 Job 中任何 StepBatchStatusExitStatus,它们只会影响 Job 的最终状态。比如,完全可能出现这样一种情况: job 中每个 step 的状态都是 FAILED,但整个 job 的状态却是 COMPLETED

在某个 Step 处结束

把某个 step 配置为“结束点”,表示让 JobCOMPLETEDBatchStatus 停止。一个状态已是 COMPLETEDJob 不能再重启,框架会抛出 JobInstanceAlreadyCompleteException

  • Java

  • XML

使用 Java 配置时,可以通过 end 方法实现这一点。end 方法还允许传入可选的 exitStatus 参数,用来 自定义 JobExitStatus。如果没有提供 exitStatus,则默认使用 COMPLETED,与 BatchStatus 保持一致。

使用 XML 配置时,可以通过 end 元素实现这一点。end 元素同样支持可选的 exit-code 属性,用来 自定义 JobExitStatus。如果没有指定 exit-code,默认值就是 COMPLETED,与 BatchStatus 保持一致。

看下面这个场景:如果 step2 失败,Job 会以 COMPLETEDBatchStatusCOMPLETEDExitStatus 结束,同时不会执行 step3。否则,流程会继续进入 step3。需要注意的是,如果 step2 失败,整个 Job 也不能重启,因为它的状态已经是 COMPLETED

  • Java

  • XML

下面的示例展示了对应的 Java 配置:

@Bean
public Job job(JobRepository jobRepository, Step step1, Step step2, Step step3) {
	return new JobBuilder("job", jobRepository)
				.start(step1)
				.next(step2)
				.on("FAILED").end()
				.from(step2).on("*").to(step3)
				.end()
				.build();
}

下面的示例展示了对应的 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">

在某个 Step 处失败结束

把某个 step 配置为在特定位置失败结束,表示让 JobFAILEDBatchStatus 停止。与 end 不同, Job 失败并不会阻止它后续被重新启动。

使用 XML 配置时,fail 元素同样支持可选的 exit-code 属性,用于自定义 JobExitStatus。 如果没有指定 exit-code,默认值就是 FAILED,与 BatchStatus 保持一致。

看下面这个场景:如果 step2 失败,Job 会以 FAILEDBatchStatusEARLY TERMINATIONExitStatus 停止,同时不会执行 step3。否则,流程继续进入 step3。另外,如果 step2 失败之后重启该 Job,执行会从 step2 重新开始。

  • Java

  • XML

下面的示例展示了对应的 Java 配置:

Java Configuration
@Bean
public Job job(JobRepository jobRepository, Step step1, Step step2, Step step3) {
	return new JobBuilder("job", jobRepository)
			.start(step1)
			.next(step2).on("FAILED").fail()
			.from(step2).on("*").to(step3)
			.end()
			.build();
}

下面的示例展示了对应的 XML 配置:

XML Configuration
<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">

在指定 Step 处停止 Job

把一个 job 配置为在特定 step 处停止,表示让该 JobSTOPPEDBatchStatus 结束。 停止 Job 可以为处理流程提供一个临时中断点,以便操作人员在重启 Job 之前执行某些操作。

  • Java

  • XML

使用 Java 配置时,stopAndRestart 方法需要指定一个重启目标 step,用来定义 Job 重启后应从哪里继续执行。

使用 XML 配置时,stop 元素必须提供 restart 属性,用来指定 Job 重启后从哪个 step 继续执行。

看下面这个场景:如果 step1COMPLETE 结束,job 就会停止;一旦重启,执行将从 step2 开始。

  • Java

  • XML

下面的示例展示了对应的 Java 配置:

@Bean
public Job job(JobRepository jobRepository, Step step1, Step step2) {
	return new JobBuilder("job", jobRepository)
			.start(step1).on("COMPLETED").stopAndRestart(step2)
			.end()
			.build();
}

下面的示例展示了对应的 XML 配置:

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

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

以编程方式决定流程走向

在某些场景下,仅凭 ExitStatus 还不足以决定下一步要执行哪个 step。这时可以使用 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);
    }
}
  • Java

  • XML

下面的示例展示了在 Java 配置中,如何把一个实现了 JobExecutionDecider 的 bean 直接传给 next 调用:

Java Configuration
@Bean
public Job job(JobRepository jobRepository, MyDecider decider, Step step1, Step step2, Step step3) {
	return new JobBuilder("job", jobRepository)
			.start(step1)
			.next(decider).on("FAILED").to(step2)
			.from(decider).on("COMPLETED").to(step3)
			.end()
			.build();
}

下面的示例 job 配置中,decision 用来指定要使用的 decider 以及所有相关的转换:

XML Configuration
<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"/>

拆分流程

到目前为止,前面描述的所有场景都属于 Job 以线性方式逐个执行 step。除了这种常见形式之外,Spring Batch 还支持把一个 job 配置为并行流程。

  • Java

  • XML

基于 Java 的配置可以通过框架提供的 builder 来声明拆分流程。如下例所示,split 元素中可以包含一个或多个 flow 元素,用来定义完整的独立流程。split 元素也可以包含前面讨论过的各种转换元素,例如 next 属性, 以及 nextendfail 等元素。

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

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

@Bean
public Job job(JobRepository jobRepository, Flow flow1, Flow flow2, Step step4) {
	return new JobBuilder("job", jobRepository)
				.start(flow1)
				.split(new SimpleAsyncTaskExecutor())
				.add(flow2)
				.next(step4)
				.end()
				.build();
}

XML 命名空间同样支持使用 split 元素。如下例所示,split 元素中可以包含一个或多个 flow 元素, 用于定义完整的独立流程。split 元素也可以包含前面讨论过的各种转换元素,例如 next 属性,或 nextendfail 等元素。

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

外部化流程定义与 Job 之间的依赖

job 中的部分流程可以被外部化为独立的 bean 定义,然后在多个地方复用。实现这一点有两种方式。第一种方式是把该流程声明为对外部已定义 flow 的引用。

  • Java

  • XML

下面的 Java 示例展示了如何把一个 flow 声明为对外部 flow 的引用:

Java Configuration
@Bean
public Job job(JobRepository jobRepository, Flow flow1, Step step3) {
	return new JobBuilder("job", jobRepository)
				.start(flow1)
				.next(step3)
				.end()
				.build();
}

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

下面的 XML 示例展示了如何把一个 flow 声明为对外部 flow 的引用:

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

像前面那样定义外部 flow,其效果是把该外部 flow 中的 step 插入到当前 job 中,就像这些 step 本来就是在这里内联声明的一样。 这样一来,多个 job 就可以引用同一个模板 flow,再把这些模板组合成不同的逻辑流程。这也是把各个 flow 的集成测试彼此拆开的好方法。

外部化 flow 的另一种形式是使用 JobStepJobStepFlowStep 类似,但它会针对所指定 flow 中的步骤, 真正创建并启动一个独立的 job execution。

  • Java

  • XML

下面的示例展示了 Java 中的 JobStep 写法:

Java Configuration
@Bean
public Job jobStepJob(JobRepository jobRepository, Step jobStepJobStep1) {
	return new JobBuilder("jobStepJob", jobRepository)
				.start(jobStepJobStep1)
				.build();
}

@Bean
public Step jobStepJobStep1(JobRepository jobRepository, JobLauncher jobLauncher, Job job, JobParametersExtractor jobParametersExtractor) {
	return new StepBuilder("jobStepJobStep1", jobRepository)
				.job(job)
				.launcher(jobLauncher)
				.parametersExtractor(jobParametersExtractor)
				.build();
}

@Bean
public Job job(JobRepository jobRepository) {
	return new JobBuilder("job", jobRepository)
				// ...
				.build();
}

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

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

	return extractor;
}

下面的示例展示了 XML 中的 JobStep 写法:

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

job parameters extractor 是一种策略,用来决定如何把该 StepExecutionContext 转换为所运行 JobJobParameters。当你希望在 job 和 step 的监控、报表方面拥有更细粒度的控制能力时,JobStep 就非常有用。 此外,JobStep 往往也是回答“如何在 job 之间建立依赖关系?”这个问题的一个好方案。它适合把大型系统拆分成更小的模块,并控制 job 之间的流转。