目录

Quartz Scheduler

目录

Quartz 是一个有丰富特性、开源的任务计划库,可以跟任何 Java 应用集成。

如果你的应用有一些需要在指定时间执行的任务,或者有定期维护工作,那么 Quartz 将是你的理想解决方案。

  • 在应用程序中提供 reminder 提醒服务
  • 系统维护
  • 驱动工作流

本教程是阅读官网 Tutorial 的相关笔记。

  • Scheduler - the main API for interacting with the scheduler.
  • Job - an interface to be implemented by components that you wish to have executed by the scheduler.
  • JobDetail - used to define instances of Jobs.
  • Trigger - a component that defines the schedule upon which a given Job will be executed.
  • JobBuilder - used to define/build JobDetail instances, which define instances of Jobs.
  • TriggerBuilder - used to define/build Trigger instances.

一个 Scheduler 的生命周期从它通过 SchedulerFactory创建开始,直到调用它的 shutdown()方法而结束。

一旦创建了 Scheduler ,就可以通过 Scheduler 接口来 add, remove, list Jobs and Triggers,以及执行一些其他操作。

然而,Scheduler 不会运行任何 Job ,除非调用其 start() 方法。

简单示例:

import static org.quartz.JobBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.CalendarIntervalScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.DateBuilder.*;  // 帮助方便地创建 Date 实例

SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();

Scheduler sched = schedFact.getScheduler();

sched.start();

// define the job and tie it to our HelloJob class
JobDetail job = newJob(HelloJob.class)  // from JobBuilder [import static], HelloJob.class 是一个 Job 实例
  .withIdentity("myJob", "group1")  // name "myJob", group "group1"
  .build();

// Trigger the job to run now, and then every 40 seconds
Trigger trigger = newTrigger()  // from TriggerBuilder
  .withIdentity("myTrigger", "group1")
  .startNow()
  .withSchedule(simpleSchedule()  // from  SimpleScheduleBulder
                .withIntervalInSeconds(40)
                .repeatForever())
  .build();

// Tell quartz to schedule the job using our trigger
sched.scheduleJob(job, trigger);

Job 是一个实现了 Job 接口的类,该接口仅有一个简单的 execute 方法

当 Job 的 Trigger fire 之后,execute() 将被调用。

当 Job 被加入到 Scheduler 时,JobDetail 将自动被你的程序创建。

Trigger 是用来触发任务执行的对象。Quartz 有几种不同的 Trigger 类型,最常用的是 SimpleTriggerCronTrigger

SimpleTrigger 常用于:“one-shot”、at given time、repeat N times、delay of T between executations

CronTrigger 常用于:calendar-like schedules, such as “at 10:15 on the 10th day of every month”

为什么要使用 Jobs AND Triggers?分开计划与要执行的任务,这种松耦合有很多好处。例如,正因它们各自独立,所以可以有各种对应关系,一对多、一对一等。另一个好处是,可以在 Trigger 过期之后,配置留在 Scheduler 中的 Job,因此就可以重复利用而不用再定义。也允许你修改或替换一个 Trigger ,而不用重新定义与之相关的 Job。

当 Jobs 和 Triggers 在 Quartz Scheduler 中注册时,会被给予一些 identifying keys 标识码。

JobKey 和 TriggerKey 允许加入 groups,以便于组织和分类,在组内 key 值必须是唯一的。

实现一个 Job 非常简单,因为它只有一个 execute 方法。Quartz 需要知道更多属性,这通过 JobDetail 实现。

public class HelloJob implements Job {

  public HelloJob() {
  }

  public void execute(JobExecutionContext context)
    throws JobExecutionException
  {
    System.err.println("Hello!  HelloJob is executing.");
  }
}

JobDetail 实例通过 JobBuilder 构建。

回到之前的示例:

// define the job and tie it to our HelloJob class
JobDetail job = newJob(HelloJob.class)
  .withIdentity("myJob", "group1") // name "myJob", group "group1"
  .build();
// Trigger 省略
// Tell quartz to schedule the job using our trigger
sched.scheduleJob(job, trigger);

这里我们给 Scheduler 的是一个 JobDetail 实例,它通过传递 Job 类实例提供任务。每次 Scheduler 执行 Job 的时候,都将在调用 execute() 方法之前创建一个新的 Job 实例,并且在执行完之后被丢弃,等待垃圾回收。这样的行为产生的后果就是 Job 必须有一个无参构造器,并且在 Job 上无法定义状态数据字段。

那么如何配置一个 Job 实例?如何追踪 Job 在多次执行之间的状态?答案都是 JobDataMap,它是 JobDetail 的一部分。

JobDataMap 可以用于存储任意数量的、在你希望 Job 实例执行时可用的(serializable)数据对象。

JobDataMap是 Java Map 接口的实现,并且增加了一些便于存储和获取原始类型数据的方法。

可在定义 JobDetail 的同时存储数据进 JobDataMap,简单示例:

// define the job and tie it to our DumbJob class
JobDetail job = newJob(DumbJob.class)
  .withIdentity("myJob", "group1") // name "myJob", group "group1"
  .usingJobData("jobSays", "Hello World!")
  .usingJobData("myFloatValue", 3.141f)
  .build();

然后,可以在 Job 执行时,从 JobDataMap 获取数据:

public class DumbJob implements Job {

  public DumbJob() {
  }

  public void execute(JobExecutionContext context)
    throws JobExecutionException
  {
    JobKey key = context.getJobDetail().getKey();

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String jobSays = dataMap.getString("jobSays");
    float myFloatValue = dataMap.getFloat("myFloatValue");

    System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
  }
}

Trigger 也可以有关联的 JobDataMap ,当你的 Job 被多个 Trigger 使用时会很有用,每个 Trigger 都可以提供给 Job 不同的数据输入。此时需要使用 merge 方法获取,例如:

JobDataMap dataMap = context.getMergedJobDataMap();  // Note the difference from the previous example
ArrayList state = (ArrayList)dataMap.get("myStateData");
state.add(new Date());

或者通过 JobFactory 注入,会更清楚简洁:

public class DumbJob implements Job {

  String jobSays;
  float myFloatValue;
  ArrayList state;

  public DumbJob() {
  }

  public void execute(JobExecutionContext context)
    throws JobExecutionException
  {
    JobKey key = context.getJobDetail().getKey();

    JobDataMap dataMap = context.getMergedJobDataMap();  // Note the difference from the previous example

    state.add(new Date());

    System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
  }

  public void setJobSays(String jobSays) {
    this.jobSays = jobSays;
  }

  public void setMyFloatValue(float myFloatValue) {
    myFloatValue = myFloatValue;
  }

  public void setState(ArrayList state) {
    state = state;
  }
}

两个注解:

  • @DisallowConcurrentExecution:不许执行多个 Job 实例(JobDetail)

  • @PersistJobDataAfterExecution:执行之后更新存储的值,推荐与上一个注解同时使用以防混淆

  • Durability:如果一个 Job 是非持久的,则当没有其他活跃的 Trigger 关联时会被自动删除。即非持久性 Job 的生命周期受其触发器的存在限制。

  • RequestsRecovery:当执行过程中产生故障硬关机时,可以在重启 Scheduler 时再次执行,JobExecutionContext.isRecovering() 方法返回 true。

你被允许在 Job.execute(..) 中抛出的唯一一种异常类型(包括 RuntimeException)是 JobExecutionException。你应该阅读更多关于此异常的文档。

除了 TriggerKey 属性之外,还有很多的属性。这些通用的属性可以使用 TriggerBuilder 构建。

  • The “jobKey” property indicates the identity of the job that should be executed when the trigger fires.
  • The “startTime” property indicates when the trigger’s schedule first comes into affect. The value is a java.util.Date object that defines a moment in time on a given calendar date. For some trigger types, the trigger will actually fire at the start time, for others it simply marks the time that the schedule should start being followed. This means you can store a trigger with a schedule such as “every 5th day of the month” during January, and if the startTime property is set to April 1st, it will be a few months before the first firing.
  • The “endTime” property indicates when the trigger’s schedule should no longer be in effect. In other words, a trigger with a schedule of “every 5th day of the month” and with an end time of July 1st will fire for it’s last time on June 5th.

Quartz 可能没有足够的资源在同一时间立即执行所有任务,因此你可以添加 priority属性,默认为5,任何的整数都可以(包括正负)。

注意,优先级仅用在 Triggers 有同一 fire 时间时的对比。

Quartz Calendar objects 可与 Trigger 关联。日历用于从触发器的触发计划中排除时间块。

Calendar 可以是任何实现了 Calendar 接口的可序列化对象。

package org.quartz;

public interface Calendar {

  public boolean isTimeIncluded(long timeStamp);  // millisecond

  public long getNextIncludedTime(long timeStamp);

}

org.quartz.impl.HolidayCalendar 可以用于排除整天整天。

Calendar 必须被实例化,必须在 Scheduler 中通过 addCalendar(..) 注册。

HolidayCalendar cal = new HolidayCalendar();
cal.addExcludedDate( someDate );
cal.addExcludedDate( someOtherDate );

sched.addCalendar("myHolidays", cal, false);


Trigger t = newTrigger()
    .withIdentity("myTrigger")
    .forJob("myJob")
    .withSchedule(dailyAtHourAndMinute(9, 30)) // execute job daily at 9:30
    .modifiedByCalendar("myHolidays") // but not on holidays
    .build();

// .. schedule job with trigger

Trigger t2 = newTrigger()
    .withIdentity("myTrigger2")
    .forJob("myJob2")
    .withSchedule(dailyAtHourAndMinute(11, 30)) // execute job daily at 11:30
    .modifiedByCalendar("myHolidays") // but not on holidays
    .build();

// .. schedule job with trigger2

更多实现还需查看 org.quartz.impl.calendar 包。

包含的属性:a start-time, and end-time, a repeat count, and a repeat interval。

repeat count:0,正数

repeat interval:0,正 long (代表 milliseconds)

start time: 在你不熟悉 DateBuilder 的时候很有用

end time:将重写 repeat count

SimpleTrigger 使用 TriggerBuilder(以使用主要的属性) 和 SimpleScheduleBuilder (以使用 SimpleTrigger-specific 属性)构建。

例1:Build a trigger for a specific moment in time, with no repeats:

import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.DateBuilder.*:

SimpleTrigger trigger = (SimpleTrigger) newTrigger()
  .withIdentity("trigger1", "group1")
  .startAt(myStartTime) // some Date
  .forJob("job1", "group1") // identify job with name, group strings
  .build();

例2:Build a trigger for a specific moment in time, then repeating every ten seconds ten times:

trigger = newTrigger()
  .withIdentity("trigger3", "group1")
  .startAt(myTimeToStartFiring)  // if a start time is not given (if this line were omitted), "now" is implied
  .withSchedule(simpleSchedule()
                .withIntervalInSeconds(10)
                .withRepeatCount(10)) // note that 10 repeats will give a total of 11 firings
  .forJob(myJob) // identify job with handle to its JobDetail itself                   
  .build();

例3:Build a trigger that will fire once, five minutes in the future:

trigger = (SimpleTrigger) newTrigger()
  .withIdentity("trigger5", "group1")
  .startAt(futureDate(5, IntervalUnit.MINUTE)) // use DateBuilder to create a date in the future
  .forJob(myJobKey) // identify job with its JobKey
  .build();

例4:Build a trigger that will fire now, then repeat every five minutes, until the hour 22:00:

trigger = newTrigger()
  .withIdentity("trigger7", "group1")
  .withSchedule(simpleSchedule()
                .withIntervalInMinutes(5)
                .repeatForever())
  .endAt(dateOf(22, 0, 0))
  .build();

例5:Build a trigger that will fire at the top of the next hour, then repeat every 2 hours, forever:

trigger = newTrigger()
  .withIdentity("trigger8") // because group is not specified, "trigger8" will be in the default group
  .startAt(evenHourDate(null)) // get the next even-hour (minutes and seconds zero ("00:00"))
  .withSchedule(simpleSchedule()
                .withIntervalInHours(2)
                .repeatForever())
  // note that in this example, 'forJob(..)' is not called
  //  - which is valid if the trigger is passed to the scheduler along with the job  
  .build();

scheduler.scheduleJob(trigger, job);

花点时间看看 TriggerBuilder 和 SimpleScheduleBuilder 定义的所有可用的方法,这样您就可以熟悉可能在上面的示例中没有演示过的可用选项。

Misfire Instruction Constants of SimpleTrigger:

MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
MISFIRE_INSTRUCTION_FIRE_NOW
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
trigger = newTrigger()
  .withIdentity("trigger7", "group1")
  .withSchedule(simpleSchedule()
                .withIntervalInMinutes(5)
                .repeatForever()
                .withMisfireHandlingInstructionNextWithExistingCount())
  .build();

比 SimpleTrigger 更有用,当你需要使用基于 calendar-like 的标记的时候。

当然,它也有 start-time(何时开始)、 end-time 属性(何时停止)。

cron 表达式专门用来配置 CronTrigger 实例。

  1. Seconds:0-59
  2. Minutes:0-59
  3. Hours:0-23
  4. Day-of-Month:1-31,但要弄清楚这个月到底多少天
  5. Month:0-11,或者字符串 JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC
  6. Day-of-Week:1-7(1 = SUN),或者字符串 SUN, MON, TUE, WED, THU, FRI and SAT.
  7. Year (optional field)

/:举个例子:0/15 在分钟字段,表示每小时内的15分钟,在0分开始执行

/35” does not mean “every 35 minutes” - it mean “every 35th minute of the hour, starting at minute zero” - or in other words the same as specifying ‘0,35’.

?:day-of-month 和 day-of-week 字段可用,表示未指明的值。当您需要在这两个字段中的一个中指定某些内容,而不是在另一个字段中指定某些内容时,这是非常有用的。

L:day-of-month 和 day-of-week 字段可用,“last”缩写,在两个字段有各自的意义。

  • “L” in the day-of-month field means “the last day of the month”
  • day-of-week field by itself, it simply means “7” or “SAT”
  • day-of-week field after another value, it means “the last xxx day of the month” - for example “6L” or “FRIL” both mean “the last friday of the month”
  • 还可以使用偏移量,“L-3” which would mean the third-to-last(倒数第三) day of the calendar month.

W:指明离得最近的 weekday (Monday-Friday)。“15W” as the value for the day-of-month field, the meaning is: “the nearest weekday to the 15th of the month”.

在 JavaDoc 中查看更多:org.quartz.CronExpression。

“0 0/5 * * * ?”:(i.e. 10:00:00 am, 10:05:00 am, etc.).

“10 0/5 * * * ?”:(i.e. 10:00:10 am, 10:05:10 am, etc.).

“0 30 10-13 ? * WED,FRI”

“0 0/30 8-9 5,20 * ?”

TriggerBuilder (for the trigger’s main properties) and CronScheduleBuilder (for the CronTrigger-specific properties).

import static org.quartz.TriggerBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.DateBuilder.*:

trigger = newTrigger()
  .withIdentity("trigger3", "group1")
  .withSchedule(cronSchedule("0 0/2 8-17 * * ?"))
  .forJob("myJob", "group1")
  .build();

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(dailyAtHourAndMinute(10, 42))  // cronSchedule("0 42 10 * * ?")
    .forJob(myJobKey)
    .build();

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(weeklyOnDayAndHourAndMinute(DateBuilder.WEDNESDAY, 10, 42))  // cronSchedule("0 42 10 ? * WED")
    .forJob(myJobKey)
    .inTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))
    .build();

当发生故障时做什么?

MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
MISFIRE_INSTRUCTION_DO_NOTHING
MISFIRE_INSTRUCTION_FIRE_NOW
trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/2 8-17 * * ?")
        .withMisfireHandlingInstructionFireAndProceed())
    .forJob("myJob", "group1")
    .build();

Quartz Documentation Quartz JavaDoc 2.3.0