一、组件设计目标
解决痛点:
- 简化 Quartz 原生 API 的复杂性
- 统一任务调度管理(增删改查、日志、重试)
- 与 Spring Boot 生态无缝整合
二、实现步骤详解
1. 组件初始化配置
1.1 初始化 Quartz 表结构
下载 SQL 脚本
🔗 官方表结构下载地址:Quartz
根据 Quartz 版本选择对应 SQL 文件
1.2 引入pom依赖
<!-- pom.xml -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
1.3 引入yaml配置
# application.yml
# Quartz 配置项,对应 QuartzProperties 配置类
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/your_db?useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverautoconfigure:exclude:#排除Quartz自动配置即关闭定时任务,注释掉该配置即打开定时任务- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 不开启 Quartz 的自动配置quartz:auto-startup: true # 本地开发环境,尽量不要开启 Jobscheduler-name: schedulerName # Scheduler 名字。默认为 schedulerNamejob-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 trueproperties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档org:quartz:# Scheduler 相关配置scheduler:instanceName: schedulerNameinstanceId: AUTO # 自动生成 instance ID# JobStore 相关配置jobStore:# JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162class: org.springframework.scheduling.quartz.LocalDataSourceJobStoreisClustered: true # 是集群模式clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒misfireThreshold: 60000 # misfire 阀值,单位:毫秒。# 线程池相关配置threadPool:threadCount: 25 # 线程池大小。默认为 10 。threadPriority: 5 # 线程优先级class: org.quartz.simpl.SimpleThreadPool # 线程池类型jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
1.4 Spring Boot 2.7 与 3.0 自动配置声明对比
1. 文件路径与作用
文件类型 | 路径 | 用途说明 |
---|---|---|
AutoConfiguration.imports | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 声明自动配置类(Spring Boot 2.7+ 新增) |
spring.factories | META-INF/spring.factories | 旧版声明方式(Spring Boot 2.7+ 兼容,直到3.0 废弃spring.factories 中自动配置类声明。) |
2. 版本差异详解
Spring Boot 2.7
- 兼容模式: 同时支持两种声明方式,优先级:
AutoConfiguration.imports > spring.factories - 推荐做法: 使用新的 AutoConfiguration.imports 文件
Spring Boot 3.0
- 强制要求: 自动配置类 必须 通过 AutoConfiguration.imports 声明
- 废弃内容: spring.factories 中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 条目失效
- 保留内容: 其他非自动配置条目(如监听器、模板引擎)仍可保留在 spring.factories
3. 文件内容对比
Spring Boot 2.7+ 配置
在 resources/META-INF 目录下创建文件:
文件路径:
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容:
# 声明自定义的自动配置类
cn.iocoder.dyh.framework.quartz.config.DyhQuartzAutoConfiguration
cn.iocoder.dyh.framework.quartz.config.DyhAsyncAutoConfiguration
Spring Boot 2.6 及以下配置
在 resources/META-INF 目录下创建文件:
文件路径:
src/main/resources/META-INF/spring.factories
文件内容:
# 声明自定义的自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\cn.iocoder.dyh.framework.quartz.config.DyhQuartzAutoConfiguration,\cn.iocoder.dyh.framework.quartz.config.DyhAsyncAutoConfiguration
1.5 自定义自动配置类
DyhAsyncAutoConfiguration
/*** 异步任务 Configuration*/
@AutoConfiguration// 1️⃣ Spring Boot自动配置注解,标识这是一个自动配置类
@EnableAsync// 2️⃣ 启用Spring的异步方法执行功能
public class DyhAsyncAutoConfiguration {@Bean// 3️⃣ 声明该方法返回的对象会被注册为Spring Beanpublic BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() {return new BeanPostProcessor() { // 4️⃣ 创建BeanPostProcessor实现(Bean生命周期处理器)@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {if (!(bean instanceof ThreadPoolTaskExecutor)) { // 5️⃣ 仅处理ThreadPoolTaskExecutor类型的Beanreturn bean;}// 修改提交的任务,接入 TransmittableThreadLocalThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; // 6️⃣ 类型转换executor.setTaskDecorator(TtlRunnable::get); // 7️⃣ 设置任务装饰器(用于TTL线程池上下文传递)return executor;}};}
}
DyhQuartzAutoConfiguration
@AutoConfiguration // 1️⃣ Spring Boot自动配置注解,标识这是一个自动配置类
@EnableScheduling // 2️⃣ 启用Spring自带的定时任务功能(@Scheduled注解生效)
@Slf4j // 3️⃣ Lombok注解,自动生成日志对象 log
public class DyhQuartzAutoConfiguration {@Bean // 4️⃣ 声明该方法返回的对象会被注册为Spring Beanpublic SchedulerManager schedulerManager(Optional<Scheduler> scheduler) { // 5️⃣ 使用Optional包装的Quartz调度器if (!scheduler.isPresent()) { // 6️⃣ 检测是否没有Scheduler实例log.info("[定时任务 - 已禁用]");return new SchedulerManager(null); // 7️⃣ 创建禁用状态的调度管理器}return new SchedulerManager(scheduler.get()); // 8️⃣ 创建正常工作的调度管理器}
}
1.6 相关类
JobDataKeyEnum
/*** Quartz Job Data 的 key 枚举*/
public enum JobDataKeyEnum {JOB_ID,JOB_HANDLER_NAME,JOB_HANDLER_PARAM,JOB_RETRY_COUNT, // 最大重试次数JOB_RETRY_INTERVAL, // 每次重试间隔
}
JobLogFrameworkService
/*** Job 日志 Framework Service 接口** @author dyh*/
public interface JobLogFrameworkService {/*** 创建 Job 日志** @param jobId 任务编号* @param beginTime 开始时间* @param jobHandlerName Job 处理器的名字* @param jobHandlerParam Job 处理器的参数* @param executeIndex 第几次执行* @return Job 日志的编号*/Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId,@NotNull(message = "开始时间") LocalDateTime beginTime,@NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName,String jobHandlerParam,@NotNull(message = "第几次执行不能为空") Integer executeIndex);/*** 更新 Job 日志的执行结果** @param logId 日志编号* @param endTime 结束时间。因为是异步,避免记录时间不准去* @param duration 运行时长,单位:毫秒* @param success 是否成功* @param result 成功数据*/void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId,@NotNull(message = "结束时间不能为空") LocalDateTime endTime,@NotNull(message = "运行时长不能为空") Integer duration,boolean success, String result);
}
CronUtils
/*** Quartz Cron 表达式的工具类** @author dyh*/
public class CronUtils {/*** 校验 CRON 表达式是否有效** @param cronExpression CRON 表达式* @return 是否有效*/public static boolean isValid(String cronExpression) {return CronExpression.isValidExpression(cronExpression);}/*** 基于 CRON 表达式,获得下 n 个满足执行的时间** @param cronExpression CRON 表达式* @param n 数量* @return 满足条件的执行时间*/public static List<LocalDateTime> getNextTimes(String cronExpression, int n) {// 1. 获得 CronExpression 对象CronExpression cron;try {cron = new CronExpression(cronExpression);} catch (ParseException e) {throw new IllegalArgumentException(e.getMessage());}// 2. 从当前开始计算,n 个满足条件的Date now = new Date();List<LocalDateTime> nextTimes = new ArrayList<>(n);for (int i = 0; i < n; i++) {Date nextTime = cron.getNextValidTimeAfter(now);// 2.1 如果 nextTime 为 null,说明没有更多的有效时间,退出循环if (nextTime == null) {break;}nextTimes.add(LocalDateTimeUtil.of(nextTime));// 2.2 切换现在,为下一个触发时间;now = nextTime;}return nextTimes;}}
2. 核心模块实现
2.1 任务调度管理(SchedulerManager)
/*** {@link Scheduler} 的管理器,负责创建任务** 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即:* 1. Job 的 {@link JobDetail#getKey()}* 2. Trigger 的 {@link Trigger#getKey()}** 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用** @author dyh*/
public class SchedulerManager {// Quartz调度器核心对象private final Scheduler scheduler;// 通过依赖注入获取Quartz调度器实例public SchedulerManager(Scheduler scheduler) {this.scheduler = scheduler;}/*** 添加 Job 到 Quartz 中** @param jobId 任务编号* @param jobHandlerName 任务处理器的名字* @param jobHandlerParam 任务处理器的参数* @param cronExpression CRON 表达式* @param retryCount 重试次数* @param retryInterval 重试间隔* @throws SchedulerException 添加异常*/public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression,Integer retryCount, Integer retryInterval)throws SchedulerException {validateScheduler();// 创建 JobDetail 对象JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class).usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId).usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName).withIdentity(jobHandlerName).build();// 创建 Trigger 对象Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);// 新增 Job 调度scheduler.scheduleJob(jobDetail, trigger);}/*** 更新 Job 到 Quartz** @param jobHandlerName 任务处理器的名字* @param jobHandlerParam 任务处理器的参数* @param cronExpression CRON 表达式* @param retryCount 重试次数* @param retryInterval 重试间隔* @throws SchedulerException 更新异常*/public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression,Integer retryCount, Integer retryInterval)throws SchedulerException {validateScheduler();// 创建新 Trigger 对象Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);// 修改调度scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger);}/*** 删除 Quartz 中的 Job** @param jobHandlerName 任务处理器的名字* @throws SchedulerException 删除异常*/public void deleteJob(String jobHandlerName) throws SchedulerException {validateScheduler();// 暂停 Trigger 对象scheduler.pauseTrigger(new TriggerKey(jobHandlerName));// 取消并删除 Job 调度scheduler.unscheduleJob(new TriggerKey(jobHandlerName));scheduler.deleteJob(new JobKey(jobHandlerName));}/*** 暂停 Quartz 中的 Job** @param jobHandlerName 任务处理器的名字* @throws SchedulerException 暂停异常*/public void pauseJob(String jobHandlerName) throws SchedulerException {validateScheduler();scheduler.pauseJob(new JobKey(jobHandlerName));}/*** 启动 Quartz 中的 Job** @param jobHandlerName 任务处理器的名字* @throws SchedulerException 启动异常*/public void resumeJob(String jobHandlerName) throws SchedulerException {validateScheduler();scheduler.resumeJob(new JobKey(jobHandlerName));scheduler.resumeTrigger(new TriggerKey(jobHandlerName));}/*** 立即触发一次 Quartz 中的 Job** @param jobId 任务编号* @param jobHandlerName 任务处理器的名字* @param jobHandlerParam 任务处理器的参数* @throws SchedulerException 触发异常*/public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam)throws SchedulerException {validateScheduler();// 触发任务JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryIntervaldata.put(JobDataKeyEnum.JOB_ID.name(), jobId);data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName);data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam);scheduler.triggerJob(new JobKey(jobHandlerName), data);}private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression,Integer retryCount, Integer retryInterval) {return TriggerBuilder.newTrigger().withIdentity(jobHandlerName).withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam).usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount).usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval).build();}private void validateScheduler() {if (scheduler == null) {throw exception0(NOT_IMPLEMENTED.getCode(),"[定时任务 - 已禁用]");}}}
2.2 任务执行器(JobHandlerInvoker)
/*** 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务** @author dyh*/
@DisallowConcurrentExecution // 禁止同一JobDetail并发执行
@PersistJobDataAfterExecution // 执行后持久化JobDataMap数据
@Slf4j
public class JobHandlerInvoker extends QuartzJobBean {@Resourceprivate ApplicationContext applicationContext;@Resourceprivate JobLogFrameworkService jobLogFrameworkService;@Overrideprotected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException {// 第一步,获得 Job 数据Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name());String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name());String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name());int refireCount = executionContext.getRefireCount();int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0);int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0);// 第二步,执行任务Long jobLogId = null;LocalDateTime startTime = LocalDateTime.now();String data = null;Throwable exception = null;try {// 记录 Job 日志(初始)jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1);// 执行任务data = this.executeInternal(jobHandlerName, jobHandlerParam);} catch (Throwable ex) {exception = ex;}// 第三步,记录执行日志this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext);// 第四步,处理有异常的情况handleException(exception, refireCount, retryCount, retryInterval);}private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception {// 获得 JobHandler 对象JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class);Assert.notNull(jobHandler, "JobHandler 不会为空");// 执行任务return jobHandler.execute(jobHandlerParam);}private void updateJobLogResultAsync(Long jobLogId, LocalDateTime startTime, String data, Throwable exception,JobExecutionContext executionContext) {LocalDateTime endTime = LocalDateTime.now();// 处理是否成功boolean success = exception == null;if (!success) {data = getRootCauseMessage(exception);}// 更新日志try {jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) LocalDateTimeUtil.between(startTime, endTime).toMillis(), success, data);} catch (Exception ex) {log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]",executionContext.getJobDetail().getKey(), jobLogId, success, data);}}private void handleException(Throwable exception,int refireCount, int retryCount, int retryInterval) throws JobExecutionException {// 如果有异常,则进行重试if (exception == null) {return;}// 情况一:如果到达重试上限,则直接抛出异常即可if (refireCount >= retryCount) {throw new JobExecutionException(exception);}// 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试// 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。if (retryInterval > 0) {ThreadUtil.sleep(retryInterval);}// 第二个参数,refireImmediately = true,表示立即重试throw new JobExecutionException(exception, true);}}
2.3 任务处理器(JobHandler)
/*** 任务处理器** @author dyh*/
public interface JobHandler {/*** 执行任务** @param param 参数* @return 结果* @throws Exception 异常*/String execute(String param) throws Exception;}
三、核心设计模式解析
1. 策略模式(Strategy Pattern)
应用场景:不同业务任务的具体实现
public interface JobHandler {String execute(String param) throws Exception;
}@Component
public class DemoJob implements JobHandler {@Overridepublic String execute(String param) {// 具体业务逻辑}
}
2. 模板方法模式(Template Method)
应用场景:任务执行流程标准化
此代码为org.springframework.scheduling.quartz包下源码
public abstract class QuartzJobBean implements Job {public QuartzJobBean() {}//不可变的流程用final修饰生成流程骨架public final void execute(JobExecutionContext context) throws JobExecutionException {try {BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);MutablePropertyValues pvs = new MutablePropertyValues();pvs.addPropertyValues(context.getScheduler().getContext());pvs.addPropertyValues(context.getMergedJobDataMap());bw.setPropertyValues(pvs, true);} catch (SchedulerException var4) {SchedulerException ex = var4;throw new JobExecutionException(ex);}this.executeInternal(context);}//可变部分抽象出来由具体子类实现功能protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
}
3. 工厂方法模式(Factory Method)
间接应用:通过 Spring 容器获取 JobHandler 实例
// 获得 JobHandler 对象
JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class);
4. 装饰器模式(Decorator)
应用场景:TTL 异步任务上下文传递
executor.setTaskDecorator(TtlRunnable::get); // 增强 Runnable
四、组件运行流程
1. 组件运行流程图
┌───────────────────────┐ ┌───────────────────────┐
│ Spring Boot │ │ Quartz Scheduler │
│ Application │ │ (Cluster Mode) │
└───────────┬───────────┘ └───────────┬───────────┘│ ││ 1. 自动配置触发 │├─────────────────────────────────▶│ - @AutoConfiguration ││ - AutoConfiguration.imports ││ ││ 2. 初始化 SchedulerManager ││ (依赖注入 Scheduler) ││ ││ 3. 业务系统调用 ││ SchedulerManager.addJob() ││ ││ 4. 创建 JobDetail & Trigger ││ (存储到 JDBC JobStore) ││ │
┌───────────▼───────────┐ ┌───────────▼───────────┐
│ Quartz JobStore │ │ ThreadPoolTaskExecutor
│ (Database) │ │ (异步日志记录) │
└───────────┬───────────┘ └───────────▲───────────┘│ ││ 5. 调度触发 ││ (Cron 表达式) ││ ││ 6. 执行 JobHandlerInvoker ││ (extends QuartzJobBean) ││ │
┌───────────▼───────────┐ │
│ JobHandler 策略模式 │ │
│ (具体业务实现类) │ │
│ - DemoJob.execute() │ │
│ - OrderTimeoutJob │ │
└───────────┬───────────┘ ││ ││ 7. 执行结果/异常 │├─────────────────────────────────▶│ JobLogFrameworkService ││ .updateJobLogResultAsync() ││ ││ 8. 异常重试机制 ││ (handleException 方法) ││ ││ │
└───────────────────────┘ └───────────────────────┘
2. 流程关键步骤说明
- 自动配置阶段
- Spring Boot 通过 AutoConfiguration.imports 加载 DyhQuartzAutoConfiguration
- 初始化 SchedulerManager 并注入 Quartz Scheduler
- 任务注册阶段
- 业务代码调用 SchedulerManager.addJob() 方法
- 创建 JobDetail(绑定 JobHandlerInvoker)和 Trigger
- 任务配置持久化到数据库(JDBC JobStore)
- 调度触发阶段
- Quartz Scheduler 根据 Cron 表达式触发任务
- 集群模式下通过数据库锁实现任务分片
- 任务执行阶段
- JobHandlerInvoker 执行模板方法:
- a. 获取任务参数(JobDataMap)
- b. 记录初始日志(JobLogFrameworkService.createJobLog())
- c. 策略模式 调用具体 JobHandler.execute()
- d. 异步更新日志结果(updateJobLogResultAsync())
- e. 异常重试机制(handleException)
- JobHandlerInvoker 执行模板方法:
- 异步日志处理
- 使用 ThreadPoolTaskExecutor 异步记录日志
- 通过 TtlRunnable 实现线程池上下文传递(TransmittableThreadLocal)
3. 设计模式标注
- 🎯 策略模式:JobHandler 接口统一任务执行入口
- 📜 模板方法:QuartzJobBean 定义任务执行骨架
- 🏭 工厂模式:Spring 容器管理 JobHandler 实例
- 🎨 装饰器模式:TtlRunnable 增强线程池任务
五、完整使用示例
1. 定义具体策略的任务处理器
@Component
@Slf4j
public class OrderTimeoutJob implements JobHandler {@Overridepublic String execute(String orderId) {log.info("[订单超时检查] 订单ID: {}", orderId);// 业务逻辑:关闭超时订单return "已关闭订单: " + orderId;}
}
2. 注册定时任务
说明:注册定时任务可以自建业务类对接该自定义定时任务组件持久化到数据库表,这样就可以像xxl-job一样界面上操作定时任务了,在这里写就篇幅太长了,有空我写了把链接贴过来。
@Autowired
private SchedulerManager schedulerManager;// 添加任务
public void addOrderTimeoutJob(Long jobId, String orderId) throws SchedulerException {schedulerManager.addJob(jobId, "orderTimeoutJob", // Bean 名称orderId, // 参数"0 0 0 * * ?", // CRON3, // 最大重试次数5000 // 重试间隔(毫秒));
}
六、组件优势总结
- 开箱即用:通过 Starter 自动配置,快速集成
- 灵活扩展:基于策略模式轻松添加新任务类型
- 企业级特性:
- 分布式调度(基于 Quartz JDBC JobStore)
- 任务日志追踪
- 失败重试机制
- 生产就绪:完善的异常处理与防御性编程