spring-batch day2 实战篇上
spring-batch day2 实战篇上
本篇不探讨底层原理和一些代码含义,只介绍一些使用场景。案例使用Maven作为依赖管理工具,数据库使用MySQL5.7,使用idea作为IDE工具(idea收费,但是学生可以使用学校邮箱申请一个免费使用的账号)
1.搭好基本框架
- 创建一个Maven项目
这个测试案例基于JDK1.8,使用SpringBoot版本为2.0.1.RELEASE - 导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 配置application.properties
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///springbatch?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=xxxx
#springbatch会将任务执行记录持久到数据库,参考的sql语句来源于此
spring.datasource.schema=classpath:/org/springframework/batch/core/schema-mysql.sql
spring.batch.initialize-schema=always
#spring.batch.job.names=parentJob
创建程序启动类SpringBatchDemoApplication:
@SpringBootApplication
public class SpringBatchDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBatchDemoApplication.class,args);
}
}
这样,一切就准备就绪了
2.先编写一个入门的小案例
既然SpringBatch是个批处理任务处理器,那就先让它跑起来,完成一个基本的打印任务,看看有什么结果
- 上篇曾提到“Job是一个封装整个批处理过程的一个概念”,“事实上,每一个Job本质上都是由一个或多个步骤组成”,也就是每个批处理对应一个Job,而Job实际由Step完成。这样就有了第一份案例代码:
@Configuration
@EnableBatchProcessing
public class IntroDemoConfig {
//注入创建任务对象的对象
@Autowired
private JobBuilderFactory jobBuilderFactory;
//任务的执行由Step决定
//注入创建Step对象的对象
@Autowired
private StepBuilderFactory stepBuilderFactory;
//注入任务对象
@Bean
public Job helloWorldJob(){
//get方法的参数是Job的名字,Job的名字不能相互冲突,由自己设置
return jobBuilderFactory.get("helloWorldJob")
.start(step1())
.build();
}
@Bean
public Step step1() {
//get方法的参数是Step的名字,Step的名字不能相互冲突,由自己设置
//tasklet是任务的具体执行逻辑,其中chunk操作是spring batch提供的“标准”动作,当然也可以自定义实现。
return stepBuilderFactory.get("step1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("执行任务");
//表示正常处理完成
return RepeatStatus.FINISHED;
}
}).build();
}
}
启动项目,运行结果如下:
-
spring-batch自动为我们创建了如下表,并将已经执行过的任务信息持久化入数据库中。
-
以上的案例代码的任务是在控制台中打印输出一句话:
-
数据库中也会由对应的记录,不过这里就不展示了
因为数据库中已经保存了“helloWordJob”的执行记录,所以在此启动项目并不会执行。而是打印如下信息:
@EnableBatchProcessing必须加上,不然一些类就无法加载,比如JobBuilderFactory
以上就演示了最基本的springbatch执行过程
3.演示一个Job对应多个Step:
我们知道step是任务执行的基本单位,那么一个复杂的Job必然需要多个Step完成。我们注释掉上一个案例的@Configuration和@EnableBatchProcessing,防止对我们的实验产生干扰。新代码如下:
@Configuration
@EnableBatchProcessing
public class JobDemo {
@Autowired
private JobBuilderFactory jobBuilderFactory;
//任务的执行由Step决定
//注入创建Step对象的对象
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Job jobDemoJob(){
return jobBuilderFactory.get("jobDemoJob")
.start(step1())
// .next(step2())
// .next(step3())
.on("COMPLETED").to(step2())
.from(step2()).on("COMPLETED").to(step3())
.from(step3()).end()
.build();
}
@Bean
public Step step3() {
return stepBuilderFactory.get("step3")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("step3");
return RepeatStatus.FINISHED;
}
}).build();
}
//这处的代码与上面一直,之不过将方法名改为step1() step2()stepBuilderFactory.get()传参改为对应的方法名,执行语句也改为对应的方法名,为节省篇幅,这里省去,以后遇到 【...】 均表示上述含义
//...
}
执行结果如下:
- 可以看到这次的案例和上面执行的结果一次,我们需要比较的是Job是如何管控3个Step的执行顺序的,可以看到执行顺序可以有两种方式决定:
- 按照next顺序顺序执行,没有end()方法,没有next()就结束
- 根据Job提供的on()-> to() -> from()顺序执行,直到遇到end()结束
4.Flow式操作Step
Step 是一个独立的、顺序的处理步骤,包含完整的输入、处理以及输出。但是在企业应用中,我们面对的更多情况是多个步骤按照一定的顺序进行处理。因此如何维护步骤之间的执行顺序是我们需要考虑的。Spring Batch 提供了 Step Flow :
@Configuration
@EnableBatchProcessing
public class FlowDemo {
//注入创建任务对象的对象
@Autowired
private JobBuilderFactory jobBuilderFactory;
//任务的执行由Step决定
//注入创建Step对象的对象
@Autowired
private StepBuilderFactory stepBuilderFactory;
//创建flow,指明哪些Flow对象包含哪些Step
@Bean
public Flow flowDemoFlow(){
return new FlowBuilder<Flow>("flowDemoFlow")
.start(flowDemoStep1())
.next(flowDemoStep2())
.build();
}
//创建Job对象
@Bean
public Job flowDemoJob(){
return jobBuilderFactory.get("flowDemoJob")
.start(flowDemoFlow())
.next(flowDemoStep3())
.end()
.build();
}
@Bean
public Step flowDemoStep1(){
return stepBuilderFactory.get("stepBuilderFactory1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("stepBuilderFactory1");
return RepeatStatus.FINISHED;
}
}).build();
}
//...
}
程序结果与上面相似:
不过我们更关注的是这样和上面有什么区别:
JobFlow - 一个job 可以有多个step ,每个step 有自己的逻辑,Flow可以对多个step进行排序,判断。举张图演示:
简单来说可以使用Flow来控制一个或多个Step的运行逻辑,相对于上一种方法逻辑性更强。
5.使用Decider控制Step流程
Step的执行顺序,执行与否往往需要某种逻辑来控制,这时候,具有了我们的Decider。
在此之前,我们需要知道两个概念1.BatchStatus:批处理状态和2.ExitStatus:退出状态
- BatchStatus:批处理状态
批处理状态是由批处理框架使用,用来记录Job、Step的执行情况。SpringBatch的重启即是利用了BatchStauts来实现。对应BATCH_JOB_INSTANCE和BATCH_STEP_EXECUTION表中的STATUS字段值。
JobExecution.getStatus()获取作业Job批处理状态;
StepExecution.getStatus()获取作业Step的批处理状态; - ExitStatus:退出状态
退出状态表示Step执行后或者Job执行后的状态,该值要以被修改,通常用于条件Flow中。
可以通过拦截器StepExecutionListener的afterStep方法来修改退出状态;
对应BATCH_JOB_INSTANCE和BATCH_STEP_EXECUTION表中的EXIT_CODE字段值;
状态图:
案例代码:
先定义Decider,需要实现JobExecutionDecider接口:
public class MyDeciderExecutionDecider implements JobExecutionDecider {
private int count ;
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
count++;
if (count%2==0){
return new FlowExecutionStatus("even");
}
return new FlowExecutionStatus("odd");
}
}
再根据Decider决定Step执行顺序:
@Configuration
@EnableBatchProcessing
public class DeciderDemo {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public JobExecutionDecider jobExecutionDecider(){
return new MyDeciderExecutionDecider();
}
//创建任务
@Bean
public Job deciderDemoJob(){
return jobBuilderFactory.get("deciderDemoJob")
.start(deciderDemoStep1())
.next(jobExecutionDecider())
.from(jobExecutionDecider()).on("even").to(deciderDemoStep2())
.from(jobExecutionDecider()).on("odd").to(deciderDemoStep3())
.from(deciderDemoStep3()).on("*").to(jobExecutionDecider())
.end().build();
}
//创建决策器
//创建Step
@Bean
public Step deciderDemoStep1(){
return stepBuilderFactory.get("deciderDemoStep1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("deciderDemoStep1");
return RepeatStatus.FINISHED;
}
}).build();
}
//... print(even) print(odd)
}
由decider决定执行顺序,该变decider得属性count = 1 ,发现结果不再打印“odd” ,每次运行到on()方法,就会执行decider的decide()一次,返回的结果作为on()的判断依据,只有符合on得条件才会转移到下一个状态,不然会一直执行当前step。上面的代码运行逻辑是遇到偶数则打印step2,遇到奇数则打印step3,并返回继续执行decider,直到遇到偶数退出。
6.异步执行的Step
spring-batch为我们提供了异步执行Step的Simple解决方案
@Configuration
@EnableBatchProcessing
public class SplitDemo {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Flow splitDemoFlow1(){
return new FlowBuilder<Flow>("splitDemoFlow1")
.start(splitDemoStep1())
.build();
}
@Bean
public Flow splitDemoFlow2(){
return new FlowBuilder<Flow>("splitDemoFlow2")
.start(splitDemoStep2())
.next(splitDemoStep3())
.build();
}
//创建任务
@Bean
public Job splitDemoJob(){
return jobBuilderFactory.get("splitDemoJob2")
.start(splitDemoFlow1())
.split(new SimpleAsyncTaskExecutor())
.add(splitDemoFlow2()).end().build();
}
@Bean
public Step splitDemoStep1(){
return stepBuilderFactory.get("splitDemoStep1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("splitDemoStep1");
return RepeatStatus.FINISHED;
}
}).build();
}
//...
}
执行可以看到step运行顺序不固定
7.父Job和子Job:
spring-batch可以将两个Job合并到同一个父Job下执行,需要将Job转为Step类型,演示如下
ChildJob1.java
@Configuration
public class ChildJob1 {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Step childJobStep1(){
return stepBuilderFactory.get("childJobStep1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("childJobStep1");
return RepeatStatus.FINISHED;
}
}).build();
}
@Bean
public Job childJobOne(){
return jobBuilderFactory.get("childJobOne").start(childJobStep1()).build();
}
}
ChildJob2.java
@Configuration
public class ChildJob2 {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Step child2JobStep2(){
return stepBuilderFactory.get("child2JobStep2").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("child2JobStep2");
return RepeatStatus.FINISHED;
}
}).build();
}
@Bean
public Step child2JobStep1(){
return stepBuilderFactory.get("child2JobStep1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
System.out.println("child2JobStep1");
return RepeatStatus.FINISHED;
}
}).build();
}
@Bean
public Job child2JobTwo(){
return jobBuilderFactory.get("child2JobOne").start(child2JobStep2()).next(child2JobStep1()).build();
}
}
NestedJobDemo.java
@Configuration
public class NestedDemo {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private Job childJobOne;
@Autowired
private Job child2JobTwo;
@Autowired
private JobLauncher jobLauncher;
@Bean
public Job parentJob(JobRepository jobRepository, PlatformTransactionManager transactionManager){
return jobBuilderFactory.get("parentJob")
//把子Job作为父Job的step
.start(childJob1(jobRepository,transactionManager))
.next(childJob2(jobRepository,transactionManager))
.build();
}
//返回Job类型的Step
private Step childJob2(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new JobStepBuilder(new StepBuilder("childJob2"))
.job(child2JobTwo)
.launcher(jobLauncher)//使用启动父Job的启动对象
.repository(jobRepository)
.transactionManager(transactionManager)
.build();
}
private Step childJob1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new JobStepBuilder(new StepBuilder("childJob2")).job(childJobOne).launcher(jobLauncher)//使用启动父Job的启动对象
.repository(jobRepository)
.transactionManager(transactionManager)
.build();
}
}
8.Listener:
JobListener
jobListener需要实现JobExecutionListener接口,实现beforeJob()和afterJob()方法,jobListener在Job启动和结束时调用。
chunkListener
定义一个chunkListener类,定义两个方法beforeChunk()和afterChunk()方法,同时分别加上@BeforeChunk和@AfterChunk方法。chunk在Step加载了指定条数据后自动调用。
用法
ListnerDemo.java
@Configuration
public class ListenerDemo {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Job listenerJob(){
return jobBuilderFactory.get("listenerJob1")
.start(step1())
.listener(new MyJobListener())
.build();
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
//每读完两个数据就进行数据的输出处理
.<String,String>chunk(2) //read process write
.faultTolerant()
.listener(new MyChunkListener())
.reader(read())
.writer(write())
.build();
}
@Bean
public ItemWriter<String> write() {
return new ItemWriter<String>() {
@Override
public void write(List<? extends String> list) throws Exception {
for (String s : list) {
System.out.println(s);
}
}
};
}
@Bean
public ItemReader<String> read() {
return new ListItemReader<>(Arrays.asList("java","shiro","springboot"));
}
}
MyJobListener和MyChunkListener由上面给出的方法创建
9.带参数的Job:
step级别的参数:通过StepListener接收项目启动的参数
案例:
@Configuration
@EnableBatchProcessing
public class ParametersDemo implements StepExecutionListener {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
private Map<String,JobParameter> parameters;
@Bean
public Job parametersJob(){
return jobBuilderFactory.get("parametersJob4")
.start(parameterStep())
.build();
}
//Job执行的是Step ,Job使用的数据肯定是在Step中被使用
//我们只需要给Step传递数据
//使用Step级别的监听的方式传递数据
@Bean
public Step parameterStep() {
return stepBuilderFactory.get("parameterStep")
.listener(this)
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
//输出接收到的参数的值
System.out.println(parameters.get("info"));
return RepeatStatus.FINISHED;
}
}).build();
}
@Override
public void beforeStep(StepExecution stepExecution) {
parameters = stepExecution.getJobParameters().getParameters();
}
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
return null;
}
}