SpringBoot集成Spring Batch批处理框架入门案例实战

Spring Batch 简介

spring batch是spring提供的一个数据处理框架。企业域中的许多应用程序需要批量处理才能在关键任务环境中执行业务操作。

Spring Batch是一个轻量级,全面的批处理框架,旨在开发对企业系统日常运营至关重要的强大批处理应用程序。 Spring Batch构建了人们期望的Spring Framework特性(生产力,基于POJO的开发方法和一般易用性),同时使开发人员可以在必要时轻松访问和利用更高级的企业服务。 Spring Batch不是一个schuedling的框架。

spring batch的一个总体的架构如下

Figure 2.1: Batch Stereotypes
在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;

 

Reference

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章