控制步骤流程

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

流程定义中的步骤 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 命名空间允许您在步骤元素内定义转换元素。其中一个转换是 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

  • XML

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

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

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

  • * 匹配零个或多个字符。

  • ? 匹配恰好一个字符。

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

虽然 Step 上的 transition 元素数量没有限制,但如果 Step 执行产生的 ExitStatus 未被任何元素覆盖,框架将抛出异常,并且 Job 失败。框架会自动将转换从最具体到最不具体进行排序。这意味着,即使 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 中任何 StepsBatchStatusExitStatus 均无影响。这些元素仅影响 Job 的最终状态。例如,作业中的每个步骤都可能具有 FAILED 状态,但作业的状态可能为 COMPLETED

在步骤处结束

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

  • Java

  • XML

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

当使用 XML 配置时,您可以使用 end 元素来执行此任务。end 元素还允许使用可选的 exit-code 属性来自定义 JobExitStatus。如果未给出 exit-code 属性,则 ExitStatus 默认情况下为 COMPLETED,以匹配 BatchStatus

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

使步骤失败

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

当使用 XML 配置时,fail 元素还允许使用可选的 exit-code 属性来自定义 JobExitStatus。如果未给出 exit-code 属性,则 ExitStatus 默认情况下为 FAILED,以匹配 BatchStatus

考虑以下场景:如果 step2 失败,则 JobBatchStatusFAILEDExitStatusEARLY 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">

在给定步骤处停止作业

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

  • Java

  • XML

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

当使用 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通常也是解决“如何创建作业之间的依赖关系?”这个问题的一个好方法。它是一种将大型系统分解成较小模块并控制作业流程的好方法。