控制步骤流程

能够将步骤分组到拥有作业中,就需要能够控制作业如何从一个步骤“流”到另一个步骤。Step 的失败并不一定意味着 Job 应该失败。此外,可能存在多种类型的“成功”来确定应该执行哪个 Step。根据一组 Step 的配置方式,某些步骤甚至可能根本不会被处理。

流定义中的步骤 bean 方法代理

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

在以下示例中,步骤作为参数注入到流程或作业 bean 定义方法中。这种依赖注入方式保证了流程定义中步骤的唯一性。但是,如果流程是通过调用用 @Bean 注解的步骤定义方法来定义的,那么如果禁用 bean 方法代理(即 @Configuration(proxyBeanMethods = false)),步骤可能不唯一。如果首选使用 bean 间注入方式,则必须启用 bean 方法代理。

有关 Spring Framework 中 bean 方法代理的更多详细信息,请参阅 使用 @Configuration 注解 部分。

顺序流程

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

Sequential Flow
图 1. 顺序流程

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

  • Java

  • XML

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

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

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 Batch XML 命名空间,配置中列出的第一个步骤始终是 Job 运行的第一个步骤。其他步骤元素的顺序无关紧要,但第一个步骤必须始终在 XML 中排在第一位。

条件流程

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

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

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

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

Conditional Flow
图 2. 条件流程
  • Java

  • XML

Java API 提供了一组流畅的方法,允许您指定流程以及在步骤失败时该怎么做。以下示例展示了如何指定一个步骤 (stepA),然后根据stepA是否成功,继续执行两个不同的步骤中的一个 (stepBstepC)

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 元素中定义 transition 元素。其中一个 transition 是next 元素。与next 属性类似,next 元素告诉Job接下来执行哪个Step。但是,与属性不同,在给定的Step上允许使用任意数量的next 元素,并且在失败的情况下没有默认行为。这意味着,如果使用 transition 元素,则必须明确定义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

  • XML

在使用 java 配置时,on() 方法使用简单的模式匹配方案来匹配Step执行后产生的ExitStatus

在使用 XML 配置时,transition 元素的on 属性使用简单的模式匹配方案来匹配Step执行后产生的ExitStatus

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

  • * 匹配零个或多个字符

  • ? 匹配正好一个字符

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

虽然Step上的 transition 元素数量没有限制,但如果Step执行导致的ExitStatus没有被元素覆盖,框架会抛出异常,并且Job会失败。框架会自动将 transition 从最具体到最不具体排序。这意味着,即使在前面的示例中stepA的排序被交换,ExitStatusFAILED仍然会转到stepC

批处理状态与退出状态

在配置用于条件流的Job时,理解BatchStatusExitStatus之间的区别非常重要。BatchStatus是一个枚举类型,它是JobExecutionStepExecution的属性,用于框架记录JobStep的状态。它可以是以下值之一:COMPLETEDSTARTINGSTARTEDSTOPPINGSTOPPEDFAILEDABANDONEDUNKNOWN。其中大多数都是不言自明的:COMPLETED是当步骤或作业成功完成时设置的状态,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 配置时,前面 Java 配置示例中显示的on()方法引用了ExitStatus的退出代码。

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

用英语来说,它表示:“如果退出代码为 FAILED,则转到 stepB”。默认情况下,退出代码始终与StepBatchStatus相同,这就是前面条目起作用的原因。但是,如果退出代码需要不同怎么办?一个很好的例子来自样本项目中的跳过样本作业

  • Java

  • XML

以下示例显示了如何在 Java 中使用不同的退出代码

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 中使用不同的退出代码

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

step1有三种可能性

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

  • Step成功完成。

  • Step成功完成,但退出代码为COMPLETED WITH SKIPS。在这种情况下,应该运行不同的步骤来处理错误。

前面的配置有效。但是,需要根据执行跳过记录的条件更改退出代码,如下面的示例所示

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。如果这两个条件都满足,则返回一个新的ExitStatus,其退出代码为COMPLETED WITH SKIPS

配置停止

在讨论了BatchStatusExitStatus之后,人们可能会想知道JobBatchStatusExitStatus是如何确定的。虽然这些状态是通过执行的代码为Step确定的,但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的状态定义如下

  • 如果StepExitStatusFAILED结束,则JobBatchStatusExitStatus都为FAILED

  • 否则,JobBatchStatusExitStatus都为COMPLETED

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

在步骤处结束

配置步骤结束会指示JobBatchStatusCOMPLETED停止。以COMPLETED状态完成的Job无法重新启动(框架会抛出JobInstanceAlreadyCompleteException)。

  • Java

  • XML

在使用 Java 配置时,end 方法用于此任务。end 方法还允许使用可选的 exitStatus 参数来自定义 JobExitStatus。如果没有提供 exitStatus 值,则默认情况下 ExitStatusCOMPLETED,以匹配 BatchStatus

在使用 XML 配置时,可以使用 end 元素来执行此任务。end 元素还允许使用可选的 exit-code 属性来自定义 JobExitStatus。如果没有提供 exit-code 属性,则默认情况下 ExitStatusCOMPLETED,以匹配 BatchStatus

考虑以下场景:如果 step2 失败,则 Job 将停止,BatchStatusCOMPLETEDExitStatusCOMPLETED,并且 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">

使步骤失败

将步骤配置为在给定点失败会指示 Job 停止,BatchStatusFAILED。与 end 不同,Job 的失败不会阻止 Job 重启。

在使用 XML 配置时,fail 元素还允许使用可选的 exit-code 属性来自定义 JobExitStatus。如果没有提供 exit-code 属性,则默认情况下 ExitStatusFAILED,以匹配 BatchStatus

考虑以下场景:如果 step2 失败,则 Job 将停止,BatchStatusFAILEDExitStatusEARLY TERMINATION,并且 step3 不会执行。否则,执行将移至 step3。此外,如果 step2 失败并且 Job 重启,则执行将从 step2 重新开始。

  • Java

  • XML

以下示例显示了 Java 中的场景

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 中的场景

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

在给定步骤停止作业

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

  • Java

  • XML

在使用 Java 配置时,stopAndRestart 方法需要一个 restart 属性,该属性指定在重启 Job 时应从哪个步骤开始执行。

在使用 XML 配置时,stop 元素需要一个 restart 属性,该属性指定在重启 Job 时应从哪个步骤开始执行。

考虑以下场景:如果 step1COMPLETE 完成,则作业将停止。一旦它被重启,执行将从 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更多的信息来决定下一步执行哪个步骤。在这种情况下,可以使用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

在以下示例中,实现JobExecutionDecider的 bean 在使用 Java 配置时直接传递给next调用

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指定要使用的决策器以及所有转换

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 还允许将作业配置为并行流程。

  • Java

  • XML

基于 Java 的配置允许您通过提供的构建器配置拆分。如下例所示,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"/>

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

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

  • Java

  • XML

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

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 示例展示了如何将流程声明为对其他地方定义的流程的引用

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>

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

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

  • Java

  • XML

以下示例展示了Java中JobStep的示例

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的示例

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>

作业参数提取器是一种策略,它决定如何将StepExecutionContext转换为运行的JobJobParameters。当您希望对作业和步骤进行更细粒度的监控和报告时,JobStep非常有用。使用JobStep通常也是解决“如何创建作业之间的依赖关系?”这个问题的良好答案。它是一种将大型系统分解成更小的模块并控制作业流程的好方法。