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 持久化存储流程
- 序列化:JobDetail 对象(包括 JobDataMap)被序列化为 BLOB 或二进制字段。
- 写入表:数据存入
QRTZ_JOB_DETAILS表,JobDataMap 同时写入QRTZ_JOB_DETAILS的JOB_DATA列。 - 触发器关联:在
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 三者之间的静态结构关系,以及持久化存储的映射路径。
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 是否存在于白名单中,防止任意类加载漏洞。