4. Job 与 JobDetail 的创建与注册

在 Quartz 调度体系中,Job 是实际执行任务的单元,而 JobDetail 则是描述 Job 实例的元数据容器。理解两者的关系、构建方式以及持久化机制,是搭建可靠任务调度平台的基石。本章将深入剖析 Job 接口定义、JobDataMap 的数据传递、JobDetail 的构建器模式,以及任务在集群环境下的持久化与恢复策略。

4.1 Job 接口定义与执行契约

所有可被 Quartz 调度的任务都必须实现 org.quartz.Job 接口。该接口仅包含一个方法:

package org.quartz;

public interface Job {
    void execute(JobExecutionContext context) throws JobExecutionException;
}

关键设计要点:

  • 无状态推荐:Job 实现类中不应持有可变成员变量。Quartz 每次执行时可能复用 Job 实例(取决于 @DisallowConcurrentExecution 注解),依赖成员变量会导致数据污染。
  • 上下文驱动:所有运行时数据均通过 JobExecutionContext 获取,包括调度器引用、触发器信息以及 JobDataMap。
  • 异常处理:抛出 JobExecutionException 可控制调度器的后续行为(如立即重试、停止触发器)。
最佳实践: 将 Job 类设计为纯执行逻辑,所有可变参数通过 JobDataMap 传入,确保线程安全与可测试性。

4.2 JobDataMap:任务参数的数据通道

JobDataMap 是 java.util.Map 的实现,用于在创建 JobDetail 或 Trigger 时向 Job 传递配置参数。它支持基本类型、字符串以及可序列化对象。

4.2.1 数据注入方式

注入层级 设置位置 获取方式 合并规则
JobDetail 级别 JobDetail.getJobDataMap() context.getMergedJobDataMap() Trigger 级别键值会覆盖 JobDetail 级别的同名键
Trigger 级别 Trigger.getJobDataMap() context.getMergedJobDataMap()
// 创建 JobDetail 时设置参数
JobDetail jobDetail = JobBuilder.newJob(EmailJob.class)
    .withIdentity("emailJob", "group1")
    .usingJobData("recipient", "user@example.com")
    .usingJobData("retryCount", 3)
    .build();

// 在 Job 执行中获取参数
public class EmailJob implements Job {
    public void execute(JobExecutionContext context) {
        JobDataMap dataMap = context.getMergedJobDataMap();
        String recipient = dataMap.getString("recipient");
        int retryCount = dataMap.getInt("retryCount");
        // 执行逻辑...
    }
}
注意: 如果 Job 类中定义了与 JobDataMap 键同名的 setter 方法,Quartz 会自动调用 setter 注入属性值,无需手动从 Map 中提取。此特性称为 “属性注入”。

4.3 JobDetail 构建器模式

Quartz 采用 Builder 模式 创建 JobDetail 实例,避免构造器参数过多导致的混乱。核心构建器为 JobBuilder。

4.3.1 构建流程与核心方法

JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
    .withIdentity("myJob", "defaultGroup")   // 必填:任务名称 + 组名
    .withDescription("发送通知邮件")           // 可选:描述信息
    .usingJobData("key1", "value1")           // 可选:添加参数
    .usingJobData("key2", 100)
    .storeDurably(true)                       // 可选:即使没有触发器也保留在调度器中
    .requestRecovery(true)                    // 可选:调度器重启后自动恢复执行
    .build();
方法 参数说明 默认值 使用场景
newJob(Class) Job 实现类的 Class 对象 无 指定执行逻辑的类
withIdentity(String, String) 任务名称、组名 无 唯一标识任务
storeDurably(boolean) 是否持久化存储 false 无触发器关联的独立任务
requestRecovery(boolean) 是否支持故障恢复 false 集群模式或高可用场景

4.3.2 构建器模式的优势

  • 链式调用:代码可读性强,参数设置顺序灵活。
  • 不可变对象:构建完成后 JobDetail 不可修改,保证线程安全。
  • 参数校验:在 build() 时统一校验必填项,提前暴露配置错误。

4.4 Job 的持久化与恢复

当调度器使用 JDBC JobStore 时,JobDetail 和 JobDataMap 会被序列化后存入数据库。这一机制保证了任务在调度器重启或故障后仍能恢复执行。

4.4.1 持久化存储流程

  1. 序列化:JobDetail 对象(包括 JobDataMap)被序列化为 BLOB 或二进制字段。
  2. 写入表:数据存入 QRTZ_JOB_DETAILS 表,JobDataMap 同时写入 QRTZ_JOB_DETAILS 的 JOB_DATA 列。
  3. 触发器关联:在 QRTZ_TRIGGERS 表中记录 JobDetail 的引用键。

4.4.2 恢复机制与 requestRecovery

当调度器集群中的某个节点宕机后,其他节点会检测到“滞留”的任务。此时 requestRecovery 标志决定行为:

requestRecovery 值 调度器重启/故障转移行为 适用场景
false(默认) 不重新执行,丢失已触发但未完成的任务 允许偶尔丢失的非关键任务
true 调度器重新执行所有在故障时正在执行的任务 财务结算、订单处理等必须执行的任务
重要约束: 启用 requestRecovery 的 Job 必须保证 幂等性,因为恢复可能导致同一任务被执行多次。建议在业务逻辑中加入去重或状态检查机制。

4.4.3 持久化配置示例

// quartz.properties 关键配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000

4.5 核心关系图:Job ↔ JobDetail ↔ JobDataMap

下图展示了 Job、JobDetail、JobDataMap 三者之间的静态结构关系,以及持久化存储的映射路径。

Job ↔ JobDetail ↔ JobDataMap 核心关系与持久化 Job 接口 execute(JobExecutionContext) JobDetail name, group, jobClass JobDataMap Map<String, Object> 引用 包含 JDBC JobStore 持久化层 序列化存储 QRTZ_JOB_DETAILS JOB_DATA (BLOB) QRTZ_TRIGGERS JOB_NAME + JOB_GROUP 恢复流程:调度器重启 → 扫描 QRTZ_JOB_DETAILS → 反序列化 JobDetail → 检查 requestRecovery → 重新执行

4.6 完整注册示例:从构建到调度

以下代码整合了 JobDetail 构建、JobDataMap 设置以及向调度器注册的完整流程:

// 1. 创建调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();

// 2. 构建 JobDetail(启用持久化与恢复)
JobDetail jobDetail = JobBuilder.newJob(DataSyncJob.class)
    .withIdentity("dataSync", "production")
    .withDescription("每日数据同步任务")
    .usingJobData("sourceDb", "jdbc:mysql://prod:3306/source")
    .usingJobData("targetDb", "jdbc:mysql://prod:3306/target")
    .usingJobData("batchSize", 5000)
    .storeDurably(true)
    .requestRecovery(true)
    .build();

// 3. 构建触发器
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("dataSyncTrigger", "production")
    .startNow()
    .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(2, 0))
    .build();

// 4. 注册到调度器
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
生产建议: 在 OpenAPI 接口中,建议将 JobDetail 的构建参数(如 jobClass、jobDataMap、requestRecovery)作为接口请求体字段暴露给调用方,实现动态任务注册。同时,在接口层校验 jobClass 是否存在于白名单中,防止任意类加载漏洞。