使用背景
在最近的项目中遇到一个需要使用到动态定时任务的需求,即定时任务的调用时间不是在某个固定时间自动执行,而是由用户控制,并且需要持久化。因此在网上搜了一下,发现了一个基于Java开发的Quartz定时任务调度框架,很符合我的需求,因此记录一下便于以后再次使用。
Quartz相关概念
quartz的使用过程主要涉及到以下几个概念:
1. Job
Job 是一个抽象接口,用户可以实现该接口,并且重写execute方法,在execute方法内,是用户需要实现的具体的功能
2. JobDetail
JobDetail 是对于Job的描述
3. JobDataMap
JobDataMap 是一个Map对象,用于存放Job需要的参数
4. Trigger
Trigger 用于触发定时任务
5. Scheduler
Scheduler 用于调度 JobDetail 和 Trigger
配置
首先创建了一个基础的SpringBoot项目(虽然Quartz不需要依赖SpringBoot也可以运行)
Maven依赖如下:
点击查看代码
<dependencies><!-- SpringBoot依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.7.18</version></dependency><!-- SpringBootTest依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.7.18</version></dependency><!-- Quartz依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId><version>2.7.18</version></dependency><!-- 数据库依赖,用于持久化定时任务 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.3.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><version>2.7.18</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.18</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency></dependencies>
项目结构如下
在项目启动后,SpringBoot已经维护了一个Scheduler
下面将以预售商品自动到期自动下架作为例子,使用Quartz框架
1. 创建并且调度任务
在 CommodityAutoOfflineJob 类中实现了Job接口,并且模拟下架商品的逻辑
点击查看代码
/*** 商品自动下架定时任务** @author panlijun* @since 2024/10/31 21:14*/
@Slf4j
public class AutoOfflineCommodityJob implements Job {@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {log.info("开始执行商品自动下架定时任务");try {Thread.sleep(5_000L);} catch (InterruptedException e) {log.error("商品自动下架定时任务执行失败", e);throw new RuntimeException(e);}log.info("商品自动下架定时任务执行完毕");}
}
在 TestController 中创建了JobDetail 、Trigger 并且使用Scheduler对任务进行调度
点击查看代码
/*** @author panlijun* @since 2024/10/31 21:26*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {@Resourceprivate Scheduler scheduler;@SneakyThrows@RequestMapping("/creatAutoOfflineCommodityJob")public String test(@RequestParam String endTime) {log.info("endTime:{}",endTime);log.info("创建JobDetail");JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class).withIdentity("autoOfflineCommodityJob", "Commodity").build();log.info("创建Trigger");SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");Trigger trigger = TriggerBuilder.newTrigger().withIdentity("autoOfflineCommodityTrigger", "Commodity").startAt(format.parse(endTime)).build();log.info("调度任务");scheduler.scheduleJob(jobDetail, trigger);return "定时任务将于" + endTime + "执行";}
}
2. 使用自定义参数
在一些业务中,执行定时任务需要依靠一些具体的参数才能执行,上面的代码就不能满足需要了,因此对代码进行修改如下:
在Controller中额外接受要需要下架的商品id
public String test(@RequestParam String endTime, @RequestParam Integer commodityId)
创建JobDataMap对象
log.info("创建JobDataMap");
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("COMMODITY_ID", commodityId);
将接收到的商品id放入Map,并在创建JobDetail时作为参数传入
log.info("创建JobDetail");
JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
.withIdentity("autoOfflineCommodityJob", "Commodity")
.usingJobData(jobDataMap)
.build();
点击查看代码
@SneakyThrows@RequestMapping("/creatAutoOfflineCommodityJob")public String test(@RequestParam String endTime,@RequestParam Integer commodityId) {log.info("endTime:{}", endTime);log.info("创建JobDataMap");JobDataMap jobDataMap = new JobDataMap();jobDataMap.put("COMMODITY_ID", commodityId);log.info("创建JobDetail");JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class).withIdentity("autoOfflineCommodityJob", "Commodity").usingJobData(jobDataMap).build();log.info("创建Trigger");SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");Trigger trigger = TriggerBuilder.newTrigger().withIdentity("autoOfflineCommodityTrigger", "Commodity").startAt(format.parse(endTime)).build();log.info("调度任务");scheduler.scheduleJob(jobDetail, trigger);return "定时任务将于" + endTime + "执行";}
并在定时任务中获取传入的参数
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
Integer commodityId = (Integer) jobDataMap.get("COMMODITY_ID");
点击查看代码
@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {log.info("开始执行商品自动下架定时任务");try {JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();Integer commodityId = (Integer) jobDataMap.get("COMMODITY_ID");log.info("要下架的商品ID为:{}", commodityId);Thread.sleep(5_000L);} catch (InterruptedException e) {log.error("商品自动下架定时任务执行失败", e);throw new RuntimeException(e);}log.info("商品自动下架定时任务执行完毕");}
定时任务执行结果如下:
3. 定时任务持久化
Quartz默认将定时任务的数据保存在内存,每次系统重启都会丢失待运行的定时任务,这显然是不能接受的,因此需要对定时任务进行持久化,好在Quartz提供了对定时任务持久化的方法。
3.1 创建相关数据库表结构
以Mysql为例,需要创建以下表:
创建的表的SQL脚本可以在Quartz的代码仓库中找到,链接如下
https://github.com/quartz-scheduler/quartz/releases
SQL脚本就在下载文件的 quartz-2.3.2\quartz-core\src\main\resources\org\quartz\impl\jdbcjobstore
文件夹下
该文件夹下有很多种类的数据库的脚本,上图中框出的文件是MySQL的脚本
3.2 在项目中添加配置数据库和Quartz相关配置
点击查看代码
spring:application:name: quartz-study# 数据库配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring_task_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=falseusername: rootpassword: 123456# quartz 持久化配置quartz:# 默认为memory,选择jdbc时,可以选择将定时任务持久化到数据库job-store-type: jdbcjdbc:# 每次启动项目时是否初始化表, 建议设置为never, 并且手动运行SQL脚本, 初始化数据库initialize-schema: never
3.3 修改代码,持久化定时任务
在创建JobDetail时设置.storeDurably(true)
,就能把定时任务持久化到数据库
点击查看代码
log.info("创建JobDetail");JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class).withIdentity("autoOfflineCommodityJob", "Commodity").storeDurably(true).usingJobData(jobDataMap).build();
再次重启项目并调用接口
并查看数据库,发现数据库中增加了一条JobDetail数据
4. 恢复中断定时任务
在定时任务运行过程中,进程被终止了,在重启项目后,是不会重新执行被中断的定时任务的
如果需要恢复运行中被中断的定时任务,只需要设置 .requestRecovery(true)
就可以在重启时重新执行被中断的定时任务
点击查看代码
log.info("创建JobDetail");JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class).withIdentity("autoOfflineCommodityJob", "commodity").usingJobData(jobDataMap).requestRecovery(true).storeDurably(true).build();
注意事项
在Quartz中,JobDetail .withIdentity("autoOfflineCommodityJob", "commodity")
中的两个参数也就是name
和group
构成的二元组是不可以重复,在以下情况
-
不使用持久化定时任务时,上一个同名 JobDetail 还未完成
-
使用持久化定时任务时,创建同名定时任务,即使上一个定时任务已经完成
会出现无法创建定时任务的情况
因此,如果可以复用JobDetail,则尽量复用,当无法复用JobDetail时,则需要给JobDetail不同的name
和group
点击查看代码
// 根据雪花算法生成IDSnowflake snowflake = IdUtil.getSnowflake();log.info("创建JobDetail");JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class).withIdentity(snowflake.nextIdStr() + "_Job", "commodity").usingJobData(jobDataMap).requestRecovery(true).storeDurably(true).build();log.info("创建Trigger");SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");Trigger trigger = TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity(snowflake.nextIdStr() + "_Trigger", "Commodity").startAt(format.parse(endTime)).build();