spring batch批处理框架使用

1、使用场景

一个典型的批处理程序通常:
从数据库、文件或队列中读取大量记录。
以某种方式处理数据。
以修改后的形式写回数据。

Spring Batch 自动化了这个基本的批处理迭代,提供了将类似事务作为一组处理的能力,通常是在离线环境中,无需任何用户交互。批处理作业是大多数 IT 项目的一部分,Spring Batch 是唯一提供强大的企业级解决方案的开源框架。

2、业务场景

定期提交批处理
并发批处理:一个作业的并行处理
分阶段的企业消息驱动处理
大规模并行批处理
失败后手动或计划重启
相关步骤的顺序处理(扩展工作流驱动的批处理)
部分处理:跳过记录(例如,在回滚时)
整批事务,适用于小批量或现有存储过程/脚本的情况

3、技术目标

批处理开发人员使用 Spring 编程模型:专注于业务逻辑,让框架负责基础设施。
基础架构、批处理执行环境和批处理应用程序之间的关注点清晰分离。
提供通用的核心执行服务作为所有项目都可以实现的接口。
提供可以“开箱即用”的核心执行接口的简单和默认实现。
通过在所有层中利用 spring 框架,易于配置、自定义和扩展服务。
所有现有的核心服务都应该易于替换或扩展,而不会对基础设施层产生任何影响。
提供一个简单的部署模型,架构 JAR 与应用程序完全分离,使用 Maven 构建。

在这里插入图片描述
上图突出了构成 Spring Batch 领域语言的关键概念。

一个 Job
有一对多的步骤step,每个步骤step正好有一个ItemReader、一个ItemProcessor和一个ItemWriter。需要启动一个作业(使用
JobLauncher),并且需要存储有关当前运行进程的元数据在JobRepository 中。

4 配置和运行一个JOB

4.1 配置job

@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .start(playerLoad())
                     .next(gameLoad())
                     .next(playerSummarization())
                     .build();
}
4.1.1 重启(启动)配置

批处理的一个核心问题是需要定义重启(启动)时的一些行为。当指定的JobInstance被JobExecution执行时候即认为某个Job已经重启(启动)。理想状态下,所有的任务都应该可以从它们之前中断的位置启动,但是某些情况下这样做是无法实现的。开发人员可以关闭重启机制或认为每次启动都是新的JobInstance:

@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .preventRestart() //防止重启
                     ...
                     .build();
}
4.1.2 Job Execution监听

在 Job 的执行过程中,通知其生命周期中的各种事件可能很有用,以便可以执行自定义代码。SimpleJob允许通过在适当的时间调用 a 来实现这 一点 JobListener:

public interface JobExecutionListener {

    void beforeJob(JobExecution jobExecution);

    void afterJob(JobExecution jobExecution);

}
@Bean
public Job footballJob() {
    return this.jobBuilderFactory.get("footballJob")
                     .listener(sampleListener())
                     ...
                     .build();
}

afterJob无论Job成功与否都会被调用。如果需要确定成功或失败,可以从JobExecution获取。
除了直接实现接口还可以用 @BeforeJob 和 @AfterJob 注解。


public void afterJob(JobExecution jobExecution){
    if( jobExecution.getStatus() == BatchStatus.COMPLETED ){
        //job success
    }
    else if(jobExecution.getStatus() == BatchStatus.FAILED){
        //job failure
    }
}
4.1.3 job 参数验证

默认校验类DefaultJobParametersValidator用来约束简单的参数校验,对于更复杂的约束你可以实现JobParametersValidator类。

@Bean
public Job job1() {
    return this.jobBuilderFactory.get("job1")
                     .validator(parametersValidator())
                     ...
                     .build();
}

4.2 java配置

@EnableBatchProcessing工作方式类似于 Spring 家族中的其他 @Enable* 注释。为构建批处理job提供基本配置,应用启动时下列bean将自动配置并注入spring容器中:
JobRepository - bean name “jobRepository”
JobLauncher - bean name “jobLauncher”
JobRegistry - bean name “jobRegistry”
PlatformTransactionManager - bean name “transactionManager”
JobBuilderFactory - bean name “jobBuilders”
StepBuilderFactory - bean name “stepBuilders”;

有了基本配置,用户就可以使用提供的构建器工厂来配置作业。

@Configuration
@EnableBatchProcessing
@Import(DataSourceConfiguration.class)
public class AppConfig {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    public Job job(@Qualifier("step1") Step step1, @Qualifier("step2") Step step2) {
        return jobs.get("myJob").start(step1).next(step2).build();
    }

    @Bean
    protected Step step1(ItemReader<Person> reader,
                         ItemProcessor<Person, Person> processor,
                         ItemWriter<Person> writer) {
        return steps.get("step1")
            .<Person, Person> chunk(10)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
    }

    @Bean
    protected Step step2(Tasklet tasklet) {
        return steps.get("step2")
            .tasklet(tasklet)
            .build();
    }
}

4.3 配置 JobRepository

开启@EnableBatchProcessing后spring-batch 为用户提供一个开箱即用的JobRepository,用于对Spring Batch中各种持久域对象基本的CRUD 操作。用户也可以实现JobRepository接口类自定义配置仓库。
配置如下:


@Override
protected JobRepository createJobRepository() throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);
    factory.setTransactionManager(transactionManager);
    factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");
    factory.setTablePrefix("BATCH_");
    factory.setMaxVarCharLength(1000);
    return factory.getObject();
    //除了 dataSource 和 transactionManager 之外,上面列出的配置选项都不是必需的。如果未设置,将使用上面显示的默认值
}
4.3.1 JobRepository 的事务配置

当job execution实体初始化创建时设置事务隔离等级
隔离1:默认ISOLATION_SERIALIZABLE两个进程同时启动同一个job时只有一个会成功;READ_COMMITTED也可以防止此种情况。
隔离2:READ_UNCOMMITTED 如果可以控制不同时启动相同job可以用这种策略。

不使用FactoryBean时,其他配置方式如下

@Bean
public TransactionProxyFactoryBean baseProxy() {
	TransactionProxyFactoryBean transactionProxyFactoryBean = new TransactionProxyFactoryBean();
	Properties transactionAttributes = new Properties();
	transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
	transactionProxyFactoryBean.setTransactionAttributes(transactionAttributes);
	transactionProxyFactoryBean.setTarget(jobRepository());
	transactionProxyFactoryBean.setTransactionManager(transactionManager());
	return transactionProxyFactoryBean;
}
4.3.2 更改元数据表前缀

默认以batch_开头,如需修改,参考如下配置;
注:只有表前缀可配置,表明和列名不能更改。

@Override
protected JobRepository createJobRepository() throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);
    factory.setTransactionManager(transactionManager);
    factory.setTablePrefix("SYSTEM.TEST_");
    return factory.getObject();
}
4.3.3 将元数据存储至内存中

在某些情况下,您可能不想将域对象持久化到数据库中。一个原因可能是速度。在每个提交点存储域对象需要额外的时间。另一个原因可能是您不需要为特定工作保留状态。出于这个原因,Spring 批处理提供了Map作业存储库的内存版本。
注:在jvm实例重启后,内存中的信息将会丢失。

@Override
protected JobRepository createJobRepository() throws Exception {
    MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean();
    factory.setTransactionManager(transactionManager);
    return factory.getObject();
}

4.4 配置 JobLauncher

@EnableBatchProcessing开启时,可以直接使用JobLauncher

...
// This would reside in your BatchConfigurer implementation
@Override
protected JobLauncher createJobLauncher() throws Exception {
	SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
	jobLauncher.setJobRepository(jobRepository);
	jobLauncher.afterPropertiesSet();
	return jobLauncher;
}

同步启动序列图:
在这里插入图片描述
异步启动序列图:
在这里插入图片描述
异步配置:

@Bean
public JobLauncher jobLauncher() {
	SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
	jobLauncher.setJobRepository(jobRepository());
	jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
	jobLauncher.afterPropertiesSet();
	return jobLauncher;
}

4.5 运行JobLauncher

@Controller
public class JobLauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

    @RequestMapping("/jobLauncher.html")
    public void handle() throws Exception{
        jobLauncher.run(job, new JobParameters());
    }
}

4.6 高级元数据应用

到目前为止,已经讨论了JobLauncher和JobRepository,它们一起代表了作业的简单启动和批处理域对象的基本 CRUD 操作。
在这里插入图片描述
高级元数据访问:
在这里插入图片描述

4.6.1 查询仓库

查询存储库以获取现有执行。此功能由JobExplorer接口提供。

public interface JobExplorer {

    List<JobInstance> getJobInstances(String jobName, int start, int count);

    JobExecution getJobExecution(Long executionId);

    StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId);

    JobInstance getJobInstance(Long instanceId);

    List<JobExecution> getJobExecutions(JobInstance jobInstance);

    Set<JobExecution> findRunningJobExecutions(String jobName);
}

JobExplorer 配置

@Override
public JobExplorer getJobExplorer() throws Exception {
	JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
	factoryBean.setDataSource(this.dataSource);
	return factoryBean.getObject();
}
4.6.2 job注册
// This is already provided via the @EnableBatchProcessing but can be customized via
// overriding the getter in the SimpleBatchConfiguration
@Override
@Bean
public JobRegistry jobRegistry() throws Exception {
	return new MapJobRegistry();
}
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() {
    JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
    postProcessor.setJobRegistry(jobRegistry());
    return postProcessor;
}
4.6.3 JobOperator

JobRepository 提供对元数据的 CRUD 操作,以及 JobExplorer提供对元数据的只读操作。但是,这些操作在一起用于执行常见的监视任务时最有用,例如停止、重新启动或汇总作业,这通常由批处理操作员完成。Spring Batch 通过JobOperator接口提供这些类型的操作 。

public interface JobOperator {

    List<Long> getExecutions(long instanceId) throws NoSuchJobInstanceException;

    List<Long> getJobInstances(String jobName, int start, int count)
          throws NoSuchJobException;

    Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException;

    String getParameters(long executionId) throws NoSuchJobExecutionException;

    Long start(String jobName, String parameters)
          throws NoSuchJobException, JobInstanceAlreadyExistsException;

    Long restart(long executionId)
          throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException,
                  NoSuchJobException, JobRestartException;

    Long startNextInstance(String jobName)
          throws NoSuchJobException, JobParametersNotFoundException, JobRestartException,
                 JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException;

    boolean stop(long executionId)
          throws NoSuchJobExecutionException, JobExecutionNotRunningException;

    String getSummary(long executionId) throws NoSuchJobExecutionException;

    Map<Long, String> getStepExecutionSummaries(long executionId)
          throws NoSuchJobExecutionException;

    Set<String> getJobNames();

}

job操作员注入配置:

 @Bean
 public SimpleJobOperator jobOperator(JobExplorer jobExplorer,
                                JobRepository jobRepository,
                                JobRegistry jobRegistry) {

	SimpleJobOperator jobOperator = new SimpleJobOperator();

	jobOperator.setJobExplorer(jobExplorer);
	jobOperator.setJobRepository(jobRepository);
	jobOperator.setJobRegistry(jobRegistry);
	jobOperator.setJobLauncher(jobLauncher);

	return jobOperator;
 }
4.6.4 JobParametersIncrementer

在JobParameters 中加入参数,addLong(“run.id”, id)使之始终可以执行一个新job。

4.6.5 jobOperator是优雅地停止作业
Set<Long> executions = jobOperator.getRunningExecutions("sampleJob");
jobOperator.stop(executions.iterator().next());

5 配置step

5.1 Chunk-oriented Processing 面向块处理

Spring Batch 在其最常见的实现中使用“面向块”的处理方式。面向块的处理是指一次读取一个数据并创建在事务边界内写出的“块”。一旦读取的项目数等于提交间隔,整个块被写出 ItemWriter,然后事务被提交。下图显示了该过程:
在这里插入图片描述
实现原理,伪代码如下:

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 时序列图如下:
在这里插入图片描述
伪代码如下:

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);
5.1.1 配置step
/**
 * 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();
}

创建step时需要以下依赖

  • reader: 输入
  • writer: 输出
  • transactionManager: 事务管理器
  • repository: 用来存储处理期内StepExecution和ExecutionContext 的仓库
  • chunk: 事务提交之前处理的项目数
5.1.2 The Commit Interval 提交间隔

下面代码中chunk提交间隔为10

@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();
}
5.1.3 step重启配置

step 启动次数限制

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

重启一个完成的step配置:

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<String, String>chunk(10)
				.reader(itemReader())
				.writer(itemWriter())
				//允许重启 如果该step已经完成了
				.allowStartIfComplete(true)
				.build();
}

其他step重启配置示例:

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

描述了该 footballJob示例的三个运行中的每一个会发生什么,代码示例如上。
运行 1:
playerLoad运行并成功完成,将 400 名玩家添加到“PLAYERS”表中。
gameLoad运行和处理 11 个文件的游戏数据,将其内容加载到“游戏”表中。
playerSummarization开始处理并在 5 分钟后失败。

运行 2:
playerLoad不运行,因为它已经成功完成,并且 allow-start-if-complete是“假”(默认值)。
gameLoad再次运行并处理另外 2 个文件,将它们的内容也加载到“GAMES”表中(进程指示器指示它们尚未处理)。
playerSummarization开始处理所有剩余的游戏数据(使用进程指示器过滤)并在 30 分钟后再次失败。

运行 3:
playerLoad不运行,因为它已经成功完成,并且 allow-start-if-complete是“假”(默认值)。
gameLoad再次运行并处理另外 2 个文件,将它们的内容也加载到“GAMES”表中(进程指示器指示它们尚未处理)。
playerSummarization没有启动并且作业被立即终止,因为这是 的第三次执行playerSummarization,并且它的限制只有 2。要么必须提高限制,要么Job必须作为新的JobInstance.

5.1.4 step跳过配置

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

以下 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();
}

5.2 TaskletStep小任务步骤

Chunk-Oriented Processing不是处理 step 的唯一方法。

考虑下面的一个场景,如果你仅仅需要调用一个存储过程,你可以在 ItemReader 中实现这个调用,然后在存储过程完成调用后返回 null。这种设计看起来不是那么自然也不是非常优美,因为你的批量设计中甚至都不需要实现 ItemWriter。针对这种情况,Spring Batch 为你提供了 TaskletStep 选项。

TaskletStep 是一个简单的接口,这个接口只需要实现一个方法execute,这个方法将会被TaskletStep多次重复的调用,直到这个方法返回 RepeatStatus.FINISHED 或者抛出异常来表示调用失败。

Tasklet 的每一次调用都会包含在事务中(Transaction)。Tasklet 的实现(implementors)可以调用一个存储过程,一个脚本或者一个简单的 SQL 更新脚本。

针对我们的实践中,我们可以使用 Tasklet 来执行一个 FTP 的任务。
将我们产生的中间文件上传到不同的 FTP 服务器上,你可以在实现中指定不同的服务器配置参数,这样更加有利于代码的重用。

为了能够创建一个 TaskletStep,Bean 需要传递一个 tasklet 方法到构造器(builder),这个 tasklet 方法需要实现 Tasklet 接口。

当你构建 TaskletStep 的时候不要调用 chunk。

下面的示例代码显示了一个在 Step build 中构建一个简单的 tasklet。

@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet(myTasklet())
.build();
}
5.2.1 TaskletAdapter小任务适配器

与 ItemReader 和 ItemWriter 接口的 adapters一样。Tasklet 接口包含的实现也允许能够通过已经存在的类使用 TaskletAdapter 来将自己进行注册。

例如,你希望使用一个已经存在的 DAO 来更新记录集上的标记的时候,你可以使用 TaskletAdapter 来进行实现。

使用 TaskletAdapter 能够让你的 DAO 可以被 Spring Batch 的 TaskletStep 调用而不需要让你的 DAO 都实现 Tasklet 的接口。

如下面的示例代码:

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

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

return adapter;
}

如果你的 tasklet 实现了 StepListener 接口的话,TaskletStep 将会自动将 tasklet 注册成为一个 StepListener。

5.2.2 Tasklet 实现(Implementation)示例

在主批量作业开始之前,可能需要很多其他的批量作业必须完成,这样以便于主批量作业能够获得必要的资源和在完成后释放资源或者进行清理。

例如我们遇到下面的使用场景,一个批量作业需要大量的对文件进行交互和使用,通常来说需要在文件被上传到其他服务器上后删除本地产生的临时文件。

下面的示例就是一个 Tasklet 的实现,这个Tasklet 的实现能够完成上面的交互要求(文件来自 Spring Batch samples project 示例程序)。

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 方法,这个 Tasklet 应该只被执行一次。

所有相关执行的操作需要在 Step 中进行设置,请参考下面有关这个 Tasklet 的设置:

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;
}

5.3 Controlling Step Flow 控制步骤流

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

5.3.1 顺序流

在这里插入图片描述

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

在这里插入图片描述

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

使用 java 配置时,该on()方法使用简单的模式匹配方案来ExitStatus匹配Step.
模式中只允许使用两个特殊字符:
"" 匹配零个或多个字符
“?” 恰好匹配一个字符
例如,“c
t”匹配“cat”和“count”,而“c?t”匹配“cat”但不匹配“count”。

5.3.2.1 Batch Status Versus Exit Status批处理状态和退出状态

批处理状态是一个枚举,它是JobExecution 和StepExecution 的一个属性,框架使用他来记录job和step的状态。他有下列值:OMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, or UNKNOWN
下面是你在配置条件流中使用的一个枚举例子:

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

下面例子展示了如何在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();
}
5.3.3 配置停止
5.3.3.1 在某一步骤后结束job
@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
				.start(step1())
				.next(step2())
				.on("FAILED").end()
				.from(step2()).on("*").to(step3())
				.end()
				.build();
}
5.3.3.2 使step失败
@Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1())
			.next(step2()).on("FAILED").fail()
			.from(step2()).on("*").to(step3())
			.end()
			.build();
}
5.3.3.2在给定的一个step停止job
Bean
public Job job() {
	return this.jobBuilderFactory.get("job")
			.start(step1()).on("COMPLETED").stopAndRestart(step2())
			.end()
			.build();
}

如果step1完成,停止job。一旦重启job,从step2开始执行

5.3.4 策略控制流

自定义策略

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);
    }
}
@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();
}
5.3.5 并行流

通过的builder来配置并行流。如下例,并行元素包括一个或多个流元素。单独的流可以被分开定义。'split’元素也可以包含任何前面讨论过的过渡元素,比如 ‘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();
}
5.3.6 外部流定义及job间的依赖关系

部分在job中的流可以作为一个独立的bean来定义和复用,有两种声明方式:
方式1:

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

方式2:使用jobstep来创建扩展流, JobStep类似于FlowStep,但实际上为指定流中的步骤创建并启动单独的job execution 。

@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;

5.4 Late Binding of Job and Step Attributes 运行期间为job和step绑定指定属性

读取指定文件在写代码时写死

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

怎么能够在运行时再确定读取的文件呢?如下,可以读取配置文件中的变量来确定需要读取的文件

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

也可以将参数放入JobParameters 来传递

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

或者从jobExecutionContext,stepExecutionContext中取值

@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.file.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}
@StepScope
@Bean
public FlatFileItemReader flatFileItemReader(@Value("#{stepExecutionContext['input.file.name']}") String name) {
	return new FlatFileItemReaderBuilder<Foo>()
			.name("flatFileItemReader")
			.resource(new FileSystemResource(name))
			...
}
5.4.1 Step Scope step作用域@StepScope

前面展示的所有后期绑定示例都在bean定义上声明了“step”的作用域 。
下面这个例子展示了在java中绑定作用域的例子

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

为了使用后属性绑定,必须使用@StepScope,以允许找到属性。因为直到step启动bean实际上是没有实例化的。因为实际上他不是spring容器的一部分。

5.4.2 job Scope job作用域@JobScope

类似于step scope ,但它实际是job content的作用域。因此每个运行的作业只有一个这样的bean实例 。使用#{…} 占位符来支持后属性绑定。

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

6 ItemReaders 和 ItemWriters

6.1 itemReader

ItemReaders描述
AbstractItemCountingItemStreamItemReader抽象基类,支持重启,通过统计(counting)从 ItemReader 返回对象的数量来实现.
AggregateItemReader此 ItemReader 提供一个 list , 用来存储 ItemReader 读取的对象, 直到他们已准备装配为一个集合。此 ItemReader 通过 FieldSetMapper 的常量值 AggregateItemReader#BEGIN_RECORD 以及 AggregateItemReader#END_RECORD 来标记记录的开始与结束。
AmqpItemReader给定一个Spring AmqpTemplate,它提供同步接收方法。 receiveAndConvert()方法允许您接收POJO对象。
KafkaItemReader一个从Apache Kafka主题读取消息的ItemReader。 可以将其配置为从同一个主题的多个分区读取消息。 此阅读器在执行上下文中存储消息偏移量,以支持重新启动功能。
FlatFileItemReader读取平面文件
HibernateCursorItemReader根据HQL查询从游标读取数据
HibernatePagingItemReader从分页的HQL查询读取数据
ItemReaderAdapter使任何类适应ItemReader接口。
JdbcCursorItemReader通过JDBC从数据库游标读取数据
JdbcPagingItemReader给定一条SQL语句,分页遍历行,这样就可以在不耗尽内存的情况下读取大型数据集。
JmsItemReader给定一个Spring JmsOperations对象和一个要向其发送错误的JMS Destination或目的地名称,提供通过注入的JmsOperations#receive()方法接收到的项目
JpaPagingItemReader给定JPQL语句,页遍历行,这样就可以在不耗尽内存的情况下读取大型数据集。
ListItemReaderProvides the items from a list, one at a time.
MongoItemReader给定一个MongoOperations对象和一个基于json的MongoDB查询,提供从MongoOperations#find()方法接收到的条目。
Neo4jItemReader给定一个Neo4jOperations对象和一个Cyhper查询的组件,条目将作为Neo4jOperations的结果返回。 查询方法。
RepositoryItemReader给定一个Spring Data PagingAndSortingRepository对象、一个Sort和要执行的方法名称,返回Spring Data存储库实现提供的项。
StoredProcedureItemReader从执行数据库存储过程后产生的数据库游标读取数据
StaxEventItemReader通过 StAX 读取. 请参考 HOWTO – Read from a File
JsonItemReaderReads items from a Json document

6.2 itemWriter

item Writer描述
AbstractItemStreamItemWriter组合ItemStream和ItemWriter接口的抽象基类。
AmqpItemWriter给定一个Spring AmqpTemplate,它提供了一个同步发送方法。 convertAndSend(Object)方法允许您发送POJO对象。
CompositeItemWriter将项传递给注入的ItemWriter对象列表中的每个对象的写方法
FlatFileItemWriter写入至文件中. 包括ItemStream and Skippable functionality
GemfireItemWriter使用一个 GemfireOperations 对象, 项根据删除标志的配置从Gemfire实例中写入或删除
HibernateItemWriter这个 item writer 是一个Hibernate-session aware 并且处理一些事物相关的工作,一个非 non-“hibernate-aware” item writer不需要了解上述工作,并且委托给另外的item writer来执行实际的写入.
ItemWriterAdapter使任何类适应ItemWriter接口。
JdbcBatchItemWriter使用PreparedStatement中的批处理特性(如果可用),并且可以采取基本步骤来定位刷新期间的故障。
JmsItemWriter使用一个 JmsOperations 对象,这些项目通过JmsOperations#convertAndSend() 方法写入至一个默认的队列
JpaItemWriter该项目是一个 JPA EntityManager-aware 并且处理一些事物相关的工作,一个并非 “JPA-aware” ItemWriter 不需要了解然后委托给另外一个writer来写入。
KafkaItemWriter使用一个KafkaTemplate对象,项目通过KafkaTemplate#sendDefault(object, object)方法写入默认主题,使用一个Converter来映射项目中的key。 还可以配置删除标志,将删除事件发送到主题。
MimeMessageItemWriter使用Spring的JavaMailSender, MimeMessage类型的项作为邮件消息发送。
MongoItemWriter给定一个MongoOperations对象,条目通过MongoOperations.save(object)方法写入。 实际的写操作被延迟到事务提交之前的最后一刻。
Neo4jItemWriter给定一个Neo4jOperations对象,项目通过save(object)方法持久化,或通过delete(object)根据ItemWriter的配置删除
PropertyExtractingDelegatingItemWriter扩展AbstractMethodInvokingDelegator,动态创建参数。 参数是根据注入的字段名称数组从要处理的项中的字段检索值来创建的(通过SpringBeanWrapper)。
RepositoryItemWriter给定一个 Spring Data CrudRepository 实现类, 项目通过指定配置的方法保存.
StaxEventItemWriter使用一个 Marshaller 实现类来转换项目至xml,然后使用StAX写入xml文件中
JsonFileItemWriter使用一个 JsonObjectMarshaller 实现类来转换每一个项目至json或json文件中

7 Item processing

在读和写之间插入业务处理。

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

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<Foo, Bar>chunk(2)
				.reader(fooReader())
				.processor(fooProcessor())
				.writer(barWriter())
				.build();
}

7.1 ItemProcessors链

多个处理链

public class Foo {}

public class Bar {
    public Bar(Foo foo) {}
}

public class Foobar {
    public Foobar(Bar bar) {}
}

public class FooProcessor implements ItemProcessor<Foo, Bar> {
    public Bar process(Foo foo) throws Exception {
        //Perform simple transformation, convert a Foo to a Bar
        return new Bar(foo);
    }
}

public class BarProcessor implements ItemProcessor<Bar, Foobar> {
    public Foobar process(Bar bar) throws Exception {
        return new Foobar(bar);
    }
}

public class FoobarWriter implements ItemWriter<Foobar>{
    public void write(List<? extends Foobar> items) throws Exception {
        //write items
    }
}
CompositeItemProcessor<Foo,Foobar> compositeProcessor =
                                      new CompositeItemProcessor<Foo,Foobar>();
List itemProcessors = new ArrayList();
itemProcessors.add(new FooProcessor());
itemProcessors.add(new BarProcessor());
compositeProcessor.setDelegates(itemProcessors);
@Bean
public Job ioSampleJob() {
	return this.jobBuilderFactory.get("ioSampleJob")
				.start(step1())
				.build();
}

@Bean
public Step step1() {
	return this.stepBuilderFactory.get("step1")
				.<Foo, Foobar>chunk(2)
				.reader(fooReader())
				.processor(compositeProcessor())
				.writer(foobarWriter())
				.build();
}

@Bean
public CompositeItemProcessor compositeProcessor() {
	List<ItemProcessor> delegates = new ArrayList<>(2);
	delegates.add(new FooProcessor());
	delegates.add(new BarProcessor());

	CompositeItemProcessor processor = new CompositeItemProcessor();

	processor.setDelegates(delegates);

	return processor;
}

7.2 过滤

processor在传递给writer前过滤掉不需要的数据,过滤掉的数据可以return null给writer。

7.3 验证

public interface Validator<T> {

    void validate(T value) throws ValidationException;

}
@Bean
public ValidatingItemProcessor itemProcessor() {
	ValidatingItemProcessor processor = new ValidatingItemProcessor();

	processor.setValidator(validator());

	return processor;
}

@Bean
public SpringValidator validator() {
	SpringValidator validator = new SpringValidator();

	validator.setValidator(new TradeValidator());

	return validator;
}

也可以声明BeanValidatingItemProcessor 的bean来验证

@Bean
public BeanValidatingItemProcessor<Person> beanValidatingItemProcessor() throws Exception {
    BeanValidatingItemProcessor<Person> beanValidatingItemProcessor = new BeanValidatingItemProcessor<>();
    beanValidatingItemProcessor.setFilter(true);

    return beanValidatingItemProcessor;
}

7.4 Fault Tolerance 容错

当回滚一个数据块时,在读取期间缓存的项可能会被重新处理。 如果将某个步骤配置为容错(通常通过使用跳过或重试处理),则所使用的任何ItemProcessor都应该以幂等的方式实现。 通常,这包括对ItemProcessor的输入项不执行任何更改,只更新作为结果的实例。

8 扩展和并行处理

很多批处理问题都可以通过单进程、单线程的工作模式来完成, 所以在想要做一个复杂设计和实现之前,请审查你是否真的需要那些超级复杂的实现。
衡量实际作业(job)的性能,看看最简单的实现是否能满足需求: 即便是最普通的硬件,也可以在一分钟内读写上百MB数据文件。

当你准备使用并行处理技术来实现批处理作业时,Spring Batch提供一系列选择,本章将对他们进行讲述,虽然某些功能不在本章中涵盖。从高层次的抽象角度看,并行处理有两种模式: 单进程,多线程模式; 或者多进程模式。还可以将他分成下面这些种类:

多线程Step(单个进程)
并行Steps(单个进程)
远程分块Step(多个进程)
对Step分区 (单/多个进程)

8.1 多线程setp(Multi-threaded Step)

最简单的方式是在step的配置加入TaskExecutor

@Bean
public TaskExecutor taskExecutor() {
    return new SimpleAsyncTaskExecutor("spring_batch");
}

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

以上配置的结果就是在 Step 在(每次提交的块)记录的读取,处理,写入时都会在单独的线程中执行。请注意,这段话的意思就是在要处理的数据项之间没有了固定的顺序, 并且一个非连续块可能包含项目相比,单线程的例子。此外executor还有一些限制(例如,如果它是由一个线程池在后台执行的),有一个tasklet的配置项可以调整,throttle-limit默认为4。你可能根据需要增加这个值以确保线程池被充分利用,如:

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

8.2 并行 Steps

只要需要并行的程序逻辑可以划分为不同的职责,并分配给各个独立的step,那么就可以在单个进程中并行执行。并行Step执行很容易配置和使用,例如,将执行步骤(step1,step2)和步骤3step3并行执行,则可以向下面这样配置一个流程:

@Bean
public Job job() {
    return jobBuilderFactory.get("job")
        .start(splitFlow())
        .next(step4())
        .build()        //builds FlowJobBuilder instance
        .build();       //builds Job instance
}

@Bean
public Flow splitFlow() {
    return new FlowBuilder<SimpleFlow>("splitFlow")
        .split(taskExecutor())
        .add(flow1(), flow2())
        .build();
}

@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 TaskExecutor taskExecutor() {
    return new SimpleAsyncTaskExecutor("spring_batch");
}

8.3 远程分块

使用远程分块的Step被拆分成多个进程进行处理,多个进程间通过中间件实现通信. 下面是一幅模型示意图:
在这里插入图片描述
Master组件是单个进程,从属组件(Slaves)一般是多个远程进程。如果Master进程不是瓶颈的话,那么这种模式的效果几乎是最好的,因此应该在处理数据比读取数据消耗更多时间的情况下使用(实际应用中常常是这种情形)。

Master组件只是Spring Batch Step 的一个实现, 只是将ItemWriter替换为一个通用的版本,这个通用版本 “知道” 如何将数据项的分块作为消息(messages)发送给中间件。 从属组件(Slaves)是标准的监听器(listeners),不论使用哪种中间件(如使用JMS时就是 MesssageListeners ), Slaves的作用都是处理数据项的分块(chunks), 可以使用标准的 ItemWriter 或者是 ItemProcessor加上一个 ItemWriter, 使用的接口是 ChunkProcessor interface。使用此模式的一个优点是: reader, processor和 writer 组件都是现成的(就和在本机执行的step一样)。数据项被动态地划分,工作是通过中间件共享的,因此,如果监听器都是饥饿模式的消费者,那么就自动实现了负载平衡。
中间件必须持久可靠,能保证每个消息都会被分发,且只分发给单个消费者。JMS是很受欢迎的解决方案,但在网格计算和共享内存产品空间里还有其他可选的方式(如 Java Spaces服务; 为Java对象提供分布式的共享存储器)。

8.4 分区

Spring Batch也为Step的分区执行和远程执行提供了一个SPI(服务提供者接口)。在这种情况下,远端的执行程序只是一些简单的Step实例,配置和使用方式都和本机处理一样容易。下面是一幅实际的模型示意图:

在这里插入图片描述
在左侧执行的作业(Job)是串行的Steps,而中间的那一个Step被标记为 Master。图中的 Slave 都是一个Step的相同实例,对于作业来说,这些Slave的执行结果实际上等价于就是Master的结果。Slaves通常是远程服务,但也有可能是本地执行的其他线程。在此模式中,Master发送给Slave的消息不需要持久化(durable) ,也不要求保证交付: 对每个作业执行步骤来说,保存在 JobRepository 中的Spring Batch元信息将确保每个Slave都会且仅会被执行一次。

Spring Batch的SPI由Step的一个专门的实现( PartitionStep),以及需要由特定环境实现的两个策略接口组成。这两个策略接口分别是 PartitionHandler 和 StepExecutionSplitter,他们的角色如下面的序列图所示:
在这里插入图片描述
此时在右边的Step就是“远程”Slave,所以可能会有多个对象 和/或 进程在扮演这一角色,而图中的 PartitionStep 在驱动(/控制)整个执行过程。PartitionStep的配置如下所示:

@Bean
public Step step1Manager() {
    return stepBuilderFactory.get("step1.manager")
        .<String, String>partitioner("step1", partitioner())
        .step(step1())
        .gridSize(10)
        .taskExecutor(taskExecutor())
        .build();
}

在Spring Batch Samples示例程序中有一个简单的例子在单元测试中可以拷贝/扩展(详情请参考 *PartitionJob.xml 配置文件)。

Spring Batch 为分区创建执行步骤,名如“step1:partition0”,等等,所以我们经常把Master step叫做“step1:master”。在Spring 3.0中也可以为Step指定别名(通过指定 name 属性,而不是 id 属性)。

8.4.1 分区处理器(PartitionHandler)

PartitionHandler组件知道远程网格环境的组织结构。 它可以发送StepExecution请求给远端Steps,采用某种具体的数据格式,例如DTO.它不需要知道如何分割输入数据,或者如何聚合多个步骤执行的结果。一般来说它可能也不需要了解弹性或故障转移,因为在许多情况下这些都是结构的特性,无论如何Spring Batch总是提供了独立于结构的可重启能力: 一个失败的作业总是会被重新启动,并且只会重新执行失败的步骤。

PartitionHandler接口可以有各种结构的实现类: 如简单RMI远程方法调用,EJB远程调用,自定义web服务、JMS、Java Spaces, 共享内存网格(如Terracotta或Coherence)、网格执行结构(如GridGain)。Spring Batch自身不包含任何专有网格或远程结构的实现。

但是 Spring Batch也提供了一个有用的PartitionHandler实现,在本地分开的线程中执行Steps,该实现类名为 TaskExecutorPartitionHandler,并且他是上面的XML配置中的默认处理器。还可以像下面这样明确地指定:

@Bean
public Step step1Manager() {
    return stepBuilderFactory.get("step1.manager")
        .partitioner("step1", partitioner())
        .partitionHandler(partitionHandler())
        .build();
}

@Bean
public PartitionHandler partitionHandler() {
    TaskExecutorPartitionHandler retVal = new TaskExecutorPartitionHandler();
    retVal.setTaskExecutor(taskExecutor());
    retVal.setStep(step1());
    retVal.setGridSize(10);
    return retVal;
}

gridSize决定要创建的独立的step执行的数量,所以它可以配置为TaskExecutor中线程池的大小,或者也可以设置得比可用的线程数稍大一点,在这种情况下,执行块变得更小一些。

TaskExecutorPartitionHandler 对于IO密集型步骤非常给力,比如要拷贝大量的文件,或复制文件系统到内容管理系统时。它还可用于远程执行的实现,通过为远程调用提供一个代理的步骤实现(例如使用Spring Remoting)。

8.4.2 分割器(Partitioner)

分割器有一个简单的职责: 仅为新的step实例生成执行环境(contexts),作为输入参数(这样重启时就不需要考虑)。 该接口只有一个方法:

public interface Partitioner {
    Map<String, ExecutionContext> partition(int gridSize);
}

这个方法的返回值是一个Map对象,将每个Step执行分配的唯一名称(Map泛型中的 String),和与其相关的输入参数以ExecutionContext 的形式做一个映射。
这个名称随后在批处理 meta data 中作为分区 StepExecutions 的Step名字显示。 ExecutionContext仅仅只是一些 名-值对的集合,所以它可以包含一系列的主键,或行号,或者是输入文件的位置。 然后远程Step 通常使用 #{…}占位符来绑定到上下文输入(在 step作用域内的后期绑定),详情请参见下一节。

step执行的名称( Partitioner接口返回的 Map 中的 key)在整个作业的执行过程中需要保持唯一,除此之外没有其他具体要求。 要做到这一点,并且需要一个对用户有意义的名称,最简单的方法是使用 前缀+后缀 的命名约定,前缀可以是被执行的Step的名称(这本身在作业Job中就是唯一的),后缀可以是一个计数器。在框架中有一个使用此约定的 SimplePartitioner。

有一个可选接口 PartitioneNameProvider 可用于和分区本身独立的提供分区名称。 如果一个 Partitioner 实现了这个接口, 那么重启时只有names会被查询。 如果分区是重量级的,那么这可能是一个很有用的优化。 很显然,PartitioneNameProvider提供的名称必须和Partitioner提供的名称一致。

8.4.3 将输入数据绑定到 Steps

因为step的输入参数在运行时绑定到ExecutionContext中,所以由相同配置的PartitionHandler执行的steps是非常高效的。 通过 Spring Batch的StepScope特性这很容易实现(详情请参考 后期绑定)。 例如,如果 Partitioner 创建 ExecutionContext 实例, 每个step执行都以fileName为key 指向另一个不同的文件(或目录),则 Partitioner 的输出看起来可能像下面这样:

表 7.1. 由执行目的目录处理Partitioner提供的的step执行上下文名称示例

| Step Execution Name (key) |    ExecutionContext (value)  |
| filecopy:partition0        |   fileName=/home/data/one   |
| filecopy:partition1        |   fileName=/home/data/two   |
| filecopy:partition2        |   fileName=/home/data/three |

然后就可以将文件名绑定到 step 中, step使用了执行上下文的后期绑定:

@Bean
public MultiResourceItemReader itemReader(
	@Value("#{stepExecutionContext['fileName']}/*") Resource [] resources) {
	return new MultiResourceItemReaderBuilder<String>()
			.delegate(fileReader())
			.name("itemReader")
			.resources(resources)
			.build();
}

11、实践。(通过JDBC reader 输入数据,处理process,并输出writer)

1. job配置

定义job bean,定义step ,定义jdbc输入(reader)

/**
 * 支付宝对账job配置
 * @author zhangwx
 * @date 2022/05/25 11:27
 */
@Configuration
public class AliCheckBillJobConf {

  
    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    private AliCheckBillProcessor aliBillProcessor;

    @Autowired
    private AliCheckBillWriter aliBillWriter;

    @Autowired
    @Qualifier(value = "orderNewDataSource")
    private DataSource orderNewDataSource;

    @Value("${job.reader.jdbc.pagesize}")
    private Integer pageSize;

    @Value("${job.checkbill.chunksize}")
    private Integer chunkSize;


    /**
     * 对账job
     * @return
     */
    @Bean(name = "aliCheckBillJob")
    public Job aliCheckBillJob(){
        return jobBuilderFactory.get("aliCheckBillJob")
                .start(aliCheckBillStep())
                .build();
    }

    /**
     * 对账step
     * @return
     */
    @Bean
    public Step aliCheckBillStep() {
        return stepBuilderFactory.get("aliCheckBillStep")
                .<AliBusinessBillNew,AliBusinessBillNew>chunk(chunkSize)
                .reader(itemReader(null))//数据输入
                .processor(aliBillProcessor)//数据处理
                .writer(aliBillWriter)//数据输出
                .faultTolerant()
//                .skipLimit(10)
                .skip(Exception.class)//出现异常直接跳过
                //完成了还能再次执行
//                .allowStartIfComplete(true)
                .build();
    }


    /**
     * 批处理 输入
     * @param date 传入jobParameters中的参数 需要注解@StepScope
     * @return
     */
    @Bean(name = "aliBusinessBillNewReader")
    @StepScope
    public ItemReader<AliBusinessBillNew> itemReader(@Value("#{jobParameters['date']}") String date) {
        Map<String, Object> parameterValues = new HashMap<>();
        parameterValues.put("startTime", date.trim()+" 00:00:00");
        parameterValues.put("endTime", date.trim()+" 23:59:59");

        return new JdbcPagingItemReaderBuilder<AliBusinessBillNew>()
                .name("aliBusinessBillNewReader")
                .dataSource(orderNewDataSource)//数据源
                .queryProvider(queryProvider())//jdbc查询
                .parameterValues(parameterValues)//传递参数
                .rowMapper(new AliBusinessBillRowMapper())//db记录转对象
                .pageSize(pageSize)//一次读取1000条记录
                .build();
    }


    /**
     * jdbc 查询
     * @return
     */
    public PagingQueryProvider queryProvider() {
        SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();
        provider.setDataSource(orderNewDataSource);
        provider.setSelectClause("select * ");
        provider.setFromClause("from ali_business_bill");
        provider.setWhereClause(" where finish_time >= :startTime and finish_time <= :endTime ");
        provider.setSortKey("id");
        try {
            return provider.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

jdbc 查询结果集合映射至实体

public class AliBusinessBillRowMapper implements RowMapper<AliBusinessBillNew> {


    /**
     * 数据集转对象
     * @param resultSet
     * @param i
     * @return
     */
    @Override
    public AliBusinessBillNew mapRow(ResultSet resultSet, int i) {
        AliBusinessBillNew aliBusinessBill = new AliBusinessBillNew();
        Class<? extends AliBusinessBillNew> aClass = aliBusinessBill.getClass();

		//遍历字段集合,获取数据集值set进aliBusinessBill 对象中
        for (Field field : aClass.getDeclaredFields()) {
            field.setAccessible(true);
            try {
                Object object;
                if (field.getType().equals(Date.class)){
                    Timestamp timestamp = resultSet.getTimestamp(StringUtils.tfToXiahua(field.getName()));
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String format = sdf.format(timestamp.getTime());
                    object = sdf.parse(format);
                }else {
                    object = resultSet.getObject(StringUtils.tfToXiahua(field.getName()), field.getType());
                }
                field.set(aliBusinessBill,object);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return aliBusinessBill;
    }




}

2. 定义处理器

/**
 * 支付宝账单处理
 * @author zhangwx
 * @date 2022/05/25 11:34
 */
@Component
public class AliCheckBillProcessor implements ItemProcessor<AliBusinessBillNew,AliBusinessBillNew> {

	//获取自定义JobParameters 参数
	private JobParameters jobParameters;

    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        jobParameters = stepExecution.getJobParameters();
    }

    @Override
    public AliBusinessBillNew process(AliBusinessBillNew aliBusinessBill) throws Exception {

        //筛选账单
        //todo业务处理
        return aliBusinessBill;
    }
}

3. 定义writer 输出

/**
 * 批处理输出
 * @author zhangwx
 * @date 2022/05/25 11:34
 */
@Component
@Slf4j
public class AliCheckBillWriter implements ItemWriter<AliBusinessBillNew> {

    private BillDiffService billDiffService;

    public AliCheckBillWriter(BillDiffService billDiffService) {
        this.billDiffService = billDiffService;
    }

    @Override
    public void write(List<? extends AliBusinessBillNew> list) throws Exception {
         //todo 输出业务处理
    }


}

4. 多数据源配置

application.properties配置

#批处理服务数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/batch?autoReconnect=true&amp;characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.batch.initialize-schema=always
spring.batch.schema=classpath:metadata/batch_innodb.sql
#在启动应用时启动所有job
spring.batch.job.enabled=false

#数据源1
spring.datasource.ordernew.url=jdbc:mysql://localhost:3306/ordernew?autoReconnect=true&amp;characterEncoding=utf-8
spring.datasource.ordernew.username=root
spring.datasource.ordernew.password=root
spring.datasource.ordernew.driver-class-name=com.mysql.jdbc.Driver
#数据源2
spring.datasource.paapi.url=jdbc:mysql://localhost:3306/paapi?autoReconnect=true&amp;characterEncoding=utf-8
spring.datasource.paapi.username=root
spring.datasource.paapi.password=root
spring.datasource.paapi.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

# jdbc 查询limit 条数
job.reader.jdbc.pagesize=1000
#自动对账处理数据块大小
job.checkbill.chunksize=200
/**
 * 多数据源配置
 * @author zhangwx
 * @date 2021/08/26 17:50
 */
@Configuration
public class DataSourceConfiguration {


    private DefaultJpaProperties defaultJpaProperties;
    private OrderNewJpaProperties orderNewJpaProperties;
    private PaapiJpaProperties paapiJpaProperties;

    public DataSourceConfiguration(DefaultJpaProperties defaultJpaProperties,
                                   OrderNewJpaProperties orderNewJpaProperties,
                                   PaapiJpaProperties paapiJpaProperties) {
        this.orderNewJpaProperties = orderNewJpaProperties;
        this.defaultJpaProperties = defaultJpaProperties;
        this.paapiJpaProperties = paapiJpaProperties;
    }

    @Bean(name = "dataSource")
    @Primary
    public DataSource dataSource() {
       
        return DataSourceBuilder.create()
                .url(defaultJpaProperties.getUrl())
                .username(defaultJpaProperties.getUsername())
                .password(defaultJpaProperties.getPassword())
                .driverClassName(defaultJpaProperties.getDriverClassName())
                .build();
    }

    /**
     * @return
     */
    @Bean(name = "orderNewDataSource")
    public DataSource orderNewDataSource() {
        
        return DataSourceBuilder.create()
                .url(orderNewJpaProperties.getUrl())
                .username(orderNewJpaProperties.getUsername())
                .password(orderNewJpaProperties.getPassword())
                .driverClassName(orderNewJpaProperties.getDriverClassName())
                .build();
    }

    /**
     * @return
     */
    @Bean(name = "paapiDataSource")
    public DataSource paapiDataSource() {
      
        return DataSourceBuilder.create()
                .url(paapiJpaProperties.getUrl())
                .username(paapiJpaProperties.getUsername())
                .password(paapiJpaProperties.getPassword())
                .driverClassName(paapiJpaProperties.getDriverClassName())
                .build();
    }

}

@ConfigurationProperties(prefix = "spring.datasource")
@Component
@Data
public class DefaultJpaProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;
}

@ConfigurationProperties(prefix = "spring.datasource.ordernew")
@Component
@Data
public class OrderNewJpaProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;
}

@ConfigurationProperties(prefix = "spring.datasource.paapi")
@Component
@Data
public class PaapiJpaProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;
}

5. 批处理配置

@EnableBatchProcessing工作方式类似于 Spring 家族中的其他 @Enable* 注释。在这种情况下, @EnableBatchProcessing为构建批处理job提供基本配置,下面bean将自动配置并注入spring容器中:
JobRepository - bean name “jobRepository”
JobLauncher - bean name “jobLauncher”
JobRegistry - bean name “jobRegistry”
PlatformTransactionManager - bean name “transactionManager”
JobBuilderFactory - bean name “jobBuilders”
StepBuilderFactory - bean name “stepBuilders”

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean(name = "jobRepository")
    public JobRepository jobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        //批处理存储使用本地数据源
        factory.setDataSource(dataSource);
        factory.setTransactionManager(transactionManager);
        factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");
        factory.setTablePrefix("BATCH_");
//        factory.setMaxVarCharLength(1000);
        return factory.getObject();
    }

    @Bean(name = "jobLauncher")
    public JobLauncher jobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository());
        //控制作业的异步执行方式
//        jobLauncher.setTaskExecutor(new ThreadPoolTaskExecutor());
        jobLauncher.afterPropertiesSet();
        return jobLauncher;
    }

//    @Bean(name = "jobExplorer")
//    public JobExplorer jobExplorer() throws Exception {
//        JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
//        factoryBean.setDataSource(this.dataSource);
//        return factoryBean.getObject();
//    }


//    @Bean
//    public SimpleJobOperator jobOperator(JobRegistry jobRegistry) throws Exception {
//        SimpleJobOperator jobOperator = new SimpleJobOperator();
//        jobOperator.setJobExplorer(jobExplorer());
//        jobOperator.setJobRepository(jobRepository());
//        jobOperator.setJobRegistry(jobRegistry);
//        jobOperator.setJobLauncher(jobLauncher());
//
//        return jobOperator;
//    }
}

6. 调度器通过restful风格接口执行批处理任务

/**
 * 系统自动对账
 * @author zhangwx
 * @date 2022/05/25 17:54
 */
@RestController
@RequestMapping("/checkBill")
public class CheckBillController {

    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    @Qualifier(value = "aliCheckBillJob")
    private Job aliCheckBillJob;

    /**
     * 支付宝账单对账
     * @param date 日期 2022-05-01
     * @param taskId 任务id
     * @return
     */
    @GetMapping("/ali")
    public ReturnData<String> aliCheckBill(@RequestParam String date,@RequestParam String taskId){

        if (StringUtils.isEmpty(date)){
            return new ReturnData<>(ReturnData.FAIL_CODE,"请传入参数",null,null);
        }

        JobParameters jobParameter = new JobParametersBuilder()
                .addString("id", taskId)
                .addString("date", date)
                .addString("type", "ali")
                .toJobParameters();

        try {
        	//执行任务
            jobLauncher.run(aliCheckBillJob,jobParameter);
        } catch (Exception e) {
            e.printStackTrace();
            return new ReturnData<>(ReturnData.SUCCESS_CODE,"任务启动异常",e.getMessage(),null);
        }
        return new ReturnData<>(ReturnData.SUCCESS_CODE,"任务已启动",null,null);

    }

}
  1. JobRepository元数据持久化表
-- do not edit this file

-- BATCH JOB 实例表 包含与aJobInstance相关的所有信息
-- JOB ID由batch_job_seq分配
-- JOB 名称,与spring配置一致
-- JOB KEY 对job参数的MD5编码,正因为有这个字段的存在,同一个job如果第一次运行成功,第二次再运行会抛出JobInstanceAlreadyCompleteException异常。
CREATE TABLE BATCH_JOB_INSTANCE  (
	JOB_INSTANCE_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT ,
	JOB_NAME VARCHAR(100) NOT NULL,
	JOB_KEY VARCHAR(32) NOT NULL,
	constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ENGINE=InnoDB;


--BATCH_JOB_EXECUTION表包含与该JobExecution对象相关的所有信息
CREATE TABLE BATCH_JOB_EXECUTION  (
	JOB_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT  ,
	JOB_INSTANCE_ID BIGINT NOT NULL,
	CREATE_TIME DATETIME NOT NULL,
	START_TIME DATETIME DEFAULT NULL ,
	END_TIME DATETIME DEFAULT NULL ,
	STATUS VARCHAR(10) ,
	EXIT_CODE VARCHAR(2500) ,
	EXIT_MESSAGE VARCHAR(2500) ,
	LAST_UPDATED DATETIME,
	JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
	constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
	references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ENGINE=InnoDB;


-- 该表包含与该JobParameters对象相关的所有信息
CREATE TABLE BATCH_JOB_EXECUTION_PARAMS  (
	JOB_EXECUTION_ID BIGINT NOT NULL ,
	TYPE_CD VARCHAR(6) NOT NULL ,
	KEY_NAME VARCHAR(100) NOT NULL ,
	STRING_VAL VARCHAR(250) ,
	DATE_VAL DATETIME DEFAULT NULL ,
	LONG_VAL BIGINT ,
	DOUBLE_VAL DOUBLE PRECISION ,
	IDENTIFYING CHAR(1) NOT NULL ,
	constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

-- 该表包含与该StepExecution 对象相关的所有信息
CREATE TABLE BATCH_STEP_EXECUTION  (
	STEP_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,
	VERSION BIGINT NOT NULL,
	STEP_NAME VARCHAR(100) NOT NULL,
	JOB_EXECUTION_ID BIGINT NOT NULL,
	START_TIME DATETIME NOT NULL ,
	END_TIME DATETIME DEFAULT NULL ,
	STATUS VARCHAR(10) ,
	COMMIT_COUNT BIGINT ,
	READ_COUNT BIGINT ,
	FILTER_COUNT BIGINT ,
	WRITE_COUNT BIGINT ,
	READ_SKIP_COUNT BIGINT ,
	WRITE_SKIP_COUNT BIGINT ,
	PROCESS_SKIP_COUNT BIGINT ,
	ROLLBACK_COUNT BIGINT ,
	EXIT_CODE VARCHAR(2500) ,
	EXIT_MESSAGE VARCHAR(2500) ,
	LAST_UPDATED DATETIME,
	constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

--BATCH_STEP_EXECUTION_CONTEXT表包含ExecutionContextStep相关的所有信息
CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT  (
	STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
	SHORT_CONTEXT VARCHAR(2500) NOT NULL,
	SERIALIZED_CONTEXT TEXT ,
	constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
	references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
) ENGINE=InnoDB;

-- 该表包含ExecutionContextJob相关的所有信息
CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT  (
	JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
	SHORT_CONTEXT VARCHAR(2500) NOT NULL,
	SERIALIZED_CONTEXT TEXT ,
	constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;


CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ);

CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ);

CREATE TABLE BATCH_JOB_SEQ (
	ID BIGINT NOT NULL,
	UNIQUE_KEY CHAR(1) NOT NULL,
	constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ);



12 maven引入

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-batch</artifactId>
</dependency>

13 项目启动时不调用job配置

搭建 SpringBoot + Spring Batch 项目时,再执行启动类时,会直接执行项目中定义好的 Job,但在实际应用场景,往往是定时调度该 Job,所以我们要在项目启动时,配置一下,让项目启动时不掉用该 Job。在 application.properties文件加上如下配置:
#Spring Batch
项目启动时不调用 Job
spring.batch.job.enabled = false
项目启动时调用 Job
spring.batch.job.enabled = true

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值