基于Springbatch的动态定时任务实现

1. 需求

对于部分需要批处理实现的任务来说,并不是每一个定时任务都需要一直跑的,部分定时任务可能会在部分特殊的时间区间需要执行,因此需要能够动态调用定时任务的方式去实现。

2. 配置

基于以上需求,导入相关的依赖。其中主要是引入batch和mybatis相关的依赖。

<dependencies>  
  
    <dependency>        
	    <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-configuration-processor</artifactId>  
        <version>2.6.7</version>  
    </dependency>  
    <dependency>        
	    <groupId>org.springframework.batch</groupId>  
        <artifactId>spring-batch-core</artifactId>  
        <version>4.3.4</version>  
    </dependency>  
    <dependency>        
	    <groupId>org.mybatis.generator</groupId>  
        <artifactId>mybatis-generator-core</artifactId>  
        <version>1.4.0</version>  
    </dependency>  
    <dependency>        
	    <groupId>org.mybatis.spring.boot</groupId>  
        <artifactId>mybatis-spring-boot-starter</artifactId>  
        <version>2.2.0</version>  
  
    </dependency>  
    <dependency>        
	    <groupId>mysql</groupId>  
        <artifactId>mysql-connector-java</artifactId>  
        <version>8.0.27</version>  
    </dependency>  
    <dependency>        
	    <groupId>org.apache.commons</groupId>  
        <artifactId>commons-lang3</artifactId>  
        <version>3.12.0</version>  
    </dependency>  
  
</dependencies>

<build>  
    <plugins>        
	    <plugin>            
		    <groupId>org.mybatis.generator</groupId>  
            <artifactId>mybatis-generator-maven-plugin</artifactId>  
            <version>1.3.7</version>  
            <configuration>                
	            <verbose>true</verbose>  
                <overwrite>true</overwrite>  
            </configuration>        
        </plugin>    
    </plugins>
</build>

mybatis-generator配置文件

  • 示例中用到的表:
CREATE TABLE `batch_control` (
  `batch_name` varchar(40) NOT NULL,
  `org_id` varchar(20) NOT NULL,
  `batch_corn` varchar(40) NOT NULL,
  `batch_desc` varchar(128) NOT NULL,
  `batch_target` varchar(32) NOT NULL,
  `flag` varchar(1) NOT NULL,
  PRIMARY KEY (`batch_name`,`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

# 示例数据
# halfMinute,888888,0/30 * * * * ?,半分钟任务,helloWorldHandler,Y
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE generatorConfiguration  
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"  
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">  
<generatorConfiguration>  
  
    <!-- 引入配置文件 -->  
    <!--<properties resource="jdbc.properties"></properties>-->  
    <!-- 目标数据库 -->  
    <!-- 一个数据库一个context, context子元素必须按照如下顺序  
        property*、plugin*、commentGenerator?、jdbcConnection、javaTypeResolver?  
        javaModelGenerator、sqlMapGenerator?、javaClientGenerator?、table+  
    -->    <!--id : 随便填,保证多个 context id 不重复就行  
        defaultModelType : 可以不填,默认值 conditional,flat表示一张表对应一个po  
        targetRuntime :可以不填,默认值 MyBatis3,常用的还有 MyBatis3Simple,这个配置会影响生成的 dao 和 mapper.xml的内容  
        targetRuntime = MyBatis3Simple,生成的 dao 和 mapper.xml,接口方法会少很多,只包含最最常用的  
    -->  
    <classPathEntry location="C:\Users\xxx\.m2\repository\mysql\mysql-connector-java\8.0.19\mysql-connector-java-8.0.19.jar"/>  
    <context id="default" targetRuntime="MyBatis3">  
  
        <!-- 生成的pojo,将implements Serializable -->  
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>  
        <!-- 为生成的pojo创建一个toString方法 -->  
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>  
        <!-- 生成的pojo,增加了equals 和 hashCode方法-->  
        <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>  
        <!--生成mapper.xml时覆盖原文件-->  
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>  
  
        <!-- 自定义注释 -->  
        <commentGenerator>  
            <!-- 是否去除自动生成的注释 true:是 : false:否 -->  
            <property name="suppressAllComments" value="true"/>  
            <property name="suppressDate" value="false"/>  
            <!--添加 db 表中字段的注释-->  
            <property name="addRemarkComments" value="true"/>  
        </commentGenerator>  
        <!-- 是否去除自动生成的注释 true:是 : false:否    -->  
        <!--<commentGenerator>                <property name="suppressAllComments" value="false" />        </commentGenerator>-->  
        <!--数据库连接信息:驱动类、链接地址、用户名、密码 -->  
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"  
                        connectionURL="jdbc:mysql://localhost:3306/batch_spring_test?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8"  
                        userId="root"  
                        password="root">  
            <!--高版本的 mysql-connector-java 需要设置 nullCatalogMeansCurrent=true-->            <!--解决mysql驱动升级到8.0后不生成指定数据库代码的问题-->  
            <property name="nullCatalogMeansCurrent" value="true"/>  
        </jdbcConnection>  
        <javaTypeResolver>            <!--类型解析器-->  
            <!-- 默认false,把jdbc decimal 和 numeric 类型解析为integer -->  
            <!-- true,把jdbc decimal 和 numeric 类型解析为java.math.bigdecimal-->  
            <property name="forceBigDecimals" value="false"/>  
            <!--默认false  
                false,将所有 JDBC 的时间类型解析为 java.util.Date                true,将 JDBC 的时间类型按如下规则解析  
                   DATE                    -> java.time.LocalDate                   TIME                    -> java.time.LocalTime                   TIMESTAMP                   -> java.time.LocalDateTime                   TIME_WITH_TIMEZONE      -> java.time.OffsetTime                   TIMESTAMP_WITH_TIMEZONE    -> java.time.OffsetDateTime            -->            <property name="useJSR310Types" value="false"/>  
        </javaTypeResolver>  
        <!-- java实体类路径 -->  
        <javaModelGenerator targetPackage="org.example.dal.model" targetProject="src/main/java">  
            <!-- 是否让schema作为包后缀 默认是false  
                会在 po 目录下在创建一个 “数据库名” 的文件夹,生成的 po 会放在该文件夹下,也就是说会多一层目录  
            -->  
            <property name="enableSubPackages" value="false"/>  
            <!-- 从数据库返回的值被清理前后的空格-->  
            <property name="trimStrings" value="true"/>  
        </javaModelGenerator>  
        <!-- 生成映射文件xml的包名和位置-->  
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">  
            <!-- 是否让schema作为包后缀-->  
            <property name="enableSubPackages" value="false"/>  
        </sqlMapGenerator>  
        <!-- 生成Mapper接口的包名和位置  
            type="XMLMAPPER" 会将接口的实现放在 mapper.xml中,也推荐这样配置。  
            type="ANNOTATEDMAPPER",接口的实现通过注解写在接口上面  
         -->  
        <javaClientGenerator type="XMLMAPPER" targetPackage="org.example.dal.mapper"  
                             targetProject="src/main/java">  
            <!-- 是否让schema作为包后缀-->  
            <property name="enableSubPackages" value="false"/>  
        </javaClientGenerator>  
        <!-- 用于自动生成代码的数据库表;生成哪些表;  
            schema为数据库名,oracle需要配置,mysql不需要配置。  
            tableName为对应的数据库表名  
            domainObjectName 是要生成的实体类名(可以不指定)(其中 domainObjectName 不配置时,它会按照帕斯卡命名法将表名转换成类名)  
            enableXXXByExample 默认为 true, 为 true 会生成一个对应Example帮助类,帮助你进行条件查询,不想要可以设为false  
            生成全部表tableName设为 %        -->  
        <!--生成全部表-->  
        <table tableName="%"></table>
        <!--生成单个表-->  
        <!--<table schema="gwshopping" tableName="pms_product" domainObjectName="PmsProduct" enableCountByExample="true"               enableDeleteByExample="true" enableSelectByExample="true"               enableUpdateByExample="true">        </table>-->    </context>  
</generatorConfiguration>

数据库的配置

# batch的基础配置
CREATE TABLE BATCH_JOB_INSTANCE (  
                                    JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY,  
                                    VERSION BIGINT,  
                                    JOB_NAME VARCHAR(100) NOT NULL,  
                                    JOB_KEY VARCHAR(32) NOT NULL,  
                                    constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_JOB_EXECUTION (  
                                     JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,  
                                     VERSION BIGINT,  
                                     JOB_INSTANCE_ID BIGINT NOT NULL,  
                                     CREATE_TIME DATETIME NOT NULL,  
                                     START_TIME DATETIME DEFAULT NULL,  
                                     END_TIME DATETIME DEFAULT NULL,  
                                     STATUS VARCHAR(10),  
                                     EXIT_CODE VARCHAR(2500),  
                                     EXIT_MESSAGE VARCHAR(2500),  
                                     LAST_UPDATED DATETIME,  
                                     JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,  
                                     constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)  
                                         references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_JOB_EXECUTION_PARAMS (  
                                            JOB_EXECUTION_ID BIGINT NOT NULL,  
                                            TYPE_CD VARCHAR(6) NOT NULL,  
                                            KEY_NAME VARCHAR(100) NOT NULL,  
                                            STRING_VAL VARCHAR(250) NOT NULL,  
                                            DATE_VAL DATETIME DEFAULT NULL,  
                                            LONG_VAL BIGINT,  
                                            DOUBLE_VAL DOUBLE,  
                                            IDENTIFYING CHARACTER(1) NOT NULL,  
                                            constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)  
                                                references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT (  
                                             JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,  
                                             SHORT_CONTEXT VARCHAR(2500) NOT NULL,  
                                             SERIALIZED_CONTEXT TEXT,  
                                             constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)  
                                                 references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_STEP_EXECUTION (  
                                      STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,  
                                      VERSION BIGINT,  
                                      STEP_NAME VARCHAR(100) NOT NULL,  
                                      JOB_EXECUTION_ID BIGINT NOT NULL,  
                                      START_TIME DATETIME NOT NULL,  
                                      END_TIME DATETIME DEFAULT NULL,  
                                      STATUS VARCHAR(10),  
                                      COMMIT_COUNT BIGINT,  
                                      READ_COUNT BIGINT,  
                                      FILTER_COUNT BIGINT,  
                                      WRITE_COUNT BIGINT,  
                                      READ_SKIP_COUNT BIGINT,  
                                      WRITE_SKIP_COUNT BIGINT,  
                                      PROCESS_SKIP_COUNT BIGINT,  
                                      ROLLBACK_COUNT BIGINT,  
                                      EXIT_CODE VARCHAR(2500),  
                                      EXIT_MESSAGE VARCHAR(2500),  
                                      LAST_UPDATED DATETIME,  
                                      constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)  
                                          references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT (  
                                              STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,  
                                              SHORT_CONTEXT VARCHAR(2500) NOT NULL,  
                                              SERIALIZED_CONTEXT TEXT,  
                                              constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)  
                                                  references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (  
                                          ID BIGINT NOT NULL,  
                                          constraint UNIQUE_STEP_EXECUTION unique (ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (  
                                         ID BIGINT NOT NULL,  
                                         constraint UNIQUE_JOB_EXECUTION unique (ID)  
) ENGINE=InnoDB;  
  
CREATE TABLE BATCH_JOB_SEQ (  
                               ID BIGINT NOT NULL,  
                               constraint UNIQUE_JOB unique (ID)  
) ENGINE=InnoDB;  
  
INSERT INTO BATCH_STEP_EXECUTION_SEQ VALUES (0);  
INSERT INTO BATCH_JOB_EXECUTION_SEQ VALUES (0);  
INSERT INTO BATCH_JOB_SEQ VALUES (0);

项目的配置

spring:  
  datasource:  
    driver-class-name: com.mysql.cj.jdbc.Driver  
    url: jdbc:mysql://localhost:3306/batch_spring_test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8  
    username: root  
    password: root
  
mybatis:  
  type-aliases-package: org.example.dal.model  
  mapper-locations: classpath:mapper/*.xml  
  
  
logging:  
  level:  
    root: INFO  
    org.example.dal.mapper: DEBUG  
  console:  
    enabled: true

3. 主体代码

利用spring提供的scheduledTaskRegistrar注册一个定时任务,扫描最新的定时任务,将这些定时任务注册到scheduleFuture中从而实现动态定时任务。

@Configuration
@EnableScheduling
public class DynamicScheduleConfigurer implements SchedulingConfigurer {

    private static final Logger logger =
            LoggerFactory.getLogger(DynamicScheduleConfigurer.class);

    @Autowired
    private BatchControlService batchControlService;

    @Autowired
    private ApplicationContext applicationContext;

    private final ConcurrentHashMap<String, ScheduledFuture<?>> scheduleFuture = new ConcurrentHashMap<>();


    private final ConcurrentHashMap<String, CronTask> cronTasks = new ConcurrentHashMap<>();


    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.addTriggerTask(
                ()->{
                    List<BatchControl> tempBatchControls = batchControlService.selectAll();
                    logger.info("获取所有定时任务的信息: {}", tempBatchControls);
                    List<BatchControl> batchControls = new ArrayList<>();
                    for (BatchControl batchControl : tempBatchControls) {
                        if("Y".equals(batchControl.getFlag())){
                            batchControls.add(batchControl);
                        }
                    }
                    Set<String> oldTaskIds = scheduleFuture.keySet();
                    List<String> newTaskIds = batchControls.stream().map(BatchControlKey::getBatchName).collect(Collectors.toList());
                    for (String oldTaskId : oldTaskIds) {
                        if(!newTaskIds.contains(oldTaskId)){
                            logger.info("取消已删除的定时任务{}", oldTaskId);
                            scheduleFuture.remove(oldTaskId).cancel(false);
                            cronTasks.remove(oldTaskId);
                        }
                    }
                    for (BatchControl batchControl : batchControls) {
                        if(StringUtils.isBlank(batchControl.getBatchCorn()) || !CronSequenceGenerator.isValidExpression(batchControl.getBatchCorn())){
                            logger.info("cron表达式不合法{}", batchControl.getBatchCorn());
                            continue;
                        }
                        if(scheduleFuture.containsKey(batchControl.getBatchName()) &&
                                StringUtils.equals(cronTasks.get(batchControl.getBatchName()).getExpression(), batchControl.getBatchCorn())){
                            continue;
                        }
                        if(scheduleFuture.containsKey(batchControl.getBatchName())){
                            logger.info("定时任务时间发生变化,删除任务{},表达式cron: {}", batchControl.getBatchName(), batchControl.getBatchCorn());
                            scheduleFuture.remove(batchControl.getBatchName()).cancel(false);
                            cronTasks.remove(batchControl.getBatchName());
                        }
                        logger.info("重新注册定时任务{},执行时间{}", batchControl.getBatchName(), batchControl.getBatchCorn());
                        CronTask task = new CronTask(()->{
                            logger.info("执行定时任务!, target: {}", batchControl.getBatchTarget());
                            ITaskJob taskJob = (ITaskJob) applicationContext.getBean(batchControl.getBatchTarget());
                            taskJob.runJob();
                        }, batchControl.getBatchCorn());
                        ScheduledFuture<?> future = scheduledTaskRegistrar.getScheduler().schedule(task.getRunnable(), task.getTrigger());
                        cronTasks.put(batchControl.getBatchName(), task);
                        scheduleFuture.put(batchControl.getBatchName(), future);
                    }
                    logger.info("定时任务刷新完成");
                },
                new CronTrigger("0/30 * * * * ?")
        );
    }
}

定时任务执行类

public interface ITaskJob {  
  
    public void runJob();  
}

@Component  
public class HelloWorldHandler implements ITaskJob {  
  
    private static final Logger logger =  
            LoggerFactory.getLogger(HelloWorldHandler.class);  
  
    @Autowired  
    private HelloWorldJobConfig helloWorldJobConfig;  
  
    @Override  
    public void runJob() {  
        logger.info("log out hello world!!!!");  
        JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();  
        jobParametersBuilder.addString("hello", "handler to hello wrold");  
        jobParametersBuilder.addString("batchTime", new Date().toString());  
        try {  
            helloWorldJobConfig.jobLauncher.run(helloWorldJobConfig.helloWorldJob(), jobParametersBuilder.toJobParameters());  
        } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException |  
                 JobParametersInvalidException e) {  
            logger.error("定时任务抛出异常");  
        }  
    }  
}

public class HelloWorldTasklet implements Tasklet {  
  
    private static final Logger logger =  
            LoggerFactory.getLogger(HelloWorldTasklet.class);  
  
    @Override  
    public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {  
        logger.info("任务执行中--------------");  
        return RepeatStatus.FINISHED;  
    }  
}

@Configuration  
@EnableBatchProcessing  
public class HelloWorldJobConfig {  
  
    @Autowired  
    private JobBuilderFactory jobBuilderFactory;  
  
    @Autowired  
    private StepBuilderFactory stepBuilderFactory;  
  
    @Autowired  
    public JobLauncher jobLauncher;  
  
  
    @Bean  
    public Job helloWorldJob(){  
        return jobBuilderFactory.get("helloWorldJob")  
                .start(helloWorldStep())  
                .build();  
    }  
  
    @Bean  
    public Step helloWorldStep(){  
        return stepBuilderFactory.get("helloWorldStep")  
                .tasklet(new HelloWorldTasklet())  
                .build();  
    }  
}

// 启动类
@SpringBootApplication(scanBasePackages = "org.example")  
@EnableScheduling  
@MapperScan(basePackages = "org.example.dal.mapper")  
public class TestBatch {  
  
    public static void main(String[] args) {  
        SpringApplication.run(TestBatch.class, args);  
    }  
}
  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Batch中,要终止多线程Step任务,可以在StepExecutionListener的afterStep方法中调用RepeatOperations的cancel方法。以下是一个示例代码片段,展示如何在StepExecutionListener中终止多线程Step任务: ``` public class MyStepExecutionListener implements StepExecutionListener { private volatile RepeatOperations repeatOperations; @Override public void beforeStep(StepExecution stepExecution) { repeatOperations = ((TaskletStep) stepExecution.getStep()).getTasklet().getRepeatOperations(); } @Override public ExitStatus afterStep(StepExecution stepExecution) { repeatOperations.cancel(); return null; } } ``` 在上面的代码中,我们实现了StepExecutionListener接口,并在beforeStep方法中获取RepeatOperations实例,以便在afterStep方法中调用其cancel方法来终止任务。请注意,我们使用了volatile关键字来确保多个线程之间的可见性。 要使用StepExecutionListener,可以将其作为Step的listener属性,如下所示: ``` <batch:step id="multiThreadedStep"> <batch:tasklet> <batch:chunk reader="reader" processor="processor" writer="writer" commit-interval="100"/> </batch:tasklet> <batch:listeners> <batch:listener ref="myStepExecutionListener"/> </batch:listeners> <batch:task-executor> <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/> </batch:task-executor> </batch:step> ``` 在上面的代码中,我们定义了一个名为myStepExecutionListener的StepExecutionListener bean,并将其用作multiThreadedStep Step的listener属性。我们还使用了SimpleAsyncTaskExecutor作为multiThreadedStep Step的task-executor属性,以便在多个线程之间分配可重复执行的任务。当需要终止任务时,StepExecutionListener会调用RepeatOperations的cancel方法,以停止多个线程执行任务。 需要注意的是,如果任务已经开始执行,调用cancel方法将不会立即停止线程。相反,线程将继续执行,直到下一个可中断点或直到任务完成。因此,要确保任务中有足够的可中断点,以便线程可以在调用cancel方法后尽快停止。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值