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

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