前言
根据需求(多条不确定的结束时间 + 提前 n 分钟推送),spring boot 中最优方案是结合 quartz 动态定时任务 + 任务持久化,支持动态添加 / 删除结束时间、自动计算提前 n 分钟的触发点,且能应对服务重启后任务不丢失的问题。
一、quartz 是什么?

quartz 是一个功能强大、开源的任务调度框架,用于实现定时、周期性或基于特定规则的任务执行。简单来说,它就是 java 生态中 “定时任务” 的标准解决方案,支持复杂的调度逻辑,广泛应用于后台系统的定时任务场景(如定时备份、数据同步、定时推送、报表生成等)。
1、核心定位:解决什么问题?
日常开发中,我们常需要 “在特定时间执行某段代码”,比如:
- 每天凌晨 2 点执行数据库备份;
- 每隔 30 分钟同步一次第三方数据;
- 每月 1 号生成上月报表;
- 某个固定时间点触发短信推送。
java 原生提供了 timer/timertask,但存在明显缺陷(如单线程执行、不支持复杂表达式、异常会导致线程终止),无法满足企业级需求。而 quartz 弥补了这些不足,提供了:
- 复杂调度规则(支持 cron 表达式,覆盖几乎所有时间场景);
- 任务与调度解耦(任务逻辑和执行规则分离);
- 高可用(支持集群部署,避免单点故障);
- 可持久化(任务和调度状态可存储到数据库,重启后不丢失);
- 并发控制(支持任务并发执行或串行执行)。
2、quartz 核心组件
quartz 的架构设计清晰,核心组件分为 3 类,需理解它们的职责和关系:
| 组件 | 作用 |
|---|---|
| job(任务) | 具体要执行的 “业务逻辑”,需实现 org.quartz.job 接口(重写 execute() 方法)。 |
| jobdetail(任务详情) | 描述 job 的元数据(如任务名称、分组、是否持久化等),是 quartz 内部管理 job 的载体(job 本身是业务逻辑,jobdetail 是管理配置)。 |
| trigger(触发器) | 定义 job 的 “执行规则”(何时执行、执行频率),是触发 job 执行的 “开关”。常见实现:simpletrigger:简单规则(如延迟 5 秒执行、每隔 10 秒执行 3 次);crontrigger:复杂规则(基于 cron 表达式,如每天 02:00 执行)。 |
| scheduler(调度器) | quartz 的核心 “大脑”,负责管理 jobdetail 和 trigger,根据 trigger 规则触发 job 执行。需通过 schedulerfactory 创建,是任务调度的入口。 |
二、使用步骤
1. 引入依赖(spring boot + quartz + 数据库)
需要 quartz 核心依赖、spring 整合包,以及数据库依赖(用于任务持久化):
<!-- spring boot 整合 quartz -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-quartz</artifactid>
</dependency>
<!-- 数据库依赖(以 mysql 为例,用于任务持久化) -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-jpa</artifactid>
</dependency>
<dependency>
<groupid>com.mysql</groupid>
<artifactid>mysql-connector-j</artifactid>
<scope>runtime</scope>
</dependency>2. 配置文件(application.yml)
配置数据库连接和 quartz 持久化(关键:禁用内存存储,启用数据库存储):
yaml
spring:
# 数据库配置(quartz 任务将存储到该库)
datasource:
url: jdbc:mysql://localhost:3306/quartz_db?usessl=false&servertimezone=utc&allowpublickeyretrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.driver
# jpa 配置(可选,用于存储业务结束时间,方便管理)
jpa:
hibernate:
ddl-auto: update
show-sql: true
# quartz 配置(核心:数据库持久化)
quartz:
job-store-type: jdbc # 任务存储类型:jdbc(数据库),默认是 memory(内存)
jdbc:
initialize-schema: always # 自动创建 quartz 所需表(首次启动用 always,后续改为 never)
properties:
org:
quartz:
scheduler:
instancename: pushscheduler
instanceid: auto # 集群模式下自动分配实例id
jobstore:
class: org.quartz.impl.jdbcjobstore.jobstoretx
driverdelegateclass: org.quartz.impl.jdbcjobstore.stdjdbcdelegate
tableprefix: qrtz_ # quartz 表前缀(自动创建的表会带此前缀)
isclustered: false # 单节点用 false,集群部署改为 true
clustercheckininterval: 20000
threadpool:
class: org.quartz.simpl.simplethreadpool
threadcount: 10 # 线程池大小3. 定义业务实体(存储结束时间,可选但推荐)
用于记录用户添加的 “结束时间”,方便后续查询、修改、删除任务(关联 quartz 任务 id):
import jakarta.persistence.*;
import lombok.data;
import java.time.localdatetime;
@data
@entity
@table(name = "push_task")
public class pushtask {
@id
@generatedvalue(strategy = generationtype.identity)
private long id; // 自增id
@column(name = "end_time", nullable = false)
private localdatetime endtime; // 原始结束时间(如明天3点)
@column(name = "trigger_time", nullable = false)
private localdatetime triggertime; // 推送触发时间(endtime -5分钟)
@column(name = "quartz_job_id", unique = true, nullable = false)
private string quartzjobid; // 关联 quartz 的 jobdetail id
@column(name = "task_desc")
private string taskdesc; // 任务描述(如“订单123超时提醒”)
@column(name = "status")
private integer status; // 状态:0-未触发 1-已触发 2-已取消
}4. 实现消息推送 job(quartz 任务逻辑)
quartz 任务的核心逻辑:触发时执行消息推送,同时更新业务任务状态:
import org.quartz.job;
import org.quartz.jobexecutioncontext;
import org.quartz.jobexecutionexception;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;
import java.time.localdatetime;
/**
* 消息推送 job(quartz 任务)
*/
@component
public class pushmessagejob implements job {
private static final logger logger = loggerfactory.getlogger(pushmessagejob.class);
@autowired
private pushtaskrepository pushtaskrepository; // 后续定义的 dao 接口
@override
public void execute(jobexecutioncontext context) throws jobexecutionexception {
// 1. 从 job 上下文获取参数( quartzjobid + 任务描述 )
string quartzjobid = context.getjobdetail().getkey().getname();
string taskdesc = (string) context.getjobdetail().getjobdatamap().get("taskdesc");
string triggertimedate = (string) context.getjobdetail().getjobdatamap().get("triggertime");
localdatetime triggertime = null;
if (null != triggertimedate) {
triggertime = localdatetime.parse(triggertimedate);
}
try {
// 2. 核心:执行消息推送(替换为你的实际逻辑:短信、app推送、邮件等)
dopush(taskdesc, triggertime);
// 3. 更新业务任务状态为“已触发”
pushtaskrepository.updatestatusbyquartzjobid(1, quartzjobid);
logger.info("消息推送成功!quartzjobid:{},任务描述:{},触发时间:{}",
quartzjobid, taskdesc, triggertime);
} catch (exception e) {
logger.error("消息推送失败!quartzjobid:{}", quartzjobid, e);
// 可选:抛出异常触发 quartz 重试(需配置重试策略)
throw new jobexecutionexception("推送失败,触发重试", e, true);
}
}
/**
* 实际推送逻辑(根据业务需求替换)
*/
private void dopush(string taskdesc, localdatetime triggertime) {
// 示例1:打印日志(实际场景替换为第三方推送接口)
system.out.printf("[推送通知] 任务描述:%s,触发时间:%s,内容:即将到达结束时间(提前5分钟提醒)%n",
taskdesc, triggertime);
// 示例2:调用 http 推送接口(如极光推送)
// resttemplate resttemplate = new resttemplate();
// string pushurl = "https://api.jpush.cn/v3/push";
// pushrequest request = new pushrequest();
// request.setcontent("即将到达结束时间:" + taskdesc);
// resttemplate.postforobject(pushurl, request, string.class);
}
}5. 定义 dao 接口(操作业务任务表)
import org.springframework.data.jpa.repository.jparepository;
import org.springframework.data.jpa.repository.modifying;
import org.springframework.data.jpa.repository.query;
import org.springframework.transaction.annotation.transactional;
import java.time.localdatetime;
public interface pushtaskrepository extends jparepository<pushtask, long> {
// 根据 quartzjobid 查询任务
pushtask findbyquartzjobid(string quartzjobid);
// 更新任务状态
@modifying
@transactional
@query("update pushtask t set t.status = :status where t.quartzjobid = :quartzjobid")
int updatestatusbyquartzjobid(integer status, string quartzjobid);
// 根据状态和触发时间查询未执行的任务(服务重启后恢复任务用)
@query("select t from pushtask t where t.status = 0 and t.triggertime > :now")
list<pushtask> finduntriggeredtasks(localdatetime now);
}6. 动态定时任务服务(核心:添加 / 删除 / 恢复任务)
封装 quartz api,实现动态添加任务(自动计算提前 5 分钟)、删除任务、服务重启后恢复未触发任务:
import org.quartz.*;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.service;
import java.time.localdatetime;
import java.util.list;
import java.util.uuid;
@service
public class dynamicquartzservice {
@autowired
private scheduler scheduler;
@autowired
private pushtaskrepository pushtaskrepository;
// 任务组名(统一分组,方便管理)
private static final string job_group = "push_job_group";
private static final string trigger_group = "push_trigger_group";
/**
* 核心方法:添加推送任务(自动计算提前5分钟触发)
*
* @params endtime 原始结束时间(如明天3点)
* @params triggertime 触发时间(如明天2点)
* @params taskdesc 任务描述(如“订单123超时提醒”)
* @return 保存的业务任务
*/
public pushtask addpushtask(localdatetime endtime, localdatetime triggertime,string taskdesc) throws schedulerexception {
paassert.notnull(endtime, "结束时间不能为空!");
paassert.notnull(triggertime, "触发时间不能为空!");
// 校验结束时间
localdatetime now = localdatetime.now();
if (endtime.isbefore(now) || endtime.isequal(now)) {
throw new illegalargumentexception("结束时间必须晚于当前时间!");
}
if (triggertime.isbefore(now) || triggertime.isequal(now)) {
throw new illegalargumentexception("触发时间必须晚于当前时间!");
}
// 3. 生成唯一的 quartzjobid(避免重复)
string quartzjobid = "push_job_" + uuid.randomuuid().tostring().replace("-", "");
// 4. 构建 quartz jobdetail(绑定 pushmessagejob)
jobdetail jobdetail = jobbuilder.newjob(pushmessagejob.class)
.withidentity(quartzjobid, job_group) // 任务id:quartzjobid,组名:job_group
.usingjobdata("taskdesc", taskdesc)
.usingjobdata("triggertime", string.valueof(triggertime))
.storedurably() // 持久化(无触发器时也保留)
.build();
// 5. 构建 crontrigger(根据 triggertime 生成 cron 表达式)
string cronexpression = buildcronexpression(triggertime);
crontrigger trigger = triggerbuilder.newtrigger()
.withidentity(quartzjobid, trigger_group)
.forjob(jobdetail)
.withschedule(cronschedulebuilder.cronschedule(cronexpression)
.withmisfirehandlinginstructionfireandproceed()) // 错过触发时立即执行
.startnow()
// 关键:触发时间+1分钟,确保仅执行一次(避免循环或重复触发)
.endat(date.from(triggertime.plusminutes(1).atzone(zoneid.systemdefault()).toinstant()))
.build();
// 6. 注册任务到 quartz 调度器
scheduler.schedulejob(jobdetail, trigger);
// 7. 保存业务任务到数据库(关联 quartzjobid)
pushtask pushtask = new pushtask();
pushtask.setendtime(endtime);
pushtask.settriggertime(triggertime);
pushtask.setquartzjobid(quartzjobid);
pushtask.settaskdesc(taskdesc);
pushtask.setstatus(0); // 0-未触发
return pushtaskrepository.save(pushtask);
}
/**
* 删除推送任务(同时删除 quartz 任务和业务任务)
* @param quartzjobid 任务id(添加任务时返回的 quartzjobid)
*/
public void deletepushtask(string quartzjobid) throws schedulerexception {
// 1. 停止并删除 quartz 任务
jobkey jobkey = jobkey.jobkey(quartzjobid, job_group);
triggerkey triggerkey = triggerkey.triggerkey(quartzjobid, trigger_group);
scheduler.pausetrigger(triggerkey); // 暂停触发器
scheduler.unschedulejob(triggerkey); // 移除触发器
scheduler.deletejob(jobkey); // 删除任务
// 2. 更新业务任务状态为“已取消”
pushtaskrepository.updatestatusbyquartzjobid(2, quartzjobid);
}
/**
* 服务重启后恢复未触发的任务(关键:避免任务丢失)
*/
public void restoreuntriggeredtasks() throws schedulerexception {
// 1. 查询数据库中“未触发”且“触发时间未到”的任务
list<pushtask> untriggeredtasks = pushtaskrepository.finduntriggeredtasks(localdatetime.now());
if (untriggeredtasks.isempty()) {
return;
}
// 2. 重新注册这些任务到 quartz
for (pushtask task : untriggeredtasks) {
string quartzjobid = task.getquartzjobid();
localdatetime triggertime = task.gettriggertime();
string taskdesc = task.gettaskdesc();
// 构建 jobdetail
jobdetail jobdetail = jobbuilder.newjob(pushmessagejob.class)
.withidentity(quartzjobid, job_group)
.usingjobdata("taskdesc", taskdesc)
.usingjobdata("triggertime", string.valueof(triggertime))
.storedurably()
.build();
// 构建 trigger
string cronexpression = buildcronexpression(triggertime);
crontrigger trigger = triggerbuilder.newtrigger()
.withidentity(quartzjobid, trigger_group)
.forjob(jobdetail)
.withschedule(cronschedulebuilder.cronschedule(cronexpression))
.startnow()
.build();
// 注册任务
scheduler.schedulejob(jobdetail, trigger);
}
system.out.printf("恢复未触发的推送任务数量:%d%n", untriggeredtasks.size());
}
/**
* 工具方法:将 localdatetime 转换为 cron 表达式(精准到秒)
* cron 格式:秒 分 时 日 月 周 年(年可选)
* 例:2025-11-01 02:55:00 → 0 55 2 1 11 ? 2025
*/
private string buildcronexpression(localdatetime triggertime) {
int second = triggertime.getsecond(); // 秒(0)
int minute = triggertime.getminute(); // 分(0)
int hour = triggertime.gethour(); // 时(16)
int day = triggertime.getdayofmonth();// 日(21)
int month = triggertime.getmonthvalue();// 月(11,localdatetime 直接是 1-12,无需+1)
int year = triggertime.getyear(); // 年(2025)
// cron 格式:秒 分 时 日 月 ? 年 → 周字段用 ?,避免与日冲突
return string.format("%d %d %d %d %d ? %d",
second, minute, hour, day, month, year);
}
}7. 启动时恢复任务(监听服务启动)
服务重启后,自动恢复数据库中 “未触发” 的任务,避免任务丢失:
import org.quartz.schedulerexception;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.boot.applicationrunner;
import org.springframework.stereotype.component;
@component
public class taskrestorerunner implements applicationrunner {
@autowired
private dynamicquartzservice dynamicquartzservice;
@override
public void run(applicationarguments args) throws schedulerexception {
// 服务启动后,恢复未触发的任务
dynamicquartzservice.restoreuntriggeredtasks();
}
}8. 测试接口(对外提供添加 / 删除任务的入口)
通过 http 接口测试动态添加、删除任务:
import org.quartz.schedulerexception;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.format.annotation.datetimeformat;
import org.springframework.web.bind.annotation.*;
import java.time.localdatetime;
@restcontroller
@requestmapping("/push")
public class pushtaskcontroller {
@autowired
private dynamicquartzservice dynamicquartzservice;
/**
* 添加推送任务
* @param endtime 结束时间(格式:yyyy-mm-dd hh:mm:ss,如 2025-11-01 03:00:00)
* @param taskdesc 任务描述
*/
@postmapping("/add")
public pushtask addtask(
@requestparam @datetimeformat(pattern = "yyyy-mm-dd hh:mm:ss") localdatetime endtime,
@requestparam string taskdesc) throws schedulerexception {
return dynamicquartzservice.addpushtask(endtime, taskdesc);
}
/**
* 删除推送任务
* @param quartzjobid 任务id(添加任务时返回的 quartzjobid)
*/
@postmapping("/delete")
public string deletetask(@requestparam string quartzjobid) throws schedulerexception {
dynamicquartzservice.deletepushtask(quartzjobid);
return "任务删除成功!quartzjobid:" + quartzjobid;
}
}三、核心功能测试
1. 添加任务测试
发送 post 请求:
plaintext http://localhost:8080/push/add?endtime=2025-11-01
03:00:00&taskdesc=订单123超时提醒
- 后台会自动计算触发时间:2025-11-01 02:55:00;
- 数据库 push_task 表会新增一条记录,状态为 0(未触发);
- quartz 会创建对应的 job 和 trigger,到 02:55 自动触发推送。
2. 触发效果
到触发时间后,控制台会输出:
[推送通知] 任务描述:订单123超时提醒,触发时间:2025-11-01t02:55,内容:即将到达结束时间(提前5分钟提醒)
同时 push_task 表中该任务的状态会更新为 1(已触发)。
3. 删除任务测试
发送 post 请求:
http://localhost:8080/push/delete?quartzjobid=push_job_xxx(添加任务时返回的
quartzjobid)
- quartz 会删除对应的 job 和 trigger;
- 数据库中任务状态更新为 2(已取消)。
四、关键特性说明
1. 动态性
- 支持任意多条结束时间:调用 addpushtask 方法可添加多个任务,每个任务独立触发;
- 结束时间不确定:无需提前配置 cron 表达式,传入 localdatetime 即可自动生成触发规则。
2. 可靠性
- 持久化:任务信息存储在数据库,服务重启后自动恢复未触发的任务;
- 失败重试:推送失败时抛出异常,quartz 会根据配置的策略重试(默认重试 3 次);
- 集群支持:修改 application.yml 中 quartz.properties.org.quartz.jobstore.isclustered=true,即可支持集群部署(避免重复触发)。
3. 灵活性
- 提前时间可配置:将 minusminutes(5) 改为参数(如 minusminutes(提前分钟数)),支- - 持动态调整提前提醒时间;
- 推送逻辑可扩展:修改 dopush 方法,整合短信、app 推送(极光 / 个推)、邮件等任意渠道。
4. 时间校验
- 避免添加已过期的任务(结束时间早于当前时间);
- 避免提前 5 分钟后仍过期的任务(如结束时间为当前时间 + 3 分钟,提前 5 分钟则已过期)。
五、生产环境优化建议
1.任务监控:通过 quartz 提供的 api 监控任务状态(如查询所有任务、触发次数、失败次数),或集成 prometheus + grafana 可视化监控;
2.过期任务清理:定时清理数据库中 “已触发” 或 “已取消” 的任务(避免表数据过大);
3.推送异步化:如果推送逻辑耗时较长(如调用第三方接口),可在 dopush 中发送 mq 消息,由消费者异步处理推送(解耦定时任务和推送逻辑);
4.日志增强:记录推送结果(成功 / 失败)到日志文件或数据库,方便问题排查;
5.权限控制:对 /push/add 和 /push/delete 接口添加权限校验(如 token 验证),避免恶意操作。
总结
本方案通过 quartz 动态任务 + 数据库持久化,完美满足 “多条不确定结束时间 + 提前 5 分钟推送” 的需求,核心优势:
- 动态灵活:支持任意结束时间,无需提前配置;
- 可靠稳定:任务持久化、服务重启恢复、失败重试;
- 易于扩展:支持多推送渠道、集群部署、监控告警。
如果需要轻量级方案(无需持久化,服务重启后任务可丢失),也可使用 spring scheduler + 内存缓存,但生产环境建议优先选择本方案(可靠性更高)
到此这篇关于springboot整合 quartz实现定时推送实战指南的文章就介绍到这了,更多相关springboot quartz定时推送内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论