SpringBoot整合Quartz实战:从建表到动态任务管理
1. 为什么你需要Quartz?从“一次性”到“动态化”的调度进化
如果你用过SpringBoot自带的@Scheduled注解,那你肯定知道它有多方便。加个注解,配个cron表达式,任务就能定时跑了。但用久了,痛点就来了:所有任务都得写在代码里,改个执行时间就得重新打包部署;任务状态没法持久化,服务一重启,谁在跑、跑到哪了全丢了;更别提想动态地新增、修改或者暂停某个任务了,基本没戏。这时候,一个更专业的调度框架就该登场了。
Quartz就是干这个的。它是个功能强大的开源作业调度库,你可以把它想象成一个高度可定制的“闹钟中心”。它不仅能处理简单的“每隔X秒响一次”,还能处理复杂的“每周一三五早上9点,但节假日除外”这种场景。而它最核心的竞争力,就是持久化和动态管理。通过把任务和触发器的信息存到数据库里,任务调度就变成了有状态的、可追溯的、可集群的。你的服务重启了?没关系,Quartz从数据库里把任务状态读出来,该接着跑的接着跑。你想在管理后台临时加个数据清理任务?通过几行API调用就能动态创建,完全不用动线上代码。
SpringBoot官方提供了spring-boot-starter-quartz,这让整合变得异常简单。但很多教程只讲到“能用”,离“好用”和“实用”还差得远。今天,我就结合自己趟过的坑,带你从零开始,把Quartz的数据库持久化配通,并实现一套真正能在生产环境用的动态任务管理功能。咱们不玩虚的,直接上干货。
2. 项目搭建与核心配置:让Quartz“记住”你的任务
2.1 引入依赖与基础认知
第一步,在pom.xml里加入依赖。这里有个小细节,我们通常会把spring-boot-starter-quartz和数据库驱动、连接池(比如HikariCP)一起引入,因为持久化离不开它们。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- 根据你的数据库选择,例如MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
引入这个starter后,SpringBoot会自动为我们配置一个Scheduler(调度器)实例。但默认配置是内存式的(RAMJobStore),我们要做的,就是通过配置文件,把它“扳”到数据库模式(JobStoreTX)。
2.2 详解application.yml配置:连接数据库的关键
配置文件是Quartz持久化的灵魂。下面这份配置是我在多个项目中提炼出来的,加了详细注释,你几乎可以照搬。
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
hikari:
# ... 你的连接池配置
quartz:
# 关键!指定使用JDBC进行持久化
job-store-type: jdbc
# 是否等待任务完成后关闭调度器,生产环境建议true
wait-for-jobs-to-complete-on-shutdown: true
# 覆盖应用级别的数据源,明确指定Quartz使用哪个数据源(非必须,但清晰)
jdbc:
initialize-schema: never # 我们手动执行SQL建表,所以设为never
properties:
org:
quartz:
scheduler:
instanceName: MyClusteredScheduler # 调度器实例名,集群中需唯一
instanceId: AUTO # 实例ID自动生成,集群部署时很重要
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX # 使用JDBC事务性存储
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQL标准委托类
tablePrefix: QRTZ_ # 表前缀,默认就是这个
isClustered: true # 开启集群支持,多实例时避免任务重复执行
clusterCheckinInterval: 15000 # 集群节点检入间隔(毫秒)
useProperties: false # 序列化策略,false表示用Java序列化,true则用字符串(需注意类型安全)
threadPool:
class: org.quartz.simpl.SimpleThreadPool # 线程池实现类
threadCount: 10 # 工作线程数,根据任务量调整
threadPriority: 5 # 线程优先级
我来解释几个容易踩坑的点:
job-store-type: jdbc:这是开关,告诉Quartz用数据库存任务。driverDelegateClass:这个必须和你的数据库匹配。用MySQL就是StdJDBCDelegate,用PostgreSQL是PostgreSQLDelegate,Oracle是OracleDelegate。配错了会报各种奇怪的SQL语法错误。isClustered: true:即使你现在是单机,我也建议先打开。这会让Quartz使用行锁来保证任务在集群环境下的唯一执行,为未来扩容留好余地。initialize-schema: never:SpringBoot虽然支持自动建表(always或embedded),但我强烈建议手动执行SQL。自动建表可能因为数据库方言问题失败,而且你无法控制表结构细节。
2.3 获取并执行建表SQL:打好地基
这是持久化的物理基础——数据库表。Quartz官网和GitHub仓库提供了所有主流数据库的建表脚本。我以最常用的MySQL为例,告诉你最稳的获取路径。
别去网上搜那些来历不明的SQL了,直接去Quartz的GitHub官方仓库:https://github.com/quartz-scheduler/quartz。
进去后,找到并进入 docs/dbTables 目录。你会看到一堆SQL文件,比如 tables_mysql.sql。点开它,复制全部内容。这个脚本会创建11张以QRTZ_开头的表,核心的有:
QRTZ_JOB_DETAILS:存储Job的详细信息(哪个类,什么参数)。QRTZ_TRIGGERS:存储触发器的基本信息(关联哪个Job,状态是什么)。QRTZ_CRON_TRIGGERS/QRTZ_SIMPLE_TRIGGERS:分别存储Cron触发器和简单触发器的特有参数。QRTZ_FIRED_TRIGGERS:正在执行的触发器信息,用于故障恢复和集群协调。QRTZ_LOCKS:集群环境下用到的锁表。
拿到SQL后,在你的业务数据库里执行一遍。执行成功后,这11张表就是Quartz的“记忆中枢”了。以后所有任务的生命周期,都会在这些表里留下记录。
3. 核心实战:设计动态任务管理的数据模型与API
配置好了,表建好了,接下来就是重头戏:写代码实现动态管理。我们的目标是:通过一个Service,接收前端或内部调用传来的参数,就能随时创建、暂停、恢复或删除一个定时任务。
3.1 设计参数封装类:理解Quartz的表结构
要动态创建,首先得知道创建时需要哪些信息。最好的方式就是对照Quartz的几张核心表来设计我们的参数类。这样设计出来的模型最贴近Quartz的本质。
首先,是所有触发器都有的基础信息,对应QRTZ_TRIGGERS表:
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter
public class TriggerBaseParam {
/** 调度器名称,通常和配置里的instanceName一致 */
private String schedName;
/** 触发器名称 (必须) */
@NotEmpty(message = "触发器名称不能为空")
private String triggerName;
/** 触发器分组 (必须) */
@NotEmpty(message = "触发器分组不能为空")
private String triggerGroup;
/** 关联的Job名称 (必须) */
@NotEmpty(message = "Job名称不能为空")
private String jobName;
/** 关联的Job分组 (必须) */
@NotEmpty(message = "Job分组不能为空")
private String jobGroup;
/** 任务描述 */
private String description;
/** 触发器类型:CRON 或 SIMPLE (必须) */
@NotEmpty(message = "触发器类型不能为空")
private String triggerType;
/** 任务开始时间(时间戳) */
private Long startTime;
/** 任务结束时间(时间戳) */
private Long endTime;
/** 优先级 */
private Integer priority = 5;
}
然后,是创建任务时需要扩展的参数,它包含了具体执行类的信息,以及CRON和SIMPLE触发器特有的参数:
@Getter
@Setter
public class TriggerCreateParam extends TriggerBaseParam {
/** 核心!任务执行类的全限定名 (必须) */
@NotEmpty(message = "任务执行类不能为空")
private String jobClassName;
/** ====== CRON触发器专属参数 ====== */
private String cronExpression;
/** ====== SIMPLE触发器专属参数 ====== */
// 重复间隔(秒)
private Integer repeatInterval;
// 重复次数(0表示无限重复)
private Integer repeatCount = 0;
/** 任务参数(可选的扩展数据) */
private Map<String, Object> jobDataMap;
}
为什么这么设计?你看,TriggerBaseParam里的字段几乎都能在QRTZ_TRIGGERS表里找到对应列。而TriggerCreateParam里的jobClassName决定了任务具体做什么,cronExpression或repeatInterval/repeatCount则决定了任务何时触发。这种设计让我们的API既清晰,又和底层存储强关联。
3.2 实现动态创建方法:一行代码创建一个任务
有了参数模型,我们就可以编写核心的创建方法了。这个方法会做几件事:校验参数、反射加载任务类、构建JobDetail和Trigger,最后交给Scheduler。
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class DynamicQuartzService {
@Autowired
private Scheduler scheduler; // SpringBoot会自动注入
/**
* 动态创建并调度一个任务
* @param param 创建参数
* @throws SchedulerException 调度异常
* @throws ClassNotFoundException 任务类找不到
*/
public void createJob(TriggerCreateParam param) throws SchedulerException, ClassNotFoundException {
// 1. 参数基础校验
if (param == null || !StringUtils.hasText(param.getJobClassName())) {
throw new IllegalArgumentException("创建参数或任务类名无效");
}
// 2. 校验任务类是否存在且实现了Job接口
Class<? extends Job> jobClass;
try {
Class<?> clazz = Class.forName(param.getJobClassName());
if (!Job.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("任务类必须实现 org.quartz.Job 接口");
}
jobClass = (Class<? extends Job>) clazz;
} catch (ClassNotFoundException e) {
throw new ClassNotFoundException("未找到任务执行类: " + param.getJobClassName(), e);
}
// 3. 构建 JobDetail (任务详情)
JobBuilder jobBuilder = JobBuilder.newJob(jobClass)
.withIdentity(param.getJobName(), param.getJobGroup())
.withDescription(param.getDescription())
.storeDurably(); // 设置任务持久化,即使没有触发器关联也不会被删除
// 添加任务参数(JobDataMap)
if (param.getJobDataMap() != null && !param.getJobDataMap().isEmpty()) {
jobBuilder.usingJobData(new JobDataMap(param.getJobDataMap()));
}
JobDetail jobDetail = jobBuilder.build();
// 4. 构建 Trigger (触发器)
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger()
.withIdentity(param.getTriggerName(), param.getTriggerGroup())
.forJob(jobDetail) // 关联上面创建的Job
.withDescription(param.getDescription());
// 设置开始和结束时间
if (param.getStartTime() != null && param.getStartTime() > 0) {
triggerBuilder.startAt(new Date(param.getStartTime()));
} else {
triggerBuilder.startNow(); // 默认立即开始
}
if (param.getEndTime() != null && param.getEndTime() > 0) {
triggerBuilder.endAt(new Date(param.getEndTime()));
}
// 5. 根据触发器类型,构建不同的调度策略 (ScheduleBuilder)
ScheduleBuilder<?> scheduleBuilder;
String triggerType = param.getTriggerType().toUpperCase();
switch (triggerType) {
case "CRON":
if (!StringUtils.hasText(param.getCronExpression())) {
throw new IllegalArgumentException("CRON触发器必须提供cron表达式");
}
// 这里可以增加cron表达式合法性校验
scheduleBuilder = CronScheduleBuilder.cronSchedule(param.getCronExpression());
break;
case "SIMPLE":
if (param.getRepeatInterval() == null || param.getRepeatInterval() <= 0) {
throw new IllegalArgumentException("SIMPLE触发器必须提供有效的重复间隔(>0)");
}
Integer count = param.getRepeatCount();
if (count == null || count < 0) {
throw new IllegalArgumentException("重复次数必须>=0 (0表示无限)");
}
SimpleScheduleBuilder simpleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(param.getRepeatInterval());
if (count == 0) {
simpleBuilder.repeatForever();
} else {
simpleBuilder.withRepeatCount(count - 1); // Quartz的重复次数包含第一次执行
}
scheduleBuilder = simpleBuilder;
break;
default:
throw new IllegalArgumentException("不支持的触发器类型: " + triggerType + ",仅支持 CRON 或 SIMPLE");
}
// 6. 组装触发器并提交给调度器
Trigger trigger = triggerBuilder.withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
// 可选:如果调度器是暂停的,这里可以调用 scheduler.start() 确保任务被调度
}
}
这段代码有几个关键点我实测下来特别重要:
storeDurably(true):这个设置保证了即使这个Job暂时没有Trigger关联(比如你把触发器删了),JobDetail也不会从数据库里被清除。这对于需要复用Job定义的场景很有用。- 重复次数的计算:
SimpleScheduleBuilder的withRepeatCount(count)参数指的是额外的重复次数。比如你想总共执行5次,那么这里应该传4。我在代码里做了处理,让用户直接传期望的总次数(0表示无限),内部再减1,这样更符合直觉。 - 异常处理:对类名、参数做了严格校验,并在异常信息里给出明确提示,方便调试。
3.3 编写你的第一个Job执行类
任务创建好了,总得有个地方写具体的业务逻辑。这就是Job接口的实现类。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 一个示例任务:清理临时文件
*/
public class TempFileCleanupJob implements Job {
private static final Logger logger = LoggerFactory.getLogger(TempFileCleanupJob.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 1. 可以从上下文获取任务参数
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String tempDirPath = dataMap.getString("tempDir"); // 获取创建任务时传入的参数
int retentionDays = dataMap.getIntValue("retentionDays");
logger.info("开始执行临时文件清理任务,目录:{},保留天数:{}", tempDirPath, retentionDays);
// 2. 这里写下你的实际业务逻辑,例如:
// File tempDir = new File(tempDirPath);
// ... 清理旧文件的逻辑 ...
// 3. 模拟一个执行结果
int deletedFiles = 15; // 假设删除了15个文件
logger.info("临时文件清理完成,共删除 {} 个文件", deletedFiles);
// 你可以把结果放回上下文,供监听器使用
context.setResult(deletedFiles);
}
}
这个类很简单,但体现了Quartz Job的核心:实现execute方法。所有动态创建的任务,最终都会指向这样一个具体的类。通过JobExecutionContext,你能拿到任务的身份信息、触发时间、以及创建时传入的jobDataMap,这让任务逻辑非常灵活。
4. 动态任务管理进阶:暂停、恢复、删除与查询
只会创建还不够,一个完整的动态管理系统必须能对任务进行全生命周期管理。
4.1 暂停与恢复触发器:让任务临时“休息”
有时候,你可能需要临时关闭某个任务(比如系统维护),但又不想删除它,因为以后还要用。这时就需要暂停和恢复功能。
public class DynamicQuartzService {
// ... 接上面的代码
/**
* 暂停一个触发器(任务将不再触发)
*/
public void pauseTrigger(String triggerName, String triggerGroup) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);
scheduler.pauseTrigger(triggerKey);
logger.info("已暂停触发器: {}.{}", triggerGroup, triggerName);
}
/**
* 恢复一个触发器(任务将按照原计划继续触发)
*/
public void resumeTrigger(String triggerName, String triggerGroup) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);
scheduler.resumeTrigger(triggerKey);
logger.info("已恢复触发器: {}.{}", triggerGroup, triggerName);
}
/**
* 暂停整个Job(它关联的所有触发器都会暂停)
*/
public void pauseJob(String jobName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
scheduler.pauseJob(jobKey);
logger.info("已暂停Job及其所有触发器: {}.{}", jobGroup, jobName);
}
/**
* 恢复整个Job
*/
public void resumeJob(String jobName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
scheduler.resumeJob(jobKey);
logger.info("已恢复Job及其所有触发器: {}.{}", jobGroup, jobName);
}
}
这里要注意pauseTrigger和pauseJob的区别。一个Job(比如“发送日报”)可以关联多个触发器(比如“早上9点发”和“下午5点发”)。暂停触发器只影响那一个触发规则,暂停Job则会让这个任务的所有触发规则都失效。根据你的业务场景选择。
4.2 删除任务与触发器:彻底清理
如果某个任务再也不需要了,就应该删除它以释放资源。删除也有两种粒度:删除触发器,或者连JobDetail一起删除。
public boolean deleteJob(String jobName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
// 第二个参数为true,表示如果这个Job还有关联的触发器,也一并删除
boolean result = scheduler.deleteJob(jobKey);
if (result) {
logger.info("已删除Job及其关联的触发器: {}.{}", jobGroup, jobName);
} else {
logger.warn("删除Job失败,可能不存在: {}.{}", jobGroup, jobName);
}
return result;
}
public boolean unscheduleJob(String triggerName, String triggerGroup) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);
// 只移除触发器,不删除JobDetail
boolean result = scheduler.unscheduleJob(triggerKey);
if (result) {
logger.info("已移除触发器: {}.{},对应的JobDetail仍保留", triggerGroup, triggerName);
}
return result;
}
deleteJob是更彻底的操作,通常用于任务完全下线。unscheduleJob则更温和,只是取消调度,JobDetail还留在数据库里,以后可以用新的触发器重新关联它。
4.3 查询与监控:掌握任务状态
管理后台总得有个列表页展示所有任务吧?这就需要查询功能。我们可以直接从注入的Scheduler里获取信息,也可以去查我们建的那11张数据库表。这里展示通过Scheduler API查询的方式,更实时。
public List<Map<String, Object>> getAllJobs() throws SchedulerException {
List<Map<String, Object>> jobList = new ArrayList<>();
// 获取所有Job组名
for (String groupName : scheduler.getJobGroupNames()) {
// 获取该组下的所有JobKey
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
Map<String, Object> jobInfo = new HashMap<>();
jobInfo.put("jobName", jobKey.getName());
jobInfo.put("jobGroup", jobKey.getGroup());
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobInfo.put("description", jobDetail.getDescription());
jobInfo.put("jobClass", jobDetail.getJobClass().getName());
// 获取这个Job关联的所有触发器
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
List<Map<String, Object>> triggerInfos = new ArrayList<>();
for (Trigger trigger : triggers) {
Map<String, Object> triggerInfo = new HashMap<>();
TriggerKey triggerKey = trigger.getKey();
triggerInfo.put("triggerName", triggerKey.getName());
triggerInfo.put("triggerGroup", triggerKey.getGroup());
triggerInfo.put("triggerState", scheduler.getTriggerState(triggerKey).toString());
triggerInfo.put("nextFireTime", trigger.getNextFireTime());
triggerInfo.put("previousFireTime", trigger.getPreviousFireTime());
if (trigger instanceof CronTrigger) {
triggerInfo.put("cronExpression", ((CronTrigger) trigger).getCronExpression());
triggerInfo.put("type", "CRON");
} else if (trigger instanceof SimpleTrigger) {
triggerInfo.put("repeatInterval", ((SimpleTrigger) trigger).getRepeatInterval());
triggerInfo.put("repeatCount", ((SimpleTrigger) trigger).getRepeatCount());
triggerInfo.put("type", "SIMPLE");
}
triggerInfos.add(triggerInfo);
}
jobInfo.put("triggers", triggerInfos);
jobList.add(jobInfo);
}
}
return jobList;
}
这个方法返回的数据结构已经足够丰富,可以让你在前端画出一个清晰的任务管理列表,展示每个任务的状态、下次执行时间、触发器类型等关键信息。
5. 生产环境必备:监听器、异常处理与集群考量
功能跑通只是第一步,要上生产环境,还得考虑健壮性和可观测性。
5.1 使用监听器监控任务生命周期
Quartz提供了强大的监听器(Listener)机制,让你能在任务执行的关键节点插入自定义逻辑,比如记录日志、发送告警、统计耗时。
最常用的是JobListener,它可以监听Job的执行开始、结束和是否被否决(Veto)。
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class GlobalJobExecutionListener implements JobListener {
private static final Logger logger = LoggerFactory.getLogger(GlobalJobExecutionListener.class);
@Override
public String getName() {
return "GlobalJobExecutionListener";
}
// Job即将被执行时
@Override
public void jobToBeExecuted(JobExecutionContext context) {
String jobKey = context.getJobDetail().getKey().toString();
logger.info("任务 [{}] 开始执行,触发时间: {}", jobKey, context.getFireTime());
// 可以在这里记录开始时间,用于后续计算耗时
context.put("startTime", System.currentTimeMillis());
}
// Job执行被否决时(例如,触发器被暂停了)
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
logger.warn("任务 [{}] 执行被否决", context.getJobDetail().getKey());
}
// Job执行完毕后
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
String jobKey = context.getJobDetail().getKey().toString();
Long startTime = (Long) context.get("startTime");
long cost = startTime != null ? System.currentTimeMillis() - startTime : 0;
if (jobException != null) {
// 任务执行出错了!
logger.error("任务 [{}] 执行失败!耗时: {}ms, 异常: {}", jobKey, cost, jobException.getMessage(), jobException);
// 这里可以接入你的告警系统,发邮件、发钉钉等
// alertService.sendAlert("Quartz任务执行失败", jobKey, jobException);
} else {
// 任务执行成功
Object result = context.getResult();
logger.info("任务 [{}] 执行成功!耗时: {}ms, 结果: {}", jobKey, cost, result);
}
}
}
写好了监听器,还需要把它注册到Scheduler上。可以在配置类里做:
@Configuration
public class QuartzConfig {
@Autowired
private GlobalJobExecutionListener globalJobListener;
@Autowired
private Scheduler scheduler;
@PostConstruct
public void addListeners() throws SchedulerException {
// 添加全局Job监听器,监听所有Job
scheduler.getListenerManager().addJobListener(globalJobListener);
// 你也可以添加TriggerListener、SchedulerListener
}
}
有了监听器,所有任务的执行情况就尽在掌握了。出问题时能第一时间收到通知,排查日志也一目了然。
5.2 异常处理与任务恢复策略
任务执行难免会出错。Quartz提供了misfire(错失触发)处理机制。简单说,就是本该在某个时间点触发的任务,因为调度器关闭、线程池满、或者任务本身执行时间过长错过了下一次触发时间,这时候该怎么处理?
我们在创建触发器时可以指定misfire策略。这是一个非常实用的高级特性。
// 在创建CRON触发器时
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
// 设置错失触发后的处理策略:以当前时间立刻触发一次,然后按原计划继续
cronScheduleBuilder.withMisfireHandlingInstructionFireAndProceed();
// 或者:忽略错过的触发,等待下一次触发(更常用)
// cronScheduleBuilder.withMisfireHandlingInstructionDoNothing();
Trigger cronTrigger = triggerBuilder.withSchedule(cronScheduleBuilder).build();
// 在创建SIMPLE触发器时
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(interval)
.withRepeatCount(count);
// 错失触发后,立即触发,并将错过的次数补上(慎用,可能引发雪崩)
simpleScheduleBuilder.withMisfireHandlingInstructionFireNow();
// 或者:忽略错过的,从现在开始重新计算下次触发时间(推荐)
// simpleScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
Trigger simpleTrigger = triggerBuilder.withSchedule(simpleScheduleBuilder).build();
选择哪种策略取决于你的业务。对于每天一次的报表任务,错过一次可能用FireAndProceed立刻补跑一次就行。对于每秒都在跑的监控任务,错过一大堆,用IgnoreMisfires忽略掉,直接开始下一个周期可能更合适。FireNow补跑所有错过的次数,在间隔短、错过多的情况下可能导致系统瞬间压力过大。
5.3 集群部署下的注意事项
当你把应用部署到多个节点时,Quartz的集群模式就派上用场了。我们之前在配置里已经打开了isClustered: true,这会让Quartz通过数据库的行锁来协调各个节点,保证同一个任务在集群中只会被一个节点执行。
集群配置有几个关键点:
- 相同的
scheduler.instanceName:集群内所有节点的调度器实例名必须相同,它们属于同一个逻辑调度器。 - 不同的
scheduler.instanceId:设置为AUTO即可,Quartz会自动生成唯一的ID。 - 相同的数据源:所有节点必须连接同一个数据库,这是它们通信和协调的基础。
- 时间同步:集群内所有服务器的时间必须同步(使用NTP服务),否则会导致触发时间计算混乱。
在集群模式下,监听器会在实际执行任务的节点上被触发。你的日志和监控系统需要能聚合所有节点的日志,才能看到完整的任务执行情况。
6. 真实场景案例:构建一个动态报表生成系统
光讲理论有点干,我们用一个贴近业务的例子来串一下全过程。假设我们要做一个“动态报表生成系统”,管理员可以在后台随时添加新的报表生成任务。
第一步:设计数据库表(业务表) 除了Quartz那11张系统表,我们还需要一张业务表来保存任务的定义和元信息。
CREATE TABLE `report_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_name` varchar(100) NOT NULL COMMENT '任务名称',
`report_type` varchar(50) NOT NULL COMMENT '报表类型(用户日报、订单周报等)',
`cron_expression` varchar(50) NOT NULL COMMENT 'CRON表达式',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-启用,0-停用',
`params_json` text COMMENT '报表生成参数(JSON格式)',
`creator` varchar(50) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_name` (`task_name`)
) ENGINE=InnoDB COMMENT='报表任务定义表';
第二步:编写报表生成Job
public class ReportGenerationJob implements Job {
@Autowired
private ReportService reportService; // 你的业务报表服务
@Override
public void execute(JobExecutionContext context) {
JobDataMap dataMap = context.getMergedJobDataMap(); // 注意用merged,它合并了JobDetail和Trigger的数据
String reportType = dataMap.getString("reportType");
String taskId = dataMap.getString("taskId");
String paramsJson = dataMap.getString("params");
// 解析参数,调用报表服务
ReportParams params = JSON.parseObject(paramsJson, ReportParams.class);
try {
String reportFilePath = reportService.generateReport(reportType, params);
logger.info("报表生成成功,任务ID: {}, 文件路径: {}", taskId, reportFilePath);
// 可以在这里把文件路径存库,或者发通知
} catch (Exception e) {
logger.error("报表生成失败,任务ID: {}", taskId, e);
// 失败重试逻辑可以在这里实现,或者抛出JobExecutionException让Quartz处理
throw new JobExecutionException(e, true); // true表示希望Quartz重新调度该任务
}
}
}
第三步:提供管理API
基于我们前面写的DynamicQuartzService,包装一个ReportTaskService。
@Service
public class ReportTaskService {
@Autowired
private DynamicQuartzService quartzService;
@Autowired
private ReportTaskMapper reportTaskMapper; // MyBatis Mapper
public void createReportTask(ReportTaskCreateRequest request) {
// 1. 保存任务定义到业务表
ReportTask task = convertToEntity(request);
reportTaskMapper.insert(task);
// 2. 动态创建Quartz任务
TriggerCreateParam param = new TriggerCreateParam();
param.setJobName("REPORT_JOB_" + task.getId());
param.setJobGroup("REPORT_GROUP");
param.setTriggerName("REPORT_TRIGGER_" + task.getId());
param.setTriggerGroup("REPORT_GROUP");
param.setTriggerType("CRON");
param.setCronExpression(task.getCronExpression());
param.setJobClassName("com.yourcompany.job.ReportGenerationJob");
// 传递业务参数
Map<String, Object> jobData = new HashMap<>();
jobData.put("reportType", task.getReportType());
jobData.put("taskId", String.valueOf(task.getId()));
jobData.put("params", task.getParamsJson());
param.setJobDataMap(jobData);
quartzService.createJob(param);
}
public void toggleTaskStatus(Long taskId, boolean active) throws SchedulerException {
ReportTask task = reportTaskMapper.selectById(taskId);
String triggerName = "REPORT_TRIGGER_" + taskId;
String triggerGroup = "REPORT_GROUP";
if (active) {
quartzService.resumeTrigger(triggerName, triggerGroup);
task.setStatus(1);
} else {
quartzService.pauseTrigger(triggerName, triggerGroup);
task.setStatus(0);
}
reportTaskMapper.updateById(task);
}
// ... 其他方法:修改Cron表达式、立即触发一次、删除任务等
}
第四步:前端管理界面 提供一个简单的管理页面,可以列表展示所有报表任务(状态、下次执行时间)、提供“新增”、“启用/禁用”、“编辑”、“立即执行”、“查看日志”等按钮。后端接口就调用我们上面写的Service。
这样一个完整的、可动态管理的报表调度系统就搭建起来了。它具备了生产环境需要的所有要素:持久化、动态增删改查、状态监控、错误处理。你可以根据这个模式,把它扩展到任何需要定时执行的业务场景,比如数据同步、缓存刷新、消息推送等等。
更多推荐

所有评论(0)