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&characterEncoding=utf8&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);
}
}