4. Quartz核心原理:Job、Trigger、Scheduler三大核心接口
Quartz这套东西,说白了就是三个角色在跳舞:Job(干什么活)、Trigger(什么时候干)、Scheduler(谁来安排)。我最早接触Quartz是在一个主数据同步项目里,当时被这哥仨的关系绕了好几天。今天咱们就把它们拆开揉碎了讲清楚。
4.1 三大核心接口:各司其职
先看一张整体关系图,心里有个谱:
4.2 Job 接口:你的业务逻辑放这里
Job 接口只有一个方法:execute(JobExecutionContext context)。你想想看,Quartz 每次触发任务时,就调用这个方法。我见过不少新手直接把数据库连接、文件IO写在 execute 里,结果任务一多就出问题。
public class DataSyncJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
// 你的业务逻辑写在这里
System.out.println("开始同步主数据...");
// 从 JobDataMap 拿参数
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String sourceTable = dataMap.getString("sourceTable");
// 执行同步...
}
}
4.3 JobDetail 与 JobDataMap:任务的身份证和行李包
JobDetail 是任务的元数据描述。它告诉 Scheduler:这个任务叫什么名字、属于哪个组、执行哪个 Job 类。
JobDataMap 是附着在 JobDetail 上的参数容器。你可以往里面塞任何 Serializable 对象。我在主数据项目里,经常用它传递数据源标识、表名、批次号这些运行时参数。
JobDetail job = JobBuilder.newJob(DataSyncJob.class)
.withIdentity("syncCustomer", "mdmGroup")
.usingJobData("sourceTable", "CUSTOMER_MASTER")
.usingJobData("targetSystem", "ERP")
.build();
| 概念 | 类比 | 关键点 |
|---|---|---|
| JobDetail | 身份证 | 标识任务身份、绑定 Job 实现类 |
| JobDataMap | 行李包 | 存储运行时参数,可序列化 |
| Job 实例 | 干活的人 | 每次执行可能新建实例(取决于配置) |
4.4 Trigger:什么时候干活,你说了算
Trigger 决定任务什么时候触发。Quartz 提供了两种最常用的触发器:SimpleTrigger 和 CronTrigger。
4.4.1 SimpleTrigger:简单粗暴,一次或多次
适合固定间隔、固定次数的场景。比如:每 5 分钟执行一次,总共执行 10 次。
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("simpleTrigger", "group1")
.startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND))
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10)
.withRepeatCount(5))
.build();
参数说明:
startAt:首次触发时间withIntervalInSeconds:执行间隔withRepeatCount:重复次数(注意:总执行次数 = repeatCount + 1)
4.4.2 CronTrigger:Cron 表达式,无所不能
这才是 Quartz 的杀手锏。一个 Cron 表达式能表达几乎任何时间规则。我在主数据治理项目中,90% 的调度需求都用 CronTrigger 搞定。
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("cronTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
.build();
上面这个表达式意思是:每天凌晨 2 点执行。Cron 表达式有 7 个字段:
| 位置 | 字段 | 取值范围 | 示例 |
|---|---|---|---|
| 1 | 秒 | 0-59 | 0 |
| 2 | 分 | 0-59 | 0 |
| 3 | 时 | 0-23 | 2 |
| 4 | 日 | 1-31 | * |
| 5 | 月 | 1-12 或 JAN-DEC | * |
| 6 | 周 | 1-7 或 SUN-SAT | ? |
| 7 | 年(可选) | 1970-2099 | 留空 |
0 0/5 * * * ?—— 每 5 分钟执行0 0 2 * * ?—— 每天凌晨 2 点0 0 2 ? * MON-FRI—— 工作日凌晨 2 点0 0 2 L * ?—— 每月最后一天凌晨 2 点0 0 2 ? * 2#1—— 每月第一个周一凌晨 2 点
4.5 Scheduler:总指挥,把 Job 和 Trigger 绑在一起
Scheduler 是 Quartz 的心脏。它负责注册 JobDetail、绑定 Trigger,然后启动整个调度生命周期。
// 1. 创建 Scheduler 工厂
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
// 2. 注册 JobDetail
JobDetail job = JobBuilder.newJob(DataSyncJob.class)
.withIdentity("syncCustomer", "mdmGroup")
.build();
// 3. 创建 Trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("cronTrigger", "mdmGroup")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
.build();
// 4. 绑定 Job 和 Trigger
scheduler.scheduleJob(job, trigger);
// 5. 启动调度器
scheduler.start();
这里有个细节:scheduleJob 方法会把 JobDetail 和 Trigger 关联起来。一个 JobDetail 可以绑定多个 Trigger,但一个 Trigger 只能绑定一个 JobDetail。我在项目中经常用这个特性:同一个数据同步任务,配一个 CronTrigger 做定时调度,再配一个 SimpleTrigger 做手动重跑。
@PostConstruct 和 @PreDestroy,或者实现 InitializingBean 和 DisposableBean。别问我为什么知道——有一次线上重启,Scheduler 没正常关闭,导致任务重复执行,主数据乱了整整一天。
4.6 三者协作:一次完整的调度流程
咱们捋一遍 Quartz 内部是怎么工作的:
- 注册阶段:你把 JobDetail 和 Trigger 交给 Scheduler。Scheduler 把它们存到 JobStore 里(内存或数据库)。
- 等待阶段:Scheduler 内部有个线程池,不停地检查所有 Trigger 的下一次触发时间。
- 触发阶段:时间到了,Scheduler 从线程池里拿一个工作线程,根据 JobDetail 创建 Job 实例(或复用已有实例),调用
execute方法。 - 执行阶段:你的业务逻辑跑完,线程归还到线程池,Scheduler 继续等待下一次触发。