1. 概述
Quartz 是一个完全用Java编写的开源任务调度框架,专为 J2SE 和 J2EE 应用设计。它在保持简单性的同时提供了极大的灵活性。
你可以创建复杂的调度计划来执行任何任务,例如:
- 每天运行的任务
- 每隔一周周五晚上7:30运行的任务
- 仅在每月最后一天运行的任务
本文将深入探讨使用Quartz API构建任务的核心要素。关于与Spring集成的入门,推荐阅读Scheduling in Spring with Quartz。
2. Maven依赖
在 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
最新版本可在 Maven中央仓库 查询。
3. Quartz API核心组件
框架的核心是 Scheduler(调度器),负责管理应用的运行时环境。
为确保可扩展性,Quartz采用多线程架构。启动时,框架会初始化一组工作线程,供 Scheduler 执行 Jobs(任务)使用。
这种设计使框架能并发运行多个任务,同时依赖松耦合的 ThreadPool(线程池)管理组件来维护线程环境。
API的关键接口包括:
- Scheduler – 与框架调度器交互的主要API
- Job – 需要被执行的组件需实现的接口
- JobDetail – 用于定义 Job 实例
- Trigger – 决定给定 Job 执行时间计划的组件
- JobBuilder – 用于构建定义 Job 实例的 JobDetail
- TriggerBuilder – 用于构建 Trigger 实例
下面我们逐一解析这些组件。
4. 调度器(Scheduler)
使用 Scheduler 前需要实例化。通过工厂 SchedulerFactory 实现:
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
Scheduler 的生命周期从 SchedulerFactory 创建开始,到调用 shutdown() 方法结束。创建后,Scheduler 接口可用于添加、删除、列出 Jobs 和 Triggers,以及执行其他调度相关操作(如暂停触发器)。
但注意:调度器在调用 start() 方法前不会响应任何触发器:
scheduler.start();
5. 任务(Jobs)
Job 是实现 Job 接口的类,仅包含一个简单方法:
public class SimpleJob implements Job {
public void execute(JobExecutionContext arg0) throws JobExecutionException {
System.out.println("This is a quartz job!");
}
}
当任务的触发器激活时,调度器的工作线程会调用 execute() 方法。
传递给该方法的 JobExecutionContext 对象提供:
- 任务实例的运行环境信息
- 执行它的调度器引用
- 触发执行的触发器引用
- 任务的 JobDetail 对象
- 其他相关项
JobDetail 对象由Quartz客户端在将任务添加到调度器时创建,本质上是任务实例的定义:
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("myJob", "group1")
.build();
该对象还可包含任务的各种属性设置和 JobDataMap,用于存储任务实例的状态信息。
5.1. JobDataMap 使用指南
JobDataMap 用于保存任意数量的数据对象,在任务执行时提供给任务实例。它实现了Java的 Map 接口,并添加了用于存储和检索原始类型数据的便捷方法。
以下是在构建 JobDetail 时向 JobDataMap 放入数据的示例(在将任务添加到调度器之前):
JobDetail job = newJob(SimpleJob.class)
.withIdentity("myJob", "group1")
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.141f)
.build();
在任务执行期间访问这些数据的示例:
public class SimpleJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String jobSays = dataMap.getString("jobSays");
float myFloatValue = dataMap.getFloat("myFloatValue");
System.out.println("Job says: " + jobSays + ", and val is: " + myFloatValue);
}
}
上述示例将输出 "Job says Hello World!, and val is 3.141"。
✅ 技巧:我们可以在任务类中添加与 JobDataMap 键名对应的setter方法。Quartz默认的 JobFactory 会在任务实例化时自动调用这些setter,避免在execute方法中显式获取值。
6. 触发器(Triggers)
Trigger 对象用于触发 Jobs 的执行。
调度任务时,需要实例化触发器并配置其属性以满足调度需求:
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
Trigger 也可以关联 JobDataMap,用于传递特定于该触发器执行的任务参数。
不同调度需求对应不同类型的触发器,每种都有独特的 TriggerKey 属性标识身份。但所有触发器类型共享以下通用属性:
- jobKey:指定触发器激活时应执行的任务
- startTime:指定触发器调度首次生效的时间(java.util.Date对象)。某些触发器类型在给定时间点触发,其他类型则仅标记调度开始时间
- endTime:指定触发器调度应被取消的时间
Quartz提供了多种触发器类型,最常用的是 SimpleTrigger 和 CronTrigger。
6.1. 触发器优先级(Priority)
当多个触发器同时触发而Quartz资源不足时,可通过优先级控制执行顺序。触发器的priority属性就是为这种场景设计的。
⚠️ 例如:当10个触发器同时触发但仅有4个工作线程可用时,优先级最高的4个触发器将优先执行。未设置优先级的触发器默认为5。优先级可以是任意正负整数。
以下示例中,如果资源不足,triggerA 将优先执行:
Trigger triggerA = TriggerBuilder.newTrigger()
.withIdentity("triggerA", "group1")
.startNow()
.withPriority(15)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
Trigger triggerB = TriggerBuilder.newTrigger()
.withIdentity("triggerB", "group1")
.startNow()
.withPriority(10)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(20)
.repeatForever())
.build();
6.2. 错过触发处理(Misfire Instructions)
当持久化触发器因调度器关闭或线程池资源不足而错过触发时间时,就会发生misfire。
不同触发器类型提供不同的misfire处理指令,默认使用智能策略。调度器启动时会搜索所有misfired的持久化触发器,并根据各自配置的misfire指令更新它们。
以下示例展示了两种处理方式:
Trigger misFiredTriggerA = TriggerBuilder.newTrigger()
.startAt(DateUtils.addSeconds(new Date(), -10))
.build();
Trigger misFiredTriggerB = TriggerBuilder.newTrigger()
.startAt(DateUtils.addSeconds(new Date(), -10))
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withMisfireHandlingInstructionFireNow())
.build();
这里模拟了misfire场景(触发器被设置为10秒前执行)。在第一个触发器中未设置处理指令,因此使用默认的智能策略(withMisfireHandlingInstructionFireNow),即调度器发现misfire后立即执行任务。第二个触发器显式指定了相同策略。
6.3. SimpleTrigger 使用场景
SimpleTrigger 适用于需要在特定时间点执行任务的场景,可执行一次或按固定间隔重复执行。
典型场景包括:
- 在2018年1月13日12:20:00精确执行一次
- 从该时间点开始,每10秒重复执行5次
以下代码构建特定时间点的触发器(myStartTime 已预先定义):
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startAt(myStartTime)
.forJob("job1", "group1")
.build();
构建特定时间点开始、每10秒重复10次的触发器:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger2", "group1")
.startAt(myStartTime)
.withSchedule(simpleSchedule()
.withIntervalInSeconds(10)
.withRepeatCount(10))
.forJob("job1")
.build();
6.4. CronTrigger 日历调度
CronTrigger 适用于基于日历表达式的复杂调度,例如:
- 每周五中午执行
- 每个工作日上午9:30执行
Cron表达式用于配置 CronTrigger,由7个子表达式组成的字符串定义。详细语法可参考Oracle文档。
以下示例构建每天8am-5pm之间每2分钟执行一次的触发器:
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?"))
.forJob("myJob", "group1")
.build();
7. 获取Quartz任务的下一次执行时间
Quartz提供了多种方式获取任务的下次执行时间,最佳方法取决于任务是否已调度。
7.1. 获取已调度任务的下次执行时间
当任务已调度时,可通过 Trigger.getNextFireTime() 获取下次执行时间。该方法适用于所有Quartz触发器类型(SimpleTrigger, CronTrigger等)。在任务执行中访问 JobExecutionContext 即可动态获取下次执行时间。
示例代码展示如何在运行时记录下次执行时间:
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
Trigger trigger = context.getTrigger();
Date nextFireTime = trigger.getNextFireTime();
System.out.println("Next execution: " + nextFireTime);
}
}
✅ 关键点:通过 JobExecutionContext.getTrigger() 获取触发器,再调用 *trigger.getNextFireTime()*。此方法不依赖触发器类型,能在活跃调度器中动态跟踪任务计划。
7.2. 计算CronExpression的下次执行时间(无需调度)
对于SimpleTriggers,通过 Trigger.getNextFireTime() 即可轻松获取下次执行时间。但Cron表达式的调度规则更复杂(如"每周一上午9点"或"8am-5pm每2分钟"),需特殊处理。
Quartz提供 CronExpression.getNextValidTimeAfter() 方法,无需调度任务即可计算下次执行时间。适用于:
- 验证cron表达式
- 预测任务执行时间
- 动态确定调度计划
以下JUnit测试展示如何计算下次执行时间:
@Test
void givenCronExpressionAndTestTime_whenCalculatingNextFireTime_thenCorrectNextFireTimeIsReturned() throws ParseException {
String cronExpression = "0 0/2 8-17 * * ?"; // 8am-5pm每2分钟执行
CronExpression expression = new CronExpression(cronExpression);
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0); // 确保测试时间无毫秒
Date testTime = calendar.getTime(); // 固定时间:10:00 AM
Date nextFireTime = expression.getNextValidTimeAfter(testTime);
assertNotNull(nextFireTime);
// 下次执行时间应为10:02 AM
calendar.add(Calendar.MINUTE, 2);
Date expectedNextFireTime = calendar.getTime();
// 比较秒级时间(忽略毫秒)
long nextFireTimeInSeconds = nextFireTime.getTime() / 1000;
long expectedNextFireTimeInSeconds = expectedNextFireTime.getTime() / 1000;
assertEquals(expectedNextFireTimeInSeconds, nextFireTimeInSeconds);
}
❌ 注意:测试中通过秒级比较(忽略毫秒)确保时间准确性。给定10:00 AM的参考时间,下次执行时间应为10:02 AM。
8. 总结
本文展示了如何构建 Scheduler 来触发 Job,并探讨了最常用的触发器类型:SimpleTrigger 和 CronTrigger。
Quartz可用于创建从简单到复杂的调度计划,执行数十、数百甚至更多任务。更多框架信息可访问官方网站。
文章示例代码可在GitHub上找到。