目录
简单介绍
核心架构
如下所示为Spring Batch的三层架构:应用层、核心层、基础架构层。
- 应用层:包含所有批处理作业,通过Spring框架管理程序员自定义的代码;
- 核心层:包含Spring Batch启动和控制所需的核心类,如:JobLauncher、Job、Step等。
- 基础架构层:提供通用的读、写和服务处理。
Spring Batch优势
- 丰富的开箱即用组件
- 面向Chunk的处理(多次读,一次写)
- 默认采用Spring提供的声明式事务管理模型
- 元数据管理(自动记录Job执行情况)
- 提供多种监控技术,对批处理操作进行监控(查看数据库记录、API、Spring Batch Admin监控、JMX控制台查看)
- 支持顺序任务、条件任务等任务流程
- 支持作业的跳过、重试、重启能力,避免因为错误导致批处理作业异常中断。
- 易扩展,如远程分块、分区等。
基本概念
概述
如下图所示,批处理操作原型。每个作业(Job)有一个到多个步骤(Step),每个步骤都只有一个ItemReader
,一个ItemProcessor
和一个ItemWriter;
需要作业启动器( JobLauncher
),并且需要存储有关当前正在运行的进程的元数据(JobRepository
)。
SpringBatch主要领域对象
- Job Repositroy:作业仓库。负责Job/Step执行过程中的状态保存,为JobLaucher、Job、Step提供标准的CRUD实现。
- Job Launcher:作业调度器。提供执行Job的入口;根据给定的Job Paramters执行作业。
- Job:作业。由多个Step组成,封装整个批处理操作。Batch操作的基础单元。
- Job Instance:作业实例。每个作业执行时,都会生成一个实例,实例会被存放在JobRepository中,如果作业失败,下次重新执行作业时,会使用同一个作业实例。
- Job Parameters:作业参数。它是一组用来启动批处理任务的参数,在启用Job时,可以设置任何需要的作业参数,需要注意的作业参数会用来标识作业实例,即不同的作业实例是通过Job参数来区分的。
- Job Execution:作业执行器。负责具体的Job的执行,每次运行Job都会启动一个新的Job执行器。
- Step:作业步。Job的一个执行环节
- Step Execution:作业步执行器。负责具体的Step的执行,每次运行Step都会启动一个新的Step执行器。
- Tasklet:Step中具体执行逻辑的操作,可以重复执行,可以设置具体的同步、异步操作等。
- Execution Context:执行上下文。他是一组框架持久化与控制的key/value对,能够让开发者在Step Execution 或 Job Execution 范畴保存需要进行持久化的状态。
- Chunk:块。给定数量Item的集合,可以定义对Chunk的读操作、处理操作、写操作、提交间隔等。
- Iem:条目。一条数据记录。
- ItemReader:条目读取器。从数据源(文件系统、数据库、队列等)中读取Item,框架提供了很多开箱即用的条目读取器实现,具体可参文档。
- ItemProcessor:条目处理器 。在Item写入数据源之前,对数据进行处理(如:数据清洗、转换、过滤、校验等)。
- ItemWriter:条目写入器。将Item批量写入数据源(文件系统、数据库、队列等)。矿机提供了很多开箱即用的条目吸入器实现,具体可参文档。
重要概念的说明
Job Instance
- Job Instance 来源:根据设置的Job Parameters从Job Repository中获取一个;如果没有获取到,则新创建。
- Job通过Job Paramters区分不同的Job Instance 。即 Job Name + Job Paramters 可以唯一的确定一个Job Instance。
- 在第一次执行Job的时候,会创建一个新的Job Instance ,但是每次执行Job的时候都会创建一个新的 Job Execution。
- 已经完成的Job Instance 不能被重复执行。
- 在同一时刻,只能有一个Job Execution 可以执行同一个Job Instance。
Job Parameters
Job Parameters支持的数据类型:String/Long/Date/Double,可以通过JobParametersBuilder构造参数。
new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
Job Execution
要注意其中 一些重要属性:status(执行状态)、startTime(开始时间)、endTime(结束时间)、exitStatus(任务运行结果)、createTime(执行器第一次持久化时间)、lastUpdated(最近一次持久化时间)、executionContext(运行过程中需要被持久化的用户数据)、failureException(任务执行过程中的异常列表)。
Spring Batch元数据架构
在Spring Batch架构中提供两种元数据的存储方式,即内存、数据库。其中数据库脚本在core包中。其结构图下图所示。在Spring Batch框架中元数据指的是:Job Instance、Job Execution、Job Parameters、Step Execution、Execution Context等数据。
对数据库表的字段的具体含义可以参考Spring Batch的官方文档,其中有详细的描述。
Job配置和运行
Job
Job的主要属性包括:id【作业唯一标识】、job-repository【定义作业仓库,默认jobRepository】、incrementer【作业参数递增器】、restartable【作业是否可以重启,默认true】、parent【指定改作业的父作业】、abstract【定义作业是否为抽象作业】。
Job主要的子元素包括Step【定义作业步】、split【定义并行作业步】、flow【引用独立定义的作业流】、decision【定义作业步执行的条件判断器、listeners【定义作业拦截器】、validator【作业参数校验器】
与Java的抽象类相似,Job也有将公共属性抽取出来,定义为AbstractJob。
Job的配置方式有两种,基于Java或者基于xml,如下代码所示:
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.preventRestart() //不允许重启
.listener(sampleListener()) //定义拦截器
.validator(parametersValidator()) //定义参数校验
.start(playerLoad())
.next(gameLoad())
.next(playerSummarization())
.end()
.build();
}
<job id="footballJob" restartable="false">
<step id="playerload" next="gameLoad"/>
<step id="gameLoad" next="playerSummarization"/>
<step id="playerSummarization"/>
<validator ref="parametersValidator"/>
<listeners>
<listener ref="sampleListener"/>
</listeners>
</job>
Job拦截器
在配置job的时候,可以首先定义拦截器,是的在Job执行前后加入自定义的业务逻辑。Spring Batch框架默认提供的实现:CompositeJobExecutionListener【拦截器组合器,支持配置一组拦截器】、JobExecutionListenerSupport【拦截器空实现,可以直接继承,仅覆写关系的方法】。
如果使用拦截器要注意拦截器异常对Job的影响。如果定义多个拦截器,则before方法按照listener的顺序执行,after方法按照相反的顺序执行。
public interface JobExecutionListener {
void beforeJob(JobExecution jobExecution);
void afterJob(JobExecution jobExecution);
}
JobParametersValidator
参数校验接口只有一个方法validate() ,框架提供了两个默认的实现类CompositeJobParametersValidator, DefaultJobParametersValidator。
void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException
继承父Job的配置
如果一组Jobs共享相似但不相同的配置,则定义一个Job
具体的Jobs可以从其继承属性的“父级”可能会有所帮助 。与Java中的类继承类似,“子代”Job
会将其元素和属性与父代结合在一起。
<job id="baseJob" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
</job>
<job id="job1" parent="baseJob">
<step id="step1" parent="standaloneStep"/>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</job>
一个完整的Job:
@Configuration
//Spring 家庭中的@Enable* 系列注解功能很类似,让我们可以运行Spring Batch。
@EnableBatchProcessing
@Import(DataSourceConfiguration.class)
public class AppConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
/**
* Job工厂构建器,构建Job
*/
@Bean
public Job job(@Qualifier("step1") Step step1, @Qualifier("step2") Step step2) {
return jobs.get("myJob").start(step1).next(step2).build();
}
/**
* Step工厂构建器,构建Step
*/
@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();
}
}
配置Job Repository
使用时@EnableBatchProcessing
,JobRepository
开箱即用的,但是我们也可以自定义Job Repository。同样是可以采用xml和Java两种形式。使用Java配置的时候,我们可以直接实现BatchConfigurer.java来完成,当然为了简便起见,我们可以继承DefaultBatchConfigurer,然后覆写其中的createJobRepository() 。代码示例如下:
@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();
}
配置Job Launcher
@Override
protected JobLauncher createJobLauncher() throws Exception {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
JobLauncher接口的简单实现。Spring Core TaskExecutor接口用于启动作业。默认设置使用同步任务执行器。SimpleJobLauncher是JobLauncher接口最基本的实现。在配置JobLaucher时,情况是同步的,时序图如【Job Launcher序列】所示;但是在很多情况下,可能存在超时问题,所以可以采用异步的方式执行作业,其时序图如【异步作业启动器序列】所示。当然此时需要在createJobLauncher() 中配置属性TaskExecutor,jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
启动Job
有很多种方式可以启动Job。现在主要介绍两中,也是在开发实践中最常用的两张,通过命令行启动、从web容器中启动。
1. 命令行启动
启动作业的脚本必须启动Java虚拟机,所以需要一个具有main方法的类作为主要入口点。Spring Batch提供了一个实现此目的的实现: CommandLineJobRunner。他
执行四项任务:
-
加载适当的
ApplicationContext
-
将命令行参数解析为
JobParameters
-
根据参数找到合适的工作
-
使用
JobLauncher
应用程序上下文中提供的启动工作。
以下示例显示了作为作业参数传递给以XML定义的作业的日期:
<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay schedule.date(date)=2007/05/05
以下示例显示了作为作业参数传递给Java中定义的作业的日期:
<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay schedule.date(date)=2007/05/05
默认情况下,CommandLineJobRunner
使用DefaultJobParametersConverter
来隐式将键/值对转换为标识作业参数。但是,可以通过在其前面加上+
来显式指定正在识别的作业参数,用-表示不用识别哪些作业参数。如下代码所示,schedule.date
是标识作业参数,而vendor.id
不是。
<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay +schedule.date(date)=2007/05/05 -vendor.id=123
2. web容器启动
如上图所示,为异步作业,需要在设置JobLaauncher时,设置为异步。代码示例如下:
@Controller
public class JobLauncherController {
@Autowired
JobLauncher jobLauncher;
@Autowired
Job job;
@RequestMapping("/jobLauncher.html")
public void handle() throws Exception{
jobLauncher.run(job, new JobParameters());
}
}
元数据的使用
这一部分可以参考官方文档。
配置Step
Step
是一个域对象,它封装了批处理作业的一个独立的顺序阶段,并包含定义和控制实际批处理所需的所有信息。
下面的代码块展示了Step的配置过程。依旧是从XML和JAVA两种方式来演示。Step配置后续内容将围绕这个展开。如下配置中配置了Job Repository、事务管理、快大小、读处理器、写处理器。
<job id="sampleJob" job-repository="jobRepository">
<step id="step1">
<tasklet transaction-manager="transactionManager">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
</job>
/**
* 注意,JobRepository通常是自动导入的,不需要显式配置
*/
@Bean
public Job sampleJob(JobRepository jobRepository, Step sampleStep) {
return this.jobBuilderFactory.get("sampleJob")
.repository(jobRepository)
.start(sampleStep)
.build();
}
/**
* 注意,TransactionManager通常是自动导入的,不需要显式配置
*/
@Bean
public Step sampleStep(PlatformTransactionManager transactionManager) {
return this.stepBuilderFactory.get("sampleStep")
.transactionManager(transactionManager)
.<String, String>chunk(10)
.reader(itemReader())
.writer(itemWriter())
.build();
}
Step与Job相类似,当需要将一些公共属性抽取出来,或者有些Step不需要完整的定义ItemReader、ItemWriter时,导致无法实例化时,
可以将Step定义为抽象的。子类在继承抽象Step的时候,可以覆写其中的属性,对于可配置的属性或者元素,也可以合并子类和父类的元素。
如下配置所示,父Step声明为抽象的,子Step继承了父Step后,覆写了commit-interval属性,然后通过merge属性,拦截器合并。
<step id="listenersParentStep" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
<tasklet>
<chunk commit-interval="10"/>
</tasklet>
</step>
<step id="concreteStep3" parent="listenersParentStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
</tasklet>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</step>
块处理
面向块的处理是指一次读取一个数据并创建在事务边界内写出的“块”。从读入一项ItemReader
,交给一项ItemProcessor
,然后汇总。一旦读取的项目数等于提交间隔,整个块就由写入 ItemWriter
,然后提交事务。下图显示了该过程:
通常情况下,Job会配置为事务作业,此时如果每read()一次就write()的话,开销比较大,影响系统性能。所以往往会,通过设置commit-interval来配置块大小,当read()commit-interval个Item的时候,一次性将其写入。以提高系统性能。
Step重启
Spring Batch框架支持状态为非“COMPELETED”的Job实例重新启动,Job实例重启的时候,会从当前失败的Step重新开始执行,同时可以通过start-limit属性控制任务启动的次数。默认情况下,Step Instance可以无限次重复启动;而Job Instance重启时,不会再次执行已经完成的任务,当然可以通过设置allow-start-if-complete来设置。
<step id="step1">
<tasklet start-limit="1">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
<step id="step2">
<tasklet allow-start-if-complete="true">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
跳过
在实际生产环境中,跳过错误非常有用,它能够让我们不因为少量的错误影响整个批处理过程。可以通过skip-limit和<skippable-exception-classes>配置跳过策略,如果想要自定义的话,可以继承SkipPolicy接口。如下的配置中,允许跳过的最大次数10次,当发生除FileNotFoundException.class之外的异常时,会跳过,直至跳过次数大于10次后,才会导致Job失败。
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(10)
.reader(flatFileItemReader())
.writer(itemWriter())
.faultTolerant()
.skipLimit(10) //跳过次数
.skip(Exception.class) //跳过异常
.noSkip(FileNotFoundException.class) //不跳过的异常
.build();
}
Step重试
Spring Batch框架提供了任务重试功能,重试次数限制功能、自定义重试策略以及重试拦截。可以通过retryable-exception-classes、retry-limit、retry-policy、cache-capacity、retry-listeners来实现重试。如下所示,配置了重试上线为3次,可以重试的异常DeadlockLoserDataAccessException.class。
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(2)
.reader(itemReader())
.writer(itemWriter())
.faultTolerant()
.retryLimit(3)
.retry(DeadlockLoserDataAccessException.class)
.build();
}
回滚控制
默认情况下,无论重试还是跳过,从ItemWriter引发的任何异常都会导致由Step控制的事务回滚。如果配置了skip,则从ItemReader引发的异常不会导致回滚。在有些情况下,ItemWriter发生的异常也不需要回滚,可以通过如下配置实现这一点。如果发生ValidationException.class异常不会回滚。当然,可以通过xml的no-rollback-exception-classes属性配置无序无须回滚的异常。
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(2)
.reader(itemReader())
.writer(itemWriter())
.faultTolerant()
.noRollback(ValidationException.class)
.build();
}
ItemReader的基本约定是仅向前。 该步骤缓冲读取器的输入,因此在回滚的情况下,不需要从读取器中重新读取项目。 但是,在某些情况下,阅读器是建立在诸如JMS队列之类的事务资源之上的。 在这种情况下,由于队列与回滚的事务相关联,因此将从队列中拉出的消息放回原处。 因此,可以将该步骤配置为不缓冲项目。
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(2)
.reader(itemReader())
.writer(itemWriter())
.readerIsTransactionalQueue()
.build();
}
关于Spring事务属性的更过设置可以参考官方文档。
Step拦截器
与Job拦截器相类似,Step也有自己的拦截器。具体可以参照官方文档。文档中会介绍的StepExecutionListener、ChunkListener、ItemReadListener、ItemProcessListener、ItemWriteListener的使用,以及如何跳过监听。
TaskletStep
面向块的处理并不是一步处理的唯一方法。如果一个步骤必须由一个简单的存储过程调用组成。除了调用实现为ItemReader,并在过程结束后返回null,并且添加一个无操作的ItemWriter。
Spring Batch为这个场景提供了一种更为优雅的实现——TaskletStep。
Tasklet是一个简单的接口,它有一个方法execute,它被TaskletStep重复调用,直到它返回RepeatStatus。完成或抛出异常来表示失败。对Tasklet的每个调用都包装在一个事务中。Tasklet实现者可以调用存储过程、脚本或简单的SQL update语句。与ItemReader
和ItemWriter
接口的其他适配器一样,该Tasklet
接口包含一个实现,使自己可以适应任何现有的类:TaskletAdapter
。
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet(myTasklet())
.build();
}
@Bean
public MethodInvokingTaskletAdapter myTasklet() {
MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
adapter.setTargetObject(fooDao());
adapter.setTargetMethod("updateFoo");
return adapter;
}
应用场景示例,设置作业删除文件
@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;
}
//自定义Tasklet
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");
}
}
控制步骤流
Step流有两种执行方式:顺序流和条件流。
//顺序流
@Bean
public Job job() {
return this.jobBuilderFactory.get("job")
.start(stepA())
.next(stepB())
.next(stepC())
.build();
}
/*
*条件流
*通配符:
* “*”匹配零个或多个字符
* “?” 完全匹配一个字符
* 当stepA()返回状态为FAILED时执行stepC,否则执行StepB
*/
@Bean
public Job job() {
return this.jobBuilderFactory.get("job")
.start(stepA())
.on("*").to(stepB())
.from(stepA()).on("FAILED").to(stepC())
.end()
.build();
}
在为条件流配置作业时,务必理解BatchStatus和ExitStatus之间的区别。BatchStatus是一个枚举,它是JobExecution和StepExecution的属性,框架使用它来记录作业或步骤的状态。它可以是以下值之一:已完成、开始、开始、停止、停止、失败、放弃或未知。其中大多数是自说明的:完成是在步骤或作业成功完成时设置的状态,失败是在步骤或作业失败时设置的状态,依此类推。在使用Java配置条件流时,on()函数的参数值来自于ExitStatus。
如果没有为某个步骤定义转换,则作业的状态定义如下:
- 如果该步骤以ExitStatus失败告终,则作业的BatchStatus和ExitStatus都失败。
- 否则,该作业的BatchStatus和ExitStatus都将完成。
Split Flows
使用并行流配置Job。
@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();
}