1. 概述

Quartz 是一个完全用Java编写的开源任务调度框架,专为 J2SEJ2EE 应用设计。它在保持简单性的同时提供了极大的灵活性

你可以创建复杂的调度计划来执行任何任务,例如:

  • 每天运行的任务
  • 每隔一周周五晚上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 接口可用于添加、删除、列出 JobsTriggers,以及执行其他调度相关操作(如暂停触发器)。

但注意:调度器在调用 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提供了多种触发器类型,最常用的是 SimpleTriggerCronTrigger

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,并探讨了最常用的触发器类型:SimpleTriggerCronTrigger

Quartz可用于创建从简单到复杂的调度计划,执行数十、数百甚至更多任务。更多框架信息可访问官方网站

文章示例代码可在GitHub上找到。


原始标题:Introduction to Quartz | Baeldung