控制步骤流程

能够将步骤分组到所属的作业中,随之而来的是需要控制作业如何从一个步骤“流动”到另一个步骤。Step 的失败并不一定意味着 Job 也应该失败。此外,可能存在多种类型的“成功”,用于确定接下来应执行哪个 Step。根据一组 Steps 的配置方式,某些步骤甚至可能完全不会被处理。spring-doc.cadn.net.cn

在流程定义中逐步进行 Bean 方法代理

步骤实例在流程定义中必须是唯一的。当一个步骤在流程定义中有多个结果时, 重要的是将同一个步骤实例传递给流程定义方法(startfrom 等)。 否则,流程执行可能会出现意外行为。spring-doc.cadn.net.cn

在以下示例中,步骤作为参数注入到流程或作业 Bean 定义方法中。这种依赖注入风格保证了流程定义中步骤的唯一性。 然而,如果流程是通过调用带有 @Bean 注解的步骤定义方法来定义的,那么当 Bean 方法代理被禁用时(即 @Configuration(proxyBeanMethods = false)),步骤可能不唯一。 如果更倾向于使用 Bean 间注入风格,则必须启用 Bean 方法代理。spring-doc.cadn.net.cn

请参阅 使用 @Configuration 注解 章节,以获取更多关于 Spring 框架中 Bean 方法代理的详细信息。spring-doc.cadn.net.cn

顺序流程

最简单的流程场景是一个所有步骤按顺序执行的任务,如下面的图片所示:spring-doc.cadn.net.cn

Sequential Flow
图 1. 顺序流程

这可以通过在 step 中使用 next 来实现。spring-doc.cadn.net.cn

以下示例展示了如何在 Java 中使用 next() 方法:spring-doc.cadn.net.cn

Java 配置
@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 属性:spring-doc.cadn.net.cn

XML 配置
<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,依此类推。然而,如果 step A 失败,整个 Job 将失败,且 stepB 不会执行。spring-doc.cadn.net.cn

使用 Spring Batch XML 命名空间时,配置中列出的第一个步骤始终是由 Job 运行的第一个步骤。其他步骤元素的顺序无关紧要,但第一个步骤必须始终出现在 XML 的最前面。

条件流程

在前面的示例中,只有两种可能性:spring-doc.cadn.net.cn

  1. step 成功,接下来应执行 stepspring-doc.cadn.net.cn

  2. step 失败了,因此,job 也应该失败。spring-doc.cadn.net.cn

在许多情况下,这可能已经足够了。然而,如果 step 的失败应该触发一个不同的 step,而不是导致整体失败,该怎么办?下图展示了这样的流程:spring-doc.cadn.net.cn

Conditional Flow
图 2. 条件流程

Java API 提供了一套流畅的方法,让您能够指定流程以及在步骤失败时执行的操作。以下示例展示了如何指定一个步骤(stepA),然后根据 stepA 是否成功,继续执行两个不同步骤(stepBstepC)中的任意一个:spring-doc.cadn.net.cn

Java 配置
@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 元素内定义转换(transitions)元素。其中一种转换是 next 元素。与 next 属性类似,next 元素会告知 Job 接下来执行哪个 Step。然而,与该属性不同的是,在给定的 Step 上允许存在任意数量的 next 元素,并且在失败情况下没有默认行为。这意味着,如果使用了转换元素,则必须显式定义 Step 转换的所有行为。还需注意,单个步骤不能同时拥有 next 属性和 transition 元素。spring-doc.cadn.net.cn

next 元素用于指定要匹配的模式以及接下来要执行的步骤,如下例所示:spring-doc.cadn.net.cn

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 配置时,on() 方法使用简单的模式匹配方案来匹配由执行 Step 所产生的 ExitStatusspring-doc.cadn.net.cn

使用 XML 配置时,转换元素的 on 属性使用简单的模式匹配方案来匹配由 Step 执行后产生的 ExitStatusspring-doc.cadn.net.cn

模式中仅允许使用两个特殊字符:spring-doc.cadn.net.cn

例如,c*t 匹配 catcount,而 c?t 匹配 cat 但不匹配 countspring-doc.cadn.net.cn

虽然Step上的转换元素数量没有限制,但如果Step的执行结果产生了一个未被任何元素覆盖的ExitStatus,框架将抛出异常,且Job会失败。框架会自动按照从最具体到最不具体的顺序对转换进行排序。这意味着,即使在前面的示例中交换了stepA的顺序,FAILEDExitStatus仍然会转向stepCspring-doc.cadn.net.cn

批处理状态与退出状态

在配置条件流程的 Job 时,理解 BatchStatusExitStatus 之间的区别非常重要。BatchStatus 是一个枚举类型,它是 JobExecutionStepExecution 共有的属性,框架用它来记录 JobStep 的状态。它可以是以下值之一:COMPLETEDSTARTINGSTARTEDSTOPPINGSTOPPEDFAILEDABANDONEDUNKNOWN。其中大多数含义自明:COMPLETED 是在步骤或作业成功完成时设置的状态,FAILED 是在失败时设置的状态,依此类推。spring-doc.cadn.net.cn

以下示例在使用 Java 配置时包含 on 元素:spring-doc.cadn.net.cn

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

以下示例在使用 XML 配置时包含 next 元素:spring-doc.cadn.net.cn

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

乍一看,on 似乎引用了它所属的 StepBatchStatus。然而,它实际上引用的是 StepExitStatus。顾名思义,ExitStatus 表示 Step 执行完成后的状态。spring-doc.cadn.net.cn

使用 Java 配置时,前述 Java 配置示例中显示的 on() 方法引用了 ExitStatus 的退出代码。spring-doc.cadn.net.cn

更具体地说,当使用 XML 配置时,前面 XML 配置示例中显示的 next 元素引用了退出代码 ExitStatusspring-doc.cadn.net.cn

在英文中,它表示:“如果退出码为 FAILED,则转到 stepBspring-doc.cadn.net.cn

以下示例展示了如何在 Java 中使用不同的退出代码:spring-doc.cadn.net.cn

Java 配置
@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 中使用不同的退出代码:spring-doc.cadn.net.cn

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

step1 有三种可能性:spring-doc.cadn.net.cn

上述配置是有效的。然而,需要根据执行过程中跳过记录的情况来更改退出代码,如下例所示:spring-doc.cadn.net.cn

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上的跳过计数是否大于 0。如果两个条件都满足,则返回一个退出码为COMPLETED WITH SKIPS的新ExitStatusspring-doc.cadn.net.cn

配置停止项

在讨论了 BatchStatusExitStatus 之后, 人们可能会想知道 JobBatchStatusExitStatus 是如何确定的。 虽然这些状态是由执行的代码为 Step 确定的,但 Job 的状态则是基于配置确定的。spring-doc.cadn.net.cn

到目前为止,所有讨论过的作业配置都至少有一个最终的 Step,且没有过渡。spring-doc.cadn.net.cn

在以下的 Java 示例中,step 执行之后,Job 结束:spring-doc.cadn.net.cn

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

在以下的 XML 示例中,step 执行后,Job 结束:spring-doc.cadn.net.cn

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

如果未为 Step 定义任何转换,则 Job 的状态定义如下:spring-doc.cadn.net.cn

虽然这种终止批处理作业的方法对于某些批处理作业(例如简单的顺序步骤作业)已经足够,但可能需要自定义的作业停止场景。为此,Spring Batch 提供了三个转换元素用于停止 Job(除了我们之前讨论过的 next 元素)。这些停止元素中的每一个都会以特定的 BatchStatus 停止一个 Job。需要注意的是,停止转换元素对 Job 中任何 StepsBatchStatusExitStatus 都没有影响。这些元素仅影响 Job 的最终状态。例如,作业中的每个步骤的状态都可能是 FAILED,但作业本身的状态却可能是 COMPLETEDspring-doc.cadn.net.cn

在某个步骤结束

配置步骤结束会指示 JobCOMPLETEDBatchStatus 状态停止。以 COMPLETED 状态完成的 Job 无法重新启动(框架将抛出 JobInstanceAlreadyCompleteException)。spring-doc.cadn.net.cn

使用 Java 配置时,end 方法用于此任务。end 方法还允许使用可选的 exitStatus 参数来自定义 JobExitStatus。如果未提供 exitStatus 值,则默认将 ExitStatus 设置为 COMPLETED,以匹配 BatchStatusspring-doc.cadn.net.cn

使用 XML 配置时,您可以使用 end 元素来完成此任务。end 元素还允许使用可选的 exit-code 属性来自定义 JobExitStatus。如果未指定 exit-code 属性,则默认将 ExitStatus 设置为 COMPLETED,以匹配 BatchStatusspring-doc.cadn.net.cn

考虑以下场景:如果 step2 失败,则 Job 将以 COMPLETEDBatchStatusCOMPLETEDExitStatus 停止,并且 step3 不会执行。否则,执行将转移到 step3。请注意,如果 step2 失败,则 Job 不可重启(因为状态为 COMPLETED)。spring-doc.cadn.net.cn

以下示例展示了在 Java 中的场景:spring-doc.cadn.net.cn

@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 中的场景:spring-doc.cadn.net.cn

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

步骤失败

配置一个步骤在指定点失败,会指示 JobFAILEDBatchStatus 状态停止。与结束不同,Job 的失败并不会阻止 Job 被重新启动。spring-doc.cadn.net.cn

使用 XML 配置时,fail 元素还允许一个可选的 exit-code 属性,可用于自定义 JobExitStatus。如果未提供 exit-code 属性,则默认将 ExitStatus 设置为 FAILED,以匹配 BatchStatusspring-doc.cadn.net.cn

考虑以下场景:如果 step2 失败,则 Job 将以 FAILEDBatchStatusEARLY TERMINATIONExitStatus 停止,且 step3 不会执行。否则,执行将转移到 step3。此外,如果 step2 失败并且 Job 被重启,则执行将从 step2 重新开始。spring-doc.cadn.net.cn

以下示例展示了在 Java 中的场景:spring-doc.cadn.net.cn

Java 配置
@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 中的场景:spring-doc.cadn.net.cn

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

在指定步骤停止作业

配置作业在特定步骤停止,会指示 JobSTOPPEDBatchStatus 状态停止。停止 Job 可以提供临时的处理中断,以便操作员在重新启动 Job 之前执行某些操作。spring-doc.cadn.net.cn

使用 Java 配置时,stopAndRestart 方法需要一个 restart 属性,该属性指定在重新启动 Job 时应从哪个步骤继续执行。spring-doc.cadn.net.cn

使用 XML 配置时,stop 元素需要一个 restart 属性,用于指定在 Job 重新启动时应从哪个步骤继续执行。spring-doc.cadn.net.cn

考虑以下场景:如果 step1 执行完成后接着执行 COMPLETE,则作业停止。一旦重新启动,执行将从 step2 开始。spring-doc.cadn.net.cn

以下示例展示了在 Java 中的场景:spring-doc.cadn.net.cn

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

以下列表展示了 XML 中的场景:spring-doc.cadn.net.cn

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

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

编程式流程决策

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

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 配置时,实现 JobExecutionDecider 的 bean 会直接传递给 next 调用:spring-doc.cadn.net.cn

Java 配置
@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();
}

在以下示例作业配置中,decision 指定了要使用的决策器以及所有转换:spring-doc.cadn.net.cn

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

拆分流程

迄今为止描述的每个场景都涉及一个Job,它以线性方式逐个执行其步骤。除了这种典型风格外,Spring Batch 还允许将作业配置为具有并行流。spring-doc.cadn.net.cn

基于 Java 的配置允许您通过提供的构建器来配置拆分。如下例所示,split 元素包含一个或多个 flow 元素,可以在其中定义完全独立的流程。split 元素还可以包含任何前面讨论过的转换元素,例如 next 属性,或者 nextendfail 元素。spring-doc.cadn.net.cn

@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元素。spring-doc.cadn.net.cn

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

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

工作流的一部分可以外部化为独立的 Bean 定义,然后重复使用。有两种方法可以实现这一点。第一种是将该流声明为对其他位置定义的流的引用。spring-doc.cadn.net.cn

以下 Java 示例展示了如何将一个流程声明为对别处定义的流程的引用:spring-doc.cadn.net.cn

Java 配置
@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 示例展示了如何将流声明为对别处定义的流的引用:spring-doc.cadn.net.cn

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>

定义外部流的效果(如前例所示)是将外部流中的步骤插入到作业中,仿佛它们是内联声明的一样。通过这种方式,多个作业可以引用同一个模板流,并将这些模板组合成不同的逻辑流。这也是分离各个流的集成测试的良好方法。spring-doc.cadn.net.cn

外部化流程的另一种形式是使用JobStepJobStep类似于FlowStep,但实际上会为流程中指定的步骤创建并启动一个独立的作业执行。spring-doc.cadn.net.cn

以下示例展示了 Java 中 JobStep 的一个实例:spring-doc.cadn.net.cn

Java 配置
@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 的一个例子:spring-doc.cadn.net.cn

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>

作业参数提取器是一种策略,用于确定如何将为 Step 指定的 ExecutionContext 转换为运行时的 Job 所需的 JobParameters。当您希望对作业和步骤进行更细粒度的监控和报告时,JobStep 非常有用。使用 JobStep 通常也是回答“如何在作业之间创建依赖关系?”这一问题的良好方案。它是将大型系统拆分为更小模块并控制作业流程的有效方式。spring-doc.cadn.net.cn