定时调度框架Quartz使用
使用背景
在最近的项目中遇到一个需要使用到动态定时任务的需求,即定时任务的调用时间不是在某个固定时间自动执行,而是由用户控制,并且需要持久化。因此在网上搜了一下,发现了一个基于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 {
@Override
public 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 {
@Resource
private 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");
点击查看代码
@Override
public 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.Driver
url: jdbc:mysql://localhost:3306/spring_task_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
# quartz 持久化配置
quartz:
# 默认为memory,选择jdbc时,可以选择将定时任务持久化到数据库
job-store-type: jdbc
jdbc:
# 每次启动项目时是否初始化表, 建议设置为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
点击查看代码
// 根据雪花算法生成ID
Snowflake 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();
浙公网安备 33010602011771号