4. Quartz核心原理:Job、Trigger、Scheduler三大核心接口

Quartz这套东西,说白了就是三个角色在跳舞:Job(干什么活)、Trigger(什么时候干)、Scheduler(谁来安排)。我最早接触Quartz是在一个主数据同步项目里,当时被这哥仨的关系绕了好几天。今天咱们就把它们拆开揉碎了讲清楚。

4.1 三大核心接口:各司其职

先看一张整体关系图,心里有个谱:

Quartz 三大核心接口关系图 Scheduler 调度器(总指挥) Job 任务(干什么活) Trigger 触发器(何时干) JobDetail 任务元数据 JobDataMap 任务参数容器 Scheduler 管理 JobDetail 和 Trigger 的注册、绑定与执行生命周期

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");
        // 执行同步...
    }
}
我的习惯:Job 类里只做编排,不做具体实现。把真正的业务逻辑抽到 Service 层,Job 只负责调用。这样单元测试好写,也方便切换调度框架。

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 实例 干活的人 每次执行可能新建实例(取决于配置)
我曾经踩过的坑:JobDataMap 里的数据如果太大,会影响序列化性能。有一次我把一个 10MB 的 JSON 字符串塞进去,结果调度器启动慢了 3 秒。记住,它只适合放配置参数,不适合放大对象。

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 留空
常用 Cron 表达式速查:
  • 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 做手动重跑。

我的建议:生产环境中,Scheduler 的启动和关闭一定要放在应用的生命周期回调里。Spring 环境下用 @PostConstruct 和 @PreDestroy,或者实现 InitializingBean 和 DisposableBean。别问我为什么知道——有一次线上重启,Scheduler 没正常关闭,导致任务重复执行,主数据乱了整整一天。

4.6 三者协作:一次完整的调度流程

咱们捋一遍 Quartz 内部是怎么工作的:

  1. 注册阶段:你把 JobDetail 和 Trigger 交给 Scheduler。Scheduler 把它们存到 JobStore 里(内存或数据库)。
  2. 等待阶段:Scheduler 内部有个线程池,不停地检查所有 Trigger 的下一次触发时间。
  3. 触发阶段:时间到了,Scheduler 从线程池里拿一个工作线程,根据 JobDetail 创建 Job 实例(或复用已有实例),调用 execute 方法。
  4. 执行阶段:你的业务逻辑跑完,线程归还到线程池,Scheduler 继续等待下一次触发。
注意:默认情况下,Quartz 每次执行 Job 都会创建新的实例。这意味着 Job 类必须有无参构造器,而且不能依赖实例变量保存状态。如果你需要跨执行共享数据,用 JobDataMap 或者外部存储(数据库、Redis)。