史上最烂 spring batch 原理分析

springboot + batch 批处理

springboot + spring batch 批处理


spring batch 简介

spring batch 是 spring 家族里的批处理框架,其主要功能是 “读-处理-写”。

spring 官网是这样描述 batch 的:

一个轻量级、综合的批处理框架,用来开发企业系统中至关重要的批处理应用程序。

spring batch 提供了处理大容量数据记录必不可少的可重用方法,包括 日志/跟踪、事务管理、作业处理信息统计、作业重启、跳过和资源管理。它还提供了更高级的技术服务和特性,通过优化和分区技术来支持极大容量和高性能的批处理作业。无论是简单亦或是复杂的大容量批处理作业都可以以高度可扩展的方式利用此框架来处理。

spring batch 特性

  • 事务管理
  • 基于块的处理
  • 声明式 I/O
  • 启动、停止、重启
  • 重试、跳过
  • 基于 web 的管理接口

spring batch 核心组件

  • JobRepository: job 的注册/存储器。
  • JobLauncher: job 启动器。
  • Job: 实际批处理任务,每一个 Job 由至少一个 Step 组成。
  • Step: Job 的执行步骤,每一个 Step 由 ItemReader、ItemProcessor、ItemWriter 组成。
  • ItemReader: 数据读取器。
  • ItemProcessor: 数据处理器。
  • ItemWriter: 数据写入器。

spring batch

注:图片来自 “书栈网 spring batch 参考文档中文版”

springboot + spring batch demo

版本:

  • springboot: 2.5.6
  • spring batch: 4.3.3
  • mybatis-plus: 3.4.3.4

pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

application.yml:

spring:
  batch:
    job:
      enabled: false   # 手动执行任务
    jdbc:
      initialize-schema: always  # 服务启动时自动初始化 batch 相关数据库

启动类上加上 @EnableBatchProcessing 注解,开启批处理。

demo 中共包含三个 job,分别是:

  • mysqlToFileJob: 读取 mysql - 处理 - 写入文件。
  • fileToMysqlJob: 读取文件 - 处理 - 写入 mysql。
  • mongoToFileJob: 读取 mongo - 处理 - 写入文件。

TestPo:

测试用的实体类。

@Data
@TableName("test")  // mybatis-plus 中的注解 值为数据库表名
public class TestPo implements Serializable {

    private static final long serialVersionUID = 7257621866423434421L;

    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private Integer age;
    private Integer gender;
    private Integer deleteFlag;
    private Timestamp createTime;
    private Timestamp updateTime;
    @TableField(exist = false)
    private String startTime;
    @TableField(exist = false)
    private String endTime;
}

TestPo 对应的 Mapper 和 xml:

// BaseMapper 为 maybatis-plus mapper 接口 提供了基本的 CURD 方法
@Repository
public interface TestMapper extends BaseMapper<TestPo> {

    // 根据条件获取列表
    List<TestPo> listTest(TestPo po);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xgllhz.batch.mapper.TestMapper">

    <select id="listTest" resultType="TestPo">
        select
            test.id,
            test.username,
            test.age,
            test.gender,
            test.delete_flag,
            test.create_time,
            test.update_time
        from test test
        where test.delete_flag = 0

        <if test="startTime != null and startTime != '' and endTime != null and endTime != ''">
            and test.create_time between #{startTime} and #{endTime}
        </if>
        order by test.create_time
        limit #{_pagesize} OFFSET #{_skiprows}
    </select>

</mapper>

BatchConfig:

主要用来配置 JobRepository、JobLauncher。

@Configuration
public class BatchConfig {
    private static final Logger logger = LogManager.getLogger(BatchConfig.class);
    // 注入的是 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
    // 可自定义 bean 如设置核心线程数、最大线程数、队列大小等
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
    public BatchConfig(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        this.threadPoolTaskExecutor = threadPoolTaskExecutor;
    }

    // job 注册/存储器 用来注册 job 以及存储 job 相关信息
    @Bean
    public JobRepository myJobRepository(@Qualifier("dataSource") DataSource dataSource,
                                       PlatformTransactionManager transactionManager) throws Exception {
        logger.info("init job repository");
        JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean();

        // 设置存储器数据源类型 默认为项目数据源
        factoryBean.setDatabaseType("mysql");
        logger.info("job repository databaseType is mysql");

        factoryBean.setDataSource(dataSource);
        factoryBean.setTransactionManager(transactionManager);
        return factoryBean.getObject();
    }

    // job 启动器 jobLauncher.run() 方法启动 job
    @Bean
    public JobLauncher myJobLauncher(@Qualifier("dataSource") DataSource dataSource,
                                   PlatformTransactionManager transactionManager) throws Exception {
        logger.info("init job launcher");
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(myJobRepository(dataSource, transactionManager));

        // 设置任务执行方式 默认为同步执行
        jobLauncher.setTaskExecutor(threadPoolTaskExecutor);
        logger.info("job launcher executor is threadPoolTaskExecutor");

        return jobLauncher;
    }
    
    /**
     * 重试机制
     * 来自 spring 框架提供的 retry 模块
     *
     * 可构建各种重试策略 如 重试次数、重试超时时间、无限重试、退避策略等
     *      如果这些都不满足你 那你可以自定义重试策略
     *      退避策略指每次重试之后的等待时间(线程睡眠时间 以等待资源释放 如 io 资源等)
     *      可给重试机制添加自定义异常 监听器等
     *
     * 退避策略包括以下几个:
     *      指数退避策略: 按照某种规则计算每次重试之后的等待时间
     *      指数随机数退避策略: 在指数退避策略的计算中加入随机数
     *      固定退避策略: 即每次重试之后的等待时间固定
     *      统一随机数退避策略: 即每次重试之后的等待时间随机于指定范围内
     *      无退避策略: 即每次重试之后不需要等待 立即重试
     *      如果这些都不满足你 那你可以自定义退避策略
     *
     * 重试 5 次 每次重试之后不等待
     * return new RetryTemplateBuilder()
     *                 .maxAttempts(5)
     *                 .build();
     *
     * 重试 10 秒 每次重试之后不等待
     * return new RetryTemplateBuilder()
     *                 .withinMillis(10000)
     *                 .build();
     *
     * 无限重试 每次重试之后不等待
     * return new RetryTemplateBuilder()
     *                 .infiniteRetry()
     *                 .build();
     *
     * 重试 5 次 每次重试之后等待 5 秒
     * return new RetryTemplateBuilder()
     *                 .maxAttempts(5)
     *                 .fixedBackoff(5000)
     *                 .build();
     *
     * 重试 5 次 每次重试之后等待时间计算公式为
     *      currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)
     *      第一次等待 Math.min(1000 * 2^0, 20 * 1000) = 1000 ms = 1 s
     *      第二次等待 Math.min(1000 * 2^1, 20 * 1000) = 2000 ms = 2 s
     *      第三次等待 Math.min(1000 * 2^2, 20 * 1000) = 4000 ms = 4 s
     *      第四次等待 Math.min(1000 * 2^3, 20 * 1000) = 8000 ms = 8 s
     * return new RetryTemplateBuilder()
     *                 .maxAttempts(5)
     *                 .exponentialBackoff(1000, 2, 20 * 1000)
     *                 .build();
     * @return
     */
    @Bean
    public RetryTemplate retryTemplate() {
        return new RetryTemplateBuilder()
                .maxAttempts(5)
                .exponentialBackoff(1000, 2, 20 * 1000)
                .build();

    }
}

注:重试机制一般用在容易收外界因素影响的操作中,如网络请求(http 请求、下载/上传 等)等中。其实际使用小 demo 如下:

// 重试机制的执行有四种实现 
// 具体见 org.springframework.retry.RetryOperations 接口中的四种声明
retryTemplate.execute(context -> {
    // todo 业务逻辑 如下载、上传等
    return null;
});

JobReader:

提供不同数据来源的数据读取器的 bean。

数据读取器,数据来源可以是数据库(mysql、mongo、oracle 等)、文件等。每一个读取器必须实现 ItemReader< T> 接口,其中 T 表示读取到的数据类型。该接口中只有一个 T read() 方法,其作用是从数据源读取数据供 processor 处理,且每次只返回一条。

ItemReader 接口的实现类中都维护了一个集合,用来存储读取到的数据。数据库相关的实现类中维护了分页相关的变量,页面大小一般由 step 中的 chunkSize 决定,mybatis 可以通过 pageSize 设置,mongo 可以通过 limit 设置,文件相关的实现类中维护了当前行数,用来实现类似于分页的功能。T read() 只有在第一次调用时是从数据源获取数据,之后的每次都是从内存中(其实现类维护的集合中)获取。当有一条数据解析失败时就会抛出 ParseException 异常。

spring batch 已经为我们实现了多种读取器,诸如 文件读取器、jpa 游标读取器、jpa 分页读取器、jdbc 游标读取器、jdbc 分页读取器、mybatis 游标读取器、mybatis 分页读取器、mongo 读取器、kafka 读取器等等,只需要通过简单的参数设置就可使用。

@Configuration
public class JobReader {

    private static final Logger logger = LogManager.getLogger(JobReader.class);

    private final SqlSessionFactory sqlSessionFactory;
    
    private final MongoTemplate mongoTemplate;
    
    public JobReader(SqlSessionFactory sqlSessionFactory,
                     MongoTemplate mongoTemplate) {
        this.sqlSessionFactory = sqlSessionFactory;
        this.mongoTemplate = mongoTemplate;
    }

    /**
     * mysql 数据读取器
     * 此处构建一个 mybatis 分页读取器
     * @param startTime
     * @param endTime
     * @return
     */
    // mysql 数据读取器 此处构建一个 mybatis 分页读取器
    @Bean
    @JobScope
    public ItemReader<TestPo> mysqlReader(@Value("#{jobParameters['startTime']}") String startTime,
                                          @Value("#{jobParameters['endTime']}") String endTime) {
        logger.info("enter JobReader.mysqlReader()");

        Map<String, Object> map = new HashMap<>();
        map.put("startTime", startTime);
        map.put("endTime", endTime);

        return new MyBatisPagingItemReaderBuilder<TestPo>()
                .sqlSessionFactory(sqlSessionFactory)
                .queryId("org.xgllhz.batch.mapper.TestMapper.listTest")
                .parameterValues(map)   // 参数
                .pageSize(3)   // 使用分页时需要在 sql 中加入分页语句 limit #{_pagesize} OFFSET #{_skiprows}
                .build();
    }

    /**
     * 文件数据读取器
     * 
     * 单个文件读取
     * 返回值类型必须为 FlatFileItemReader<T> 否则会抛出异常
     * throw new ReaderNotOpenException("Reader must be open before it can be read.");
     *
     * FlatFileItemReader 最主要的组件是 resource 和 LineMapper
     *      resource 表示被读取的文件路径
     *      LineMapper 将读取到的每一行映射成实体对象 其又有 LineTokenizer 和 FieldSetMapper 组成
     *          LineTokenizer 为分词器及分隔符 将每一行按照分隔符分割成多个 fieldSet
     *          FieldSetMapper 将每一行的 fieldSet 设置到实体对象中
     *
     * @return
     */
    @Bean
    @JobScope
    public FlatFileItemReader<TestPo> fileReader() {
        logger.info("enter JobReader.fileReader()");

        return new FlatFileItemReaderBuilder<TestPo>()
                .name("testPoFileReader")   // 读取器名称
                .encoding(StandardCharsets.UTF_8.name())   // 字符集
                .resource(new FileSystemResource("D:\\project\\tmp\\test.txt"))   // 被读取文件路径
                .linesToSkip(1)   // 读取时跳过的行数
                .delimited()   // 分割器示例
                .delimiter(",")   // 分隔符
                .names("username", "age", "gender", "createTime")   // 要读取的列名称
                .fieldSetMapper(fieldSet -> {   // 映射到实体对象
                    TestPo po = new TestPo();
                    po.setUsername(fieldSet.readString(0));
                    po.setAge(fieldSet.readInt(1));
                    po.setGender(fieldSet.readInt(2));
                    return po;
                })
                .build();
    }

    /**
     * mongo 数据读取器
     *
     * MongoItemReader 的分页是使用 skip 性能较差
     * 建议使用文档的 _id 字段自增属性结合 limit 分页
     * 具体见 {@link org.xgllhz.batch.job.step.item.MyMongoItemReader}
     *      return new MyMongoItemReader(mongoTemplate,
     *                 "collection",
     *                 new Query(Criteria.where("create_time").gte(startTime).lt(endTime)).limit(3));
     *
     * @param startTime
     * @param endTime
     * @return
     */
    @Bean
    @JobScope
    public ItemReader<Map> mongoReader(@Value("#{jobParameters['startTime']}") Long startTime,
                                            @Value("#{jobParameters['endTime']}") Long endTime) {
        logger.info("enter JobReader.mongoReader()");

        return new MongoItemReaderBuilder<Map>()
                .name("testPoMongoReader")
                .template(mongoTemplate)
                .collection("collection")
                .targetType(Map.class)
                .query(new Query(Criteria.where("create_time").gte(startTime).lt(endTime)))
                .pageSize(3)
                .sorts(Collections.singletonMap("create_time", Sort.Direction.ASC))
                .build();
    }
}

MyMongoItemReader:

自定义的 mongo 分页读取器。

public class MyMongoItemReader implements ItemReader<Map> {

    private static final Logger logger = LogManager.getLogger(MyMongoItemReader.class);

    private final MongoTemplate mongoTemplate;
    private final String collection;
    private final Query query;
    private List<Map> list;
    private String maxId;

    public MyMongoItemReader(MongoTemplate mongoTemplate,
                             String collection,
                             Query query) {
        this.mongoTemplate = mongoTemplate;
        this.collection = collection;
        this.query = query;
    }

    @Override
    public Map read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        if (list != null && !list.isEmpty()) {
            return list.remove(0);
        }

        Query query1 = Query.of(query);
        if (StringUtils.isNotBlank(maxId)) {
            query1.addCriteria(new Criteria("_id").gt(new ObjectId(maxId)));
        }

        list = mongoTemplate.find(query1, Map.class, collection);

        if (list.isEmpty()) {
            maxId = null;
            return null;
        }

        maxId = list.get(list.size() - 1).get("_id").toString();

        return list.remove(0);
    }
}

JobProcessor:

提供各种数据处理器的 bean。

数据处理器,负责处理 reader 读取到的数据,一般根据具体业务自定义实现。

每一个处理器都必须实现 ItemProcessor<I, O> 接口,其中 I 表示要处理的数据类型 即 reader 返回的数据类型,O 表示处理后要返回的数据类型。

该接口中只有一个 O process(I item) 方法 负责处理 I 并输出 O。

每次处理的 item 不能为 null,若为 null 则此过程将停止,如果在处理过程中发生异常 则 job 结束。

spring batch 同样为我们实现了多种数据处理器,但实际上我们需要根据具体业务自行实现。

@Configuration
public class JobProcessor {

    private static final Logger logger = LogManager.getLogger(JobProcessor.class);

    /**
     * mysql to file 数据处理器
     * @return
     */
    @Bean
    @JobScope
    public ItemProcessor<TestPo, TestPo> mysqlToFileProcessor() {
        logger.info("enter JobProcessor.mysqlToFileProcessor()");
        return item -> {
            logger.info(item.toString());
            return item;
        };
    }

    /**
     * file to mysql 数据处理器
     * @return
     */
    @Bean
    @JobScope
    public ItemProcessor<TestPo, TestPo> fileToMysqlProcessor() {
        logger.info("enter JobProcessor.fileToMysqlProcessor()");
        return item -> {
            logger.info(item.toString());
            return item;
        };
    }

    /**
     * mongo to file 数据处理器
     * @return
     */
    @Bean
    @JobScope
    public ItemProcessor<Map, TestPo> mongoToFileProcessor() {
        logger.info("enter JobProcessor.mongoToFileProcessor()");
        return item -> {
            logger.info(item.toString());
            TestPo po = new TestPo();
            po.setUsername(item.get("username").toString());
            po.setAge(Integer.valueOf(item.get("age").toString()));
            po.setGender(Integer.valueOf(item.get("gender").toString()));
            po.setCreateTime(new Timestamp(Long.parseLong(item.get("create_time").toString())));
            return po;
        };
    }
}

JobWriter:

提供各种数据写入器的 bean。

数据写入器,写入对象可以是文件、数据库(mysql、oracle、mongo)等。每一写入器都必须实现 ItemWriter< T> 接口,T 表示要写入的数据类型。该接口中只有一个 void write(List<? extends T> items) 方法,负责将数据写入到目的地。

其中入参 items 的大小由 step 中的 chunkSize 决定,如:当要写入数据库时,processor 处理了 chunkSize 条数据后,将其交给 writer,writer 负责将这批数据一次性提交;当要写入文件时,processor 处理了 chunkSize 条数据后,将其交给 writer,writer 负责将这批数据先 append 到 StringBuilder,然后写入文件。

同样,spring batch 已经实现了许多写入器,如 文件写入器、jdbc 批量写入器、jpa 写入器、mongo 写入器、mybatis 写入器、kafka 写入器等等。

@Configuration
public class JobWriter {

    private static final Logger logger = LogManager.getLogger(JobWriter.class);

    private final SqlSessionFactory sqlSessionFactory;
    
    public final MongoTemplate mongoTemplate;
    
    public JobWriter(SqlSessionFactory sqlSessionFactory,
                     MongoTemplate mongoTemplate) {
        this.sqlSessionFactory = sqlSessionFactory;
        this.mongoTemplate = mongoTemplate;
    }

    /**
     * 数据文件写入器
     * 此处构建一个单个文件写入器
     * 返回类型必须为 FlatFileItemWriter<T> 否则会抛出异常
     * throw new WriterNotOpenException("Writer must be open before it can be written to");
     * @return
     */
    @Bean
    @JobScope
    public FlatFileItemWriter<TestPo> fileWriter() {
        logger.info("enter JobWriter.fileWriter()");

        return new FlatFileItemWriterBuilder<TestPo>()
                .name("testPoFileWriter")   // 写入器名称
                .encoding(StandardCharsets.UTF_8.name())
                .resource(new FileSystemResource("D:\\project\\tmp\\test.txt"))   // 目的文件
                .delimited()   // 创建分割器实例
                .delimiter(",")   // 声明分隔符
                .names("username", "age", "gender", "createTime")   // 写入数据字段
                .shouldDeleteIfExists(true)   // 目的文件若已存在则删除 默认为 true
                .transactional(true)   // 设置为 true 标识数据先放入 BufferedWriter 等这一批处理完在一起写入 默认为 true
                .headerCallback(writer -> writer.append("username,age,gender,createTime"))   // 文件头
                .build();
    }


    /**
     * mysql 数据写入器
     * @return
     */
    @Bean
    @JobScope
    public ItemWriter<TestPo> mysqlWriter() {
        logger.info("enter JobWriter.mysqlWriter()");

        return new MyBatisBatchItemWriterBuilder<TestPo>()
                .sqlSessionFactory(sqlSessionFactory)
                .statementId("org.xgllhz.batch.mapper.TestMapper.insert")
                .build();
    }

    /**
     * mongo 数据写入器
     * @return
     */
    @Bean
    @JobScope
    public ItemWriter<TestPo> mongoWriter() {
        logger.info("enter JobWriter.mongoWriter()");

        return new MongoItemWriterBuilder<TestPo>()
                .template(mongoTemplate)
                .collection("collection")
                .build();
    }
}

JobTasklet:

JobTasklet 为自定义步骤内容。

自定义步骤内容,自定义步骤必须实现 Tasklet 接口,该接口中只有一个 RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) 方法,方法体为此 step 要执行的操作。

如果一个 job 的某个 step 不需要 读-处理-写 操作,则可以考虑通过实现 Tasklet 接口来自定义 step 执行内容。

@Configuration
public class JobTasklet {

    private static final Logger logger = LogManager.getLogger(JobTasklet.class);

    /**
     * mysqlToFileJob 自定义 step 执行内容
     * @return
     */
    @Bean
    @JobScope
    public Tasklet myTasklet() {
        return (contribution, chunkContext) -> {
            logger.info("execute myTasklet");

            // 不需要 读-处理-写 的操作 eg: 上传文件、删除文件

            boolean operateResult = true;
            if (operateResult) {
                return RepeatStatus.FINISHED;
            } else {
                return RepeatStatus.CONTINUABLE;
            }
        };
    }

}

JobListener:

job 执行监听器,负责监听 job 的执行情况,且可以在 job 开始执行和执行结束之后自定义操作。同样的也有 step 监听器。

// 篇幅原因 这里只列出 mysqlToFileJob 的 listener
@Configuration
public class MysqlToFileJobListener implements JobExecutionListener {

    private static final Logger logger = LogManager.getLogger(MysqlToFileJobListener.class);

    @Override
    public void beforeJob(JobExecution jobExecution) {
        logger.info("Start execution mysqlToFileJob. jobId = {}", jobExecution.getJobId());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
            logger.info("End execution mysqlToFileJob. jobId = {}", jobExecution.getJobId());
        } else {
            logger.error("Failed to execute mysqlToFileJob. jobId = {}", jobExecution.getJobId());
        }
    }
}

MysqlToFileJob:

将 listener、step、reader、processor、writer 组合成 job。

@Configuration
public class MysqlToFileJob {

    private static final Logger logger = LogManager.getLogger(MysqlToFileJob.class);
    // batch 提供的 job builder factory
    private final JobBuilderFactory jobBuilderFactory;   
    // batch 提供的 step builder factory
    private final StepBuilderFactory stepBuilderFactory;   
    // 自定义 job 监听器
    private final JobExecutionListener mysqlToFileJobListener;   
    // 自定义 mysql 读取器
    private final ItemReader<TestPo> mysqlReader;   
    // 自定义处理器
    private final ItemProcessor<TestPo, TestPo> mysqlToFileProcessor;   
    // 自定义文件写入器
    private final ItemWriter<TestPo> fileWriter;
    // 自定义 tasklet
    private final Tasklet myTasklet;

    public MysqlToFileJob(JobBuilderFactory jobBuilderFactory,
                          StepBuilderFactory stepBuilderFactory,
                          JobExecutionListener mysqlToFileJobListener,
                          ItemReader<TestPo> mysqlReader,
                          ItemProcessor<TestPo, TestPo> mysqlToFileProcessor,
                          ItemWriter<TestPo> fileWriter,
                          Tasklet myTasklet) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.mysqlToFileJobListener = mysqlToFileJobListener;
        this.mysqlReader = mysqlReader;
        this.mysqlToFileProcessor = mysqlToFileProcessor;
        this.fileWriter = fileWriter;
        this.myTasklet = myTasklet;
    }

    /**
     * 每一个 job 都由至少一个 step 组成
     * job 都从 start 开始
     * 当第一个 step 完成后进入下一个 step
     * @return
     */
    @Bean
    public Job mysqlToFile() {
        logger.info("init mysqlToFileJob");
        return jobBuilderFactory.get("mysqlToFileJob")
                .listener(mysqlToFileJobListener)
                .start(mysqlToFileFirstStep())
                .next(mysqlToFileSecondStep())
                .build();
    }

    /**
     * step 的一般流程是 读-处理-写
     *      也可以通过 .tasklet(Tasklet tasklet) 来设置自定义步骤
     *      也就是说一个 step 内有两种实现方式
     *          一、.reader().processor().writer()
     *          二、.tasklet()
     *
     * chunk 表示块大小 即这一个 step 要处理的数据量
     * 如 chunk(500) 表示:
     *      先从数据来源中读取 500 条放入内存中(调用 ItemReader 接口的 read() 方法)
     *      然后 processor 从内存中每次获取一条进行处理(也是调用 ItemReader 接口的 read() 方法
     *          第一次调用是从数据源读取 500 条放入内存且返回第一条 第二次是直接从内存中读取第二条)
     *          当处理完 500 条后 进入 writer
     *      最后 writer 将这 500 条数据写入目的地
     *
     * @return
     */
    @Bean
    public Step mysqlToFileFirstStep() {
        logger.info("init mysqlToFileFirstStep");
        return stepBuilderFactory.get("mysqlToFileFirstStep")
                .<TestPo, TestPo> chunk(3)
                .reader(mysqlReader)
                .processor(mysqlToFileProcessor)
                .writer(fileWriter)
                .build();
    }
    
    /**
     * 步骤二
     *
     * 当 job 的某一个步骤不需要 读-处理-写 时
     * 则这个步骤可以通过实现 Tasklet 接口来自定义要执行的内容
     *
     * @return
     */
    @Bean
    public Step mysqlToFileSecondStep() {
        logger.info("init mysqlToFileSecondStep");
        return stepBuilderFactory.get("mysqlToFileSecondStep")
                .tasklet(myTasklet)
                .build();
    }
}

TestController:

整个接口测试下。

@RestController
@RequestMapping("/api/test")
public class TestController {

    private static final Logger logger = LogManager.getLogger(TestController.class);
    // 注入自定义的 JobLauncher
    private final JobLauncher myJobLauncher;
    // 注入 mysqlToFile Job
    private final Job mysqlToFile;

    public TestController(JobLauncher myJobLauncher,
                          Job mysqlToFile) {
        this.myJobLauncher = myJobLauncher;
        this.mysqlToFile = mysqlToFile;
    }

    @RequestMapping("/mysqlToFile")
    @ResponseBody
    public APIResponse<Map<String, Object>> mysqlToFile(@RequestBody Map<String, Object> map) throws Exception {
        logger.info("enter TestController.mysqlToFile() params = {}", map.toString());

        // job 参数
        Map<String, JobParameter> maps = new HashMap<>(3);
        // code 非业务参数 只是为了方便测试
        maps.put("code", new JobParameter(RandomUtil.sixCode()));
        maps.put("startTime", new JobParameter(map.get("startTime").toString()));
        maps.put("endTime", new JobParameter(map.get("endTime").toString()));

        // 启动 job
        myJobLauncher.run(mysqlToFile, new JobParameters(maps));

        return new APIResponse<>();
    }
}

springboot + quartz + batch demo

spring batch 与 quartz 结合使用会有奇效哦!

关于 springboot + quartz 分布式定时任务可参考 springboot + quartz 分布式定时任务

MysqlToFileSchedule:

public class MysqlToFileSchedule extends QuartzJobBean {

    private static final Logger logger = LogManager.getLogger(MysqlToFileSchedule.class);

    private static JobLauncher jobLauncher;

    private static Job job;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("mysqlToFileSchedule threadName = {} scheduleName = {} currentTime = {}",
                Thread.currentThread().getName(), context.getJobDetail().getKey().getName(),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

        Map<String, JobParameter> map = new HashMap<>(3);
        map.put("code", new JobParameter(RandomUtil.sixCode()));
        map.put("startTime", new JobParameter("2021-11-18 00:00:00"));
        map.put("endTime", new JobParameter("2021-11-18 23:59:59"));

        try {
            jobLauncher.run(job, new JobParameters(map));
            logger.info("mysqlToFileSchedule submitted successfully");
        } catch (Exception e) {
            logger.error("mysqlToFileSchedule submitted failed");
            e.printStackTrace();
        }
    }

    public static void setJobLauncher(JobLauncher jobLauncher) {
        MysqlToFileSchedule.jobLauncher = jobLauncher;
    }

    public static void setJob(Job job) {
        MysqlToFileSchedule.job = job;
    }

}

QuartzConfig:

@Configuration
@EnableScheduling
public class QuartzConfig {

    private static final Logger logger = LogManager.getLogger(QuartzConfig.class);

    private final JobLauncher myJobLauncher;

    private final Job mysqlToFile;

    public QuartzConfig(JobLauncher myJobLauncher,
                        Job mysqlToFile) {
        this.myJobLauncher = myJobLauncher;
        this.mysqlToFile = mysqlToFile;
    }

    /**
     * mysqlToFile 定时任务
     * @return
     */
    @Bean
    public JobDetailFactoryBean mysqlToFileJobDetail() {
        JobDetailFactoryBean bean = new JobDetailFactoryBean();
        bean.setBeanName("mysqlToFileJobDetail");
        bean.setJobClass(MysqlToFileSchedule.class);

        MysqlToFileSchedule.setJobLauncher(myJobLauncher);
        MysqlToFileSchedule.setJob(mysqlToFile);
        return bean;
    }

    /**
     * mysqlToFile 定时任务触发器
     * @return
     */
    @Bean
    public CronTriggerFactoryBean mysqlToFileTrigger() {
        CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
        bean.setJobDetail(Objects.requireNonNull(mysqlToFileJobDetail().getObject()));
        bean.setCronExpression("0 40 * * * ?");
        return bean;
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        logger.info("init scheduledFactoryBean");
        SchedulerFactoryBean bean = new SchedulerFactoryBean();
        bean.setConfigLocation(new ClassPathResource("/config/quartz.properties"));
        bean.setTriggers(mysqlToFileTrigger().getObject());
        bean.setStartupDelay(10);
        return bean;
    }

}

@XGLLHZ-奔赴星空.mp3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值