Spring Batch 簡介
spring batch是spring提供的一個數據處理框架。企業域中的許多應用程序需要批量處理才能在關鍵任務環境中執行業務操作。
Spring Batch是一個輕量級,全面的批處理框架,旨在開發對企業系統日常運營至關重要的強大批處理應用程序。 Spring Batch構建了人們期望的Spring Framework特性(生產力,基於POJO的開發方法和一般易用性),同時使開發人員可以在必要時輕鬆訪問和利用更高級的企業服務。 Spring Batch不是一個schuedling的框架。
spring batch的一個總體的架構如下
在spring batch中一個job可以定義很多的步驟step,在每一個step裏面可以定義其專屬的ItemReader用於讀取數據,ItemProcesseor用於處理數據,ItemWriter用於寫數據,而每一個定義的job則都在JobRepository裏面,我們可以通過JobLauncher來啓動某一個job。
step數據流
數據輸入源(來源於文件,數據庫等) --> ItemReader --> ItemProcessor --> ItemWriter --> 數據輸出到文件、數據庫等
實戰案例
場景描述: 現在需要將一個存儲了幾百萬條用戶信息的 data.csv 文件導入到系統的用戶表sys_user中, data.csv文件中每一行代表一條用戶信息,用戶信息字段之間用製表符\t
分隔
數據源文件
data.csv文件內容
姓名 性別 年齡 地址
張三 男 12 深圳
李四 男 32 廣州
王雪 女 21 上海
孫雲 女 23 北京
趙柳 女 42 成都
孫雪 女 15 武漢
文件存放路徑: resources/data.csv
maven jar包依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
spring batch配置
spring:
batch:
# 在項目啓動時進行執行建表sql
initialize-schema: always
job:
# 禁止Spring Batch自動執行,既需要用戶觸發才能執行
enabled: false
names: parentjob
# spring batch相關表前綴, 默認爲 batch_
table-prefix: batch_
datasource:
# 項目啓動時的建表sql腳本,該腳本由Spring Batch提供
schema: classpath:/org/springframework/batch/core/schema-mysql.sql
項目啓動後,會在系統對應的數據庫中新建以
batch
開頭的Spring Batch 相關表
- batch_job_execution: 表示Job執行的句柄(一次執行)
- batch_job_execution_params: 通過Job參數區分不同的Job實例,實際使用hashMap存儲參數(僅4種數據類型)
- batch_job_instance: 作業實例,一個運行期概念(一次執行關聯一個實例)
- batch_job_execution_seq
- batch_job_seq
- batch_step_execution: 執行上下文,在job/Step執行時保存需要進行持久化的狀態信息。
- batch_job_execution_context: 執行上下文,在job/Step執行時保存需要進行持久化的狀態信息。
- batch_step_execution_context
- batch_step_execution_seq
用戶實體對象
@Data
public class User {
private String userName;
private String sex;
private Integer age;
private String address;
private Byte status;
private Date createTime;
}
Spring Batch配置類
import org.springframework.batch.core.*;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.item.validator.Validator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import javax.sql.DataSource;
import java.io.FileNotFoundException;
@Configuration
@EnableBatchProcessing
public class CsvBatchJobConfig {
// 用來讀取數據
@Bean
public ItemReader<User> reader() {
// FlatFileItemReader是一個用來加載文件的itemReader
FlatFileItemReader<User> reader = new FlatFileItemReader<>();
// 跳過第一行的標題
reader.setLinesToSkip(1);
// 設置csv的位置
reader.setResource(new ClassPathResource("data.csv"));
// 設置每一行的數據信息
reader.setLineMapper(new DefaultLineMapper<User>(){{
setLineTokenizer(new DelimitedLineTokenizer(){{
// 配置了四行文件
setNames(new String[]{"userName","sex","age", "address"});
// 配置列於列之間的間隔符,會通過間隔符對每一行進行切分
setDelimiter("\t");
}});
// 設置要映射的實體類屬性
setFieldSetMapper(new BeanWrapperFieldSetMapper<User>(){{
setTargetType(User.class);
}});
}});
return reader;
}
// 用來處理數據
@Bean
public ItemProcessor<User,User> processor(){
// 使用我們自定義的ItemProcessor的實現CsvItemProcessor
CsvItemProcessor processor = new CsvItemProcessor();
// 爲processor指定校驗器爲CsvBeanValidator()
processor.setValidator(csvBeanValidator());
return processor;
}
// 用來輸出數據
@Bean
public ItemWriter<User> writer(@Qualifier("dataSource") DataSource dataSource) {
// 通過Jdbc寫入到數據庫中
JdbcBatchItemWriter writer = new JdbcBatchItemWriter();
writer.setDataSource(dataSource);
// setItemSqlParameterSourceProvider 表示將實體類中的屬性和佔位符一一映射
writer.setItemSqlParameterSourceProvider(
new BeanPropertyItemSqlParameterSourceProvider<>());
// 設置要執行批處理的SQL語句。其中佔位符的寫法是 `:屬性名`
writer.setSql("insert into sys_user(user_name, sex, age, address, status, create_time) " +
"values(:userName, :sex, :age, :address, :status, :createTime)");
return writer;
}
// 配置一個Step
@Bean
public Step csvStep(
StepBuilderFactory stepBuilderFactory,
ItemReader<User> reader,
ItemProcessor<User,User> processor,
ItemWriter<User> writer) {
return stepBuilderFactory.get("csvStep")
// 批處理每次提交5條數據
.<User, User>chunk(5)
// 給step綁定 reader
.reader(reader)
// 給step綁定 processor
.processor(processor)
// 給step綁定 writer
.writer(writer)
.faultTolerant()
// 設定一個我們允許的這個step可以跳過的異常數量,假如我們設定爲3,則當這個step運行時,只要出現的異常數目不超過3,整個step都不會fail。注意,若不設定skipLimit,則其默認值是0
.skipLimit(3)
// 指定我們可以跳過的異常,因爲有些異常的出現,我們是可以忽略的
.skip(Exception.class)
// 出現這個異常我們不想跳過,因此這種異常出現一次時,計數器就會加一,直到達到上限
.noSkip(FileNotFoundException.class)
.build();
}
/**
* 配置一個要執行的Job任務, 包含一個或多個Step
*/
@Bean
public Job csvJob(JobBuilderFactory jobBuilderFactory, Step step) {
// 爲 job 起名爲 csvJob
return jobBuilderFactory.get("csvJob")
.start(step)
// .next(step)
.listener(listener())
.build();
}
@Bean
public Validator<User> csvBeanValidator(){
return new CsvBeanValidator<>();
}
@Bean
public JobExecutionListener listener() {
return new JobCompletionListener();
}
}
自定義校驗器
import org.springframework.batch.item.validator.ValidatingItemProcessor;
import java.util.Date;
public class CsvItemProcessor extends ValidatingItemProcessor<User> {
@Override
public User process(User item) {
super.process(item);
// 對數據進行簡單的處理,若性別爲男,則數據轉換爲1,其餘轉換爲2
if (item.getSex().equals("男")) {
item.setSex("1");
} else {
item.setSex("2");
}
// 設置默認值
item.setStatus((byte) 1);
item.setCreateTime(new Date());
return item;
}
}
數據校驗類
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.batch.item.validator.Validator;
import org.springframework.beans.factory.InitializingBean;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class CsvBeanValidator<T> implements Validator<T>, InitializingBean {
private javax.validation.Validator validator;
@Override
public void validate(T value) throws ValidationException {
// 使用Validator的validate方法校驗數據
Set<ConstraintViolation<T>> constraintViolations =
validator.validate(value);
if (constraintViolations.size() > 0) {
StringBuilder message = new StringBuilder();
for (ConstraintViolation<T> constraintViolation : constraintViolations) {
message.append(constraintViolation.getMessage() + "\n");
}
throw new ValidationException(message.toString());
}
}
/**
* 使用JSR-303的Validator來校驗我們的數據,在此進行JSR-303的Validator的初始化
*/
@Override
public void afterPropertiesSet() throws Exception {
ValidatorFactory validatorFactory =
Validation.buildDefaultValidatorFactory();
validator = validatorFactory.usingContext().getValidator();
}
}
批處理監聽類
public class JobCompletionListener extends JobExecutionListenerSupport {
// 用於批處理開始前執行
@Override
public void beforeJob(JobExecution jobExecution) {
System.out.println(String.format("任務id=%s開始於%s", jobExecution.getJobId(), jobExecution.getStartTime()));
}
// 用於批處理開始後執行
@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
System.out.println(String.format("任務id=%s結束於%s", jobExecution.getJobId(), jobExecution.getEndTime()));
} else {
System.out.println(String.format("任務id=%s執行異常狀態=%s", jobExecution.getJobId(), jobExecution.getStatus()));
}
}
}
執行批處理接口
@RestController
public class JobController {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job job;
@GetMapping("/doJob")
public void doJob() {
try {
// 同一個job執行多次, 由於job定義一樣, 則無法區分jobInstance, 所以增加jobParameter用於區分
JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();
jobParametersBuilder.addDate("jobDate", new Date());
// 執行一個批處理任務
jobLauncher.run(job, jobParametersBuilder.toJobParameters());
} catch (Exception e) {
e.printStackTrace();
}
}
}
調用接口執行批處理: http://localhost:8080/doJob
數據庫建表語句
CREATE TABLE `sys_user` (
`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`user_name` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '姓名',
`sex` tinyint(1) DEFAULT NULL COMMENT '性別',
`age` int(5) DEFAULT NULL COMMENT '年齡',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`status` tinyint(4) DEFAULT NULL COMMENT '狀態',
`create_time` datetime DEFAULT NULL COMMENT '創建時間',
`update_time` datetime DEFAULT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;