目录

一、Spring Batch简介

二、业务场景

三、基础知识

3.1 基础架构

3.2 核心接口

四、代码示例

4.1 引入POM依赖

4.2 读取和写入实体类

4.2.1 文件读取实体类

4.2.2 文件写入实体类

4.2.3 二者区别

4.3 数据处理Processor

4.4 配置Job

4.4.1 新建配置类

4.4.2 配置ItemWriter

4.4.3 配置ItemReader

4.4.4 配置Processor

4.4.5 配置Step

4.4.6 配置Job

4.5 调用Job

五、接口测试

5.1 接口测试

5.2 通过postman测试

5.3 查看控制台日志

5.4 查看数据库记录

六、问题记录

七、示例代码地址


前言:项目中需要使用批量文件导入的功能,调研了Spring Batch对应的用法。本文介绍了Spring Batch的基本概念和示例用法,读取平面文件,对数据进行处理后保存至数据库中。

一、Spring Batch简介

Spring Batch是一个轻量级、全面的批处理框架,旨在支持开发健壮的批处理成用程序。Spring Batch建在Spring Framework特性的基础上,使开发人员能够轻松使用更高级的企业服务。

与调度框架不同,Spring Batch不是一个独立的调度程序。但Spring Batch旨在与调度程序结合使用,以实现批处理作业的处理和调度。通过结合使用Spring Batch和企业调度程序,可以创建功能强大的批处理系统,可以可靠地处理大量的数据和业务操作,提高系统的可靠性和稳定性。

二、业务场景

在当前的系统开发中存在一种场景:

springbatch Reader 参数 springbatch读取数据库_实体类

SpringBatch支持以下业务场景:

定期提交批处理、并发处理作业、分阶段的企业消息驱动处理、大规模并行批处理、失败后手动或计划重启、相关步骤的顺序处理、部分处理(例如在回滚时跳过记录)和整批交易(适用于批量较小的情况)等场景。与调度程序结合使用,而不是替代调度程序。

三、基础知识

3.1 基础架构

springbatch Reader 参数 springbatch读取数据库_实体类_02

名称

作用

JobRepository

为所有的原型(Job、Joblnstance、Step)提供持久化的支持

JobLauncher

表示一个简单的接口,用于启动一个Job给定的集合JobParameters

Job

封装了整个批处理处理过程的实体

Step

一个域对象,封装了批处理作业的一个独立的顺序阶段

3.2 核心接口

  • ItemReader:表示一个Step的输入,一次批处理或一块条目。
  • ltemProcessor:表示一个条目的业务处理。
  • Itemwriter:表示一个Step的输出,一次批处理或一块条目。

大体流程为输入-->数据加工-->输出,一个Job包含一个或多个Step及处理流程,一个Step

通常涵盖 ltemReader、ltemProcessor和ltemWriter。

四、代码示例

以批处理文件,将数据处理后,写入数据库,以Student学生信息为例:

4.1 引入POM依赖


<!--spring batch 批处理-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-batch</artifactId>
   <version>2.3.12.RELEASE</version>
</dependency>


4.2 读取和写入实体类

4.2.1 文件读取实体类

在文件读取时,将字段值映射到实体类StudentFile中


@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentFile {
    /**
     * id 20
     */
    private String id;
    /**
     * 姓名  50
     */
    private String name;
    /**
     * 年龄 5
     */
    private int age;
    /**
     * 性别 5
     */
    private String sex;
    /**
     * 地址 100
     */
    private String address;

    /**
     * 联系电话 20
     */
    private String phone;
}


4.2.2 文件写入实体类

在数据处理完成之后,进行数据库写入时的实体类StudentEntity


@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentEntity {
    private static final long serialVersionUID = 1L;
    /**
     * id 20
     */
    private String id;
    /**
     * 姓名 50
     */
    private String name;
    /**
     * 年龄 5
     */
    private int age;
    /**
     * 性别 5
     */
    private String sex;
    /**
     * 地址 100
     */
    private String address;
    /**
     * 联系电话 20
     */
    private String phone;
    /**
     * 创建时间 20
     */
    private String createTime;
}


4.2.3 二者区别

根据ItemProcessor<I, O>的接口定义,文件数据处理时,映射泛型为I的实体,处理后,返回一个O类型的实体供写入操作。写入实体类与读取实体类也可以相同,具体看业务需求。


package org.springframework.batch.item;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public interface ItemProcessor<I, O> {
    @Nullable
    O process(@NonNull I var1) throws Exception;
}


4.3 数据处理Processor

数据处理类,将读取的数据进行转换、筛选等操作。


@Slf4j
@Component
public class StudentFileProcessor implements ItemProcessor<StudentFile, StudentEntity> {

    private JobParameters jobParameters;

    @BeforeStep
    public void beforeStep(final StepExecution stepExecution) {
        jobParameters = stepExecution.getJobParameters();
    }

    //通过jobParameters接收参数
    public String getHandleDate() {
        return jobParameters.getString("createTime");
    }

    @Override
    public StudentEntity process(StudentFile student) throws Exception {
        if (Objects.isNull(student)) {
            //跳过空行
            return null;
        }

        //数据处理 此处简单赋值
        StudentEntity studentEntity = new StudentEntity();
        studentEntity.setId(UUID.randomUUID().toString().replace("-","").substring(0,20));
        studentEntity.setName(student.getName());
        studentEntity.setAge(student.getAge());
        studentEntity.setSex(student.getSex());
        studentEntity.setAddress(student.getAddress());
        studentEntity.setPhone(student.getPhone());

        studentEntity.setCreateTime(getHandleDate());

        return studentEntity;
    }
}


4.4 配置Job

4.4.1 新建配置类

新建配置类StudentFileStepConfig,配置Job、Step、ItemReader、ItemWriter等。


@Slf4j
@Configuration
public class StudentFileStepConfig {
    @Resource
    private JobBuilderFactory jobBuilderFactory;
    @Resource
    private StepBuilderFactory stepBuilderFactory;
    @Resource
    private StudentFileProcessor studentFileProcessor;
    @Resource
    private DataSource dataSource;
}


以下小节中的配置均在此配置类中。

4.4.2 配置ItemWriter

配置ItemWriter,通过JDBC将数据写入到数据库中。


/**
 * 配置ItemWriter
 *
 * @return ItemWriter
 */
@Bean
public JdbcBatchItemWriter studentJdbcBatchItemWriter() {
    //便用JdbcBatchItemWriter通过JDBC将数据写到数据库中
    JdbcBatchItemWriter writer = new JdbcBatchItemWriter();

    //设置数据源
    writer.setDataSource(dataSource);
    //设置插入更新的SQL,注意占位符的写法 :"属性名"
    writer.setSql("insert into student(id, name, age, sex, address, phone, createTime) values (:id, :name, :age, :sex, :address, :phone, :createTime)");
    writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider());
    return writer;
}


4.4.3 配置ItemReader

1、配置ItemReader,读取文件时,通常以FlatFileItemReader实现。


/**
 * 配置ItemReader
 *
 * @return ItemReader
 */
@Bean
@StepScope
public FlatFileItemReader<StudentFile> studentFileReader() {
    //FlatFileItemReader 是一个加载普通文件的 ItemReader
    FlatFileItemReader<StudentFile> reader = new FlatFileItemReader<>();
    //第一行为标题跳过,若没有标题,注释掉即可
    // reader.setLinesToSkip(1);
    String fileName = "studentinfo";
    String filePath = "C:\\test";
    String fullName = filePath + File.separator + fileName;
    FileSystemResource fileResource = new FileSystemResource(fullName);
    reader.setEncoding("GBK");
    reader.setResource(fileResource);
    reader.setLineMapper(new DefaultLineMapper<StudentFile>() {
        {
            setLineTokenizer(
                    //固定长度分割
                    studentFixedLengthTokenizer()
            );
            setFieldSetMapper(new BeanWrapperFieldSetMapper() {
                {
                    setTargetType(StudentFile.class);
                }
            });
        }
    });
    return reader;
}


2、配置tokenizer,文件格式为定长,使用FixedLengthTokenizer 。

(1)Range的范围截取需要按照文件的格式,从1开始计算,截取相应的值。Range数组顺序与Names的列一一对应。

(2)此处没有读取文件的全部列,只是截取了部分,所以需要设置tokenizer.setStrict(false)。


/**
 * 定长分割,文件一行长度固定,每个字段固定长度,长度不足的,数字前补0,字符串后补空格
 *
 * @return
 */
@Bean
public FixedLengthTokenizer studentFixedLengthTokenizer() {
    FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
    //设置文件中一共有多少列,分别是哪些列
    //需要读取的列
    tokenizer.setNames("name", "age", "sex", "address", "phone");
    tokenizer.setColumns(
            // new Range(1, 20),//id 20
            new Range(21, 70),//姓名 50
            new Range(71, 75),//年龄 5
            new Range(76, 80),//性别 5
            new Range(81, 180),//地址 100
            new Range(181, 200)//联系电话 20
    );

    /*
      是否严格匹配。设置读取一行的部分列时,必须设置为false。
      例如一行里有30个字段,但是只读取其中10个字段,其他字段忽略不需要读取。
      可以容忍标记比较少的行,并用空列填充,标记较多的行将被简单地截断。默认为true
    */
    tokenizer.setStrict(false);
    return tokenizer;
}


3、若文件格式为分隔符分割,则配置tokenizer时,需要使用DelimitedLineTokenizer ,并将全部的列一一列出。

4、按照文件格式的需要与第2步二选一即可。


/**
 * 分割符分割,文件一行长度不固定,字段之间使用指定的分隔符进行分割
 *
 * @return
 */
@Bean
public DelimitedLineTokenizer studentDelimitedLineTokenizer() {
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    tokenizer.setNames("id", "name", "age", "sex", "address", "phone");
    tokenizer.setDelimiter(" ");
    return tokenizer;
}


4.4.4 配置Processor

在4.3数据处理中已经单独实现,此处通过注入的形式配置。


@Resource
private StudentFileProcessor studentFileProcessor;


4.4.5 配置Step


/**
 * 配置Step
 *
 * @return Step
 */
@Bean
public Step studentStep() {
    return stepBuilderFactory.get("studentStep")//通过get获取一个stepBuilder,参数为step的name
            .<StudentFile, StudentEntity>chunk(5000)//配置chunk,每读取5000条,就执行一次writer操作
            .reader(studentFileReader())//配置reader
            .processor(studentFileProcessor)//配置processor,进行数据的预处理
            .writer(studentJdbcBatchItemWriter())//配置writer
            .taskExecutor(new SimpleAsyncTaskExecutor())
            .build();
}


4.4.6 配置Job

一个Job可以包含多个Step。至此,一个完整的Job配置完成。


/**
 * 配置Job
 *
 * @return Job
 */
@Bean
public Job studentBatchJob() {
    return jobBuilderFactory.get("studentJob")//通过 jobBuilderFactory 构建一个 Job. get参数为 Job 的name
            .start(studentStep())//配置该Job的Step
            //.next(studentStep2())//多个Step时,通过next()进行配置
            .build();
}


4.5 调用Job

在Service的实现类中,使用jobLauncher.run(),通过Job的名称“studentBatchJob”调用指定的Job。


@Slf4j
@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private JobLauncher jobLauncher;

    @Resource
    private Job studentBatchJob;

    @Override
    public String importStudentFile() {
        log.info("导入学生信息开始...");
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            String createTime = sdf.format(new Date());

            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("createTime", createTime)
                    .toJobParameters();

            jobLauncher.run(studentBatchJob, jobParameters);
        } catch (Exception e) {
          log.error("导入学生信息异常", e);
        }
        log.info("导入学生信息结束...");
        return "success";
    }
}


五、接口测试

5.1 接口测试


@Controller
@RequestMapping("/student")
public class StudentController {

    @Resource
    private StudentService studentService;

    @PostMapping("import")
    @ResponseBody
    public String importStudentFile() {
        return studentService.importStudentFile();
    }

}


5.2 通过postman测试

springbatch Reader 参数 springbatch读取数据库_batch_03

5.3 查看控制台日志

springbatch Reader 参数 springbatch读取数据库_batch_04

5.4 查看数据库记录

springbatch Reader 参数 springbatch读取数据库_batch_05

六、问题记录

1、在processor中,如果要使用调用时传进来的参数,例如“createTime”,需要通过JobParameters 来传值和获取。详细见4.3数据处理Processor和4.5调用Job

2、如果文件中出现空行,在processor中进行判断,如果读取的对象属性全为null,直接return null即可。

3、在ItemWriter中,注意sql的写法,以及参数的占位符,需要用“:属性名”。

4、在配置tokenizer时,文件有定长和分隔符的区分,如果是定长文件,需要计算每个列的区间Range,字段较多时,可以使用Excel表格辅助计算。

5、定长文件的tokenizer,如果字段较多,但是只需要读取其中部分,例如一行里有30个字段,但是只读取其中10个字段,其他字段忽略不需要读取,需要设置 tokenizer.setStrict(false)。(一个小坑,最后通过官网的api解决)