关于版本
依赖 | 版本 |
---|---|
springboot | 2.4.0 |
spring batch | 2.4.0 |
代码地址
因为每个例子涉及代码较多,且包含测试用例,如果都贴到文章中内容过多,所以只贴出了部分代码。全部的代码在这里: https://gitee.com/daifyutils/springboot-samples。
此篇文章所属模块为:base-batch-2.4.0
目录地址
目录 | 测试目录 | 内容 |
---|---|---|
dai.samples.batch.allowstart | dai.samples.allow | 测试任务可以重复执行 |
dai.samples.batch.base | dai.samples.base | 基础任务配置 |
dai.samples.batch.skip | dai.samples.skip | 跳过操作 |
dai.samples.batch.listener | dai.samples.listener | 任务监听器 |
dai.samples.batch.process | dai.samples.process | 流程控制的代码 |
dai.samples.batch.add | dai.samples.add | 任务流程切割 |
dai.samples.batch.retry | dai.samples.retry | 任务重试 |
dai.samples.batch.rollback | dai.samples.rollback | 任务回滚 |
dai.samples.batch.rw | dai.samples.rw | 数据的读取和输出 |
Spring Batch其他内容
Spring Boot 整合——Spring batch基本使用
Spring Boot 整合——Spring batch的监听器
Spring Boot 整合——Spring batch任务流程控制以及流程分割
Spring Boot 整合——Spring batch通过不同方式读取数据(ItemReader)
Spring Boot 整合——Spring batch通过不同方式输出数据(ItemWriter)
Spring Boot 整合——Spring batch重试和回滚
spring batch 是spring提供的一个批数据处理的框架。提供了大量信息的自动化和定时处理的操作。其是一个相对轻量级的批处理操作
job
表示是Spring batch一次批处理的过程。其定义了批处理执行的具体逻辑,封装整个批处理过程的实体。
Step
批处理任务中的某一步骤,每个处理job由一个或者多个步骤组成。
ExecutionContext
批处理执行上下文,其能够将所需的参数在处理任务的过程中进行传递。
JobRepository
提供对处理任务的持久化操作。
JobLauncher
其主要是为处理任务job提供了一个启动器,同时在启动job的时候我们可以传递自定义的参数。
Item Reader
其可以给Step提供数据的输入,当数据读取结束后, 其会返回null来告知内容已经结束。
Item Writer
作为数据的输出,spring batch提供了一个chunk参数,任务一次会写入一个chunk的数据,而chunk的数量取决于任务启动时候的配置。
Item Processor
此内容复制在数据读入Step到写出之间对数据处理的逻辑。
step的流程
引入依赖
除了常规的Spring boot依赖,只需要额外引入此内容
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-batchartifactId>
dependency>
<dependency>
<groupId>org.springframework.batchgroupId>
<artifactId>spring-batch-testartifactId>
<scope>testscope>
dependency>
application
在使用内存作为job的持久化策略的时候可以无需配置任何额外参数,而如果希望Job信息被保存到数据库中的时候需要以下配置
spring:
application:
name: batch
datasource:
url: jdbc:mysql://localhost:3306/batch?charactorEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
use-new-id-generator-mappings: true
ddl-auto: none
naming:
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
hbm2ddl:
auto: update
# 使用create-drop存在问题,jpa会执行一些外键SQL但是此时表不存在而报错
# auto: create-drop
show-sql: true
batch:
# 使用JDBC的时候为了避免不存在表结果需要配置此内容
initialize-schema: always
server:
port: 8000
上面配置中关于datasource
的配置需要结合自己项目实际情况。而spring.batch.initialize-schema
是因为Spring Batch需要将job相关内容保存到不同的数据表中,此时需要保证数据库中存在此内容,设置always来创建对应表结构
表名 | 作用 |
---|---|
batch_job_execution | 批处理任务处理的相关记录 |
batch_job_execution_context | 任务处理的上下文操作 |
batch_job_execution_params | 处理任务相关参数 |
batch_job_execution_seq | 任务执行的序号表 |
batch_job_instance | 处理任务实例 |
batch_job_seq | 任务处理的序号表 |
batch_step_execution | 任务步骤处理的相关记录 |
batch_step_execution_context | 任务步骤处理的上下文操作 |
batch_step_execution_seq | 任务步骤处理的序号表 |
java配置
需要在java中初始化下面组件:
transactionManager
:事务管理器
JobRepository
:任务持久化操作
JobBuilderFactory
:任务创建工厂
StepBuilderFactory
:任务步创建工厂
JobLauncher
:任务启动器
@Configuration
public class BatchConfig extends DefaultBatchConfigurer {
public static final String PATH = "target/test-outputs/";
@Autowired
private DataSource dataSource;
/**
* 初始化资源型事务控制
* @return
*/
@Bean
PlatformTransactionManager transactionManager() {
return new ResourcelessTransactionManager();
}
/**
* 任务持久化 使用JDBC
* @return
* @throws Exception
*/
@Bean
protected JobRepository jobRepository() throws Exception {
setDataSource(dataSource);
JobRepository jobRepository = createJobRepository();
return jobRepository;
}
/**
* 任务持久化 使用内存的时候JDBC设置
* @return
* @throws Exception
*/
/*@Override
public void setDataSource(DataSource dataSource) {
}*/
/**
* 任务持久化 使用内存
* @return
* @throws Exception
*/
/*@Bean
protected JobRepository jobRepository() throws Exception {
MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean();
factory.setTransactionManager(transactionManager());
return factory.getObject();
}*/
@Bean
JobBuilderFactory jobBuilderFactory() throws Exception {
return new JobBuilderFactory(jobRepository());
}
@Bean
StepBuilderFactory stepBuilderFactory() throws Exception {
return new StepBuilderFactory(jobRepository(),transactionManager());
}
/**
* 任务启动器
* @return
* @throws Exception
*/
@Bean
public JobLauncher jobLauncher() throws Exception {
SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher();
simpleJobLauncher.setJobRepository(jobRepository());
return simpleJobLauncher;
}
@Bean
JobExplorer jobExplorer() throws Exception {
JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
jobExplorerFactoryBean.setDataSource(dataSource);
jobExplorerFactoryBean.afterPropertiesSet();
return jobExplorerFactoryBean.getObject();
}
@Bean
public ListableJobLocator listableJobLocator() {
return new MapJobRegistry();
}
@Bean
public SimpleJobOperator jobOperator(@Autowired ObjectProvider<JobParametersConverter> jobParametersConverter) {
SimpleJobOperator factory = new SimpleJobOperator();
factory.setJobExplorer(getJobExplorer());
factory.setJobLauncher(getJobLauncher());
factory.setJobRegistry(listableJobLocator());
factory.setJobRepository(getJobRepository());
jobParametersConverter.ifAvailable(factory::setJobParametersConverter);
return factory;
}
}
配置一个简单的处理任务
下面是一个简单的任务处理,一个简单的JOB包含了一个step以及从读-处理-写
的流程。模拟读取目标文件夹下json文件然后进行处理后写出到指定目录。
// 创建任务的配置
@Slf4j
@Component
public class BaseJobConfig {
@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;
@Autowired
PlatformTransactionManager transactionManager;
/**
* 任务
* @param jobRepository
* @return
*/
@Bean("sampleJob")
public Job sampleJob(JobRepository jobRepository) {
return this.jobBuilderFactory.get("sampleJob")
.repository(jobRepository)
.start(sampleStep())
.build();
}
/**
* 步骤处理
* @return
*/
@Bean(value = "sampleStep")
public Step sampleStep() {
return this.stepBuilderFactory.get("sampleStep")
.transactionManager(transactionManager)
.<BatchEntity, BatchEntity>chunk(1)
.reader(itemReader())
.processor(getProcessor())
.writer(itemWriter())
.build();
}
/**
* JSON写数据
* 在target/test-outputs目录下写相关结果
* @return
*/
public ItemWriter<BatchEntity> itemWriter() {
long time = System.currentTimeMillis();
String name = "sampleJob-" + time;
String patch = "target/test-outputs/" + name + ".json";
return new JsonFileItemWriterBuilder<BatchEntity>()
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
.resource(new FileSystemResource(patch))
.name("batchJobWriter")
.build();
}
/**
* JSON读数据
* 读取resource/data下的JSON文件
* @return
*/
public ItemReader<BatchEntity> itemReader() {
return new JsonItemReaderBuilder<BatchEntity>()
.jsonObjectReader(new JacksonJsonObjectReader<>(BatchEntity.class))
.resource(new ClassPathResource("data/batchJob.json"))
.name("batchJobReader")
.build();
}
/**
* 新建任务处理器
* @return
*/
public ItemProcessor<? super BatchEntity, ? extends BatchEntity> getProcessor() {
return new ChangeNameProcessor();
}
/**
* 任务处理器
*/
class ChangeNameProcessor implements ItemProcessor<BatchEntity, BatchEntity> {
@Override
public BatchEntity process(BatchEntity person) {
String fullName = "sampleJob-" + person.getFirstName() + " " +
person.getLastName();
person.setFullName(fullName);
log.info(fullName);
return person;
}
}
}
BatchEntity
JOSN内容解析的对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BatchEntity {
/**
* 初始名称
*/
private String firstName;
private String lastName;
/**
* 初始年龄
*/
private int age;
private String fullName;
private boolean isAdult;
private String helloMessage;
}
测试用例
通过JobLauncherTestUtils
的测试工具,就能测试上述内容是否正常
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {BatchApplication.class})
public class BaseJobConfigTests {
@Autowired
@Qualifier(value = "sampleJob")
private Job sampleJob;
@Autowired
private JobLauncher jobLauncher;
/**
* 基础任务测试
* @throws Exception
*/
@Test
public void testBaseJob() throws Exception {
JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
jobLauncherTestUtils.setJobLauncher(jobLauncher);
jobLauncherTestUtils.setJob(sampleJob);
File file = new File(BatchConfig.PATH);
Assert.isTrue(file.exists(),"地址错误");
Assert.isTrue(file.isDirectory(),"目录错误");
File[] files = file.listFiles();
Arrays.stream(files).forEach(item -> item.delete());
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
Assert.isTrue(BatchStatus.COMPLETED.equals(jobExecution.getStatus()),"返回状态失败");
File[] newFiles = file.listFiles();
Arrays.stream(newFiles).forEach(item -> System.out.println(item.getName()));
}
}
在进行任务处理的时候有些时候我们希望任务能够忽略一些异常或者是关注一些异常这里可以使用skipLimit
、skip
、noSkip
三个方法。
skipLimit 设置需要忽略异常的次数,当忽略的异常大于此数字任务依旧会返回FAILED
的结果
skip 设置需要忽略的异常
noSkip 设置不能忽略的异常
java代码
使用上面大致的逻辑,在ChangeNameProcessor
中添加产生异常的逻辑,使用下面的step执行任务
/**
* 遇见
* RuntimeException异常进行跳过,但是错误超过5次任务执行中断
* FileNotFoundException异常不进行跳过,直接中断任务
* @return
*/
@Bean("skipStep")
public Step skipStep() {
return this.stepBuilderFactory.get("skipStep")
.<BatchEntity, BatchEntity>chunk(5)
.reader(itemReader())
.processor(getExceptionProcessor())
.writer(itemWriter())
.faultTolerant()
.skipLimit(5)
.skip(RuntimeException.class)
.noSkip(FileNotFoundException.class)
.build();
}
测试
使用下面测试用例在异常没有超过规定的次数上限时任务依旧会成功
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {BatchApplication.class})
@Slf4j
public class SkipJobTests {
@Autowired
@Qualifier(value = "skipJob")
private Job skipJob;
@Autowired
@Qualifier(value = "skipJob10")
private Job skipJob10;
@Autowired
private JobLauncher jobLauncher;
/**
* 因为存在10条数据,没有达到限制数值,所以此时任务不会被
* @throws Exception
*/
@Test
public void testAllowStartJob10() throws Exception {
JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
jobLauncherTestUtils.setJobLauncher(jobLauncher);
jobLauncherTestUtils.setJob(skipJob10);
File file = new File(BatchConfig.PATH);
Assert.isTrue(file.exists(),"地址错误");
Assert.isTrue(file.isDirectory(),"目录错误");
File[] files = file.listFiles();
Arrays.stream(files).forEach(item -> item.delete());
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
Assert.isTrue(BatchStatus.COMPLETED.equals(jobExecution.getStatus()),"返回状态失败");
File[] newFiles = file.listFiles();
Arrays.stream(newFiles).forEach(item -> System.out.println(item.getName()));
}
}
在上面的测试用例中使用的都是JobLauncherTestUtils
方法,此方法是为了避免任务在JDBC中被设置为已完成而导致的任务无法重复执行,但是在生产中我们并不想拥有一个只能运行一次的任务,所以我们可以通过allowStartIfComplete
来调整此配置
使用上面大致的逻辑,只需要修改Step内参数就可以实现任务的重新执行。
/**
* 设置任务可以重启,但是只能重启2次
* @return
*/
@Bean(value = "allowLimitStartStep")
public Step allowLimitStartStep() {
return this.stepBuilderFactory.get("allowLimitStartStep")
.transactionManager(transactionManager)
.<BatchEntity, BatchEntity>chunk(20)
.reader(itemReader())
.processor(getProcessor())
.writer(itemWriter())
.startLimit(2)
.allowStartIfComplete(true)
.build();
}
此时需要注意startLimit
用来控制任务可以重复执行但是不能超过这个值。如果超过会抛出下面异常。当然如果需要创建一个可以无限制的任务,只需要不设置startLimit
即可
org.springframework.batch.core.StartLimitExceededException: Maximum start limit exceeded for step: allowLimitStartStepStartMax: 2
个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,因为每一个例子都提供了测试代码,一般来说不会有问题,但是因为这几篇内容断断续续用了一个半月可能会出现之后的代码影响了之前的例子,假如开发同学发现了,请及时告知,我会第一时间修改相关内容,也希望大家看在这个新春佳节只能宅到家中埋头苦逼的码代码的情况下,能给我点一个赞。你的点赞就是我前进的动力。