Quartz Job Scheduling Framework[翻译]第五章. Cron 触发器及相关内容 (第一部分)

译者:Unmi(隔叶黄莺)
http://www.blogjava.net/Unmi/archive/2008/02/21/181178.html

第五章. Cron 触发器及相关内容

我们在上章中有承诺过会花更多时间来讲 Quartz 的 CronTrigger,所以不会让你失望的。SimpleTrigger 对于需要在指定的毫秒处及时执行的作业还是不错的,但是假如你的作业需要更复杂的执行计划时,你也就要 CronTrigger 给你提供更强更灵活的功能。

一. Cron 的快速一课

cron 这一观念是来自于 UNIX 世界。在 UNIX 中,cron 是一个运行于后台的守护程序,它负责所有基于时间的事件。尽管 Quartz 除相同的名字和相似的表达式语法外,并未分享到 UNIX cron 别的东西,我们还是值得花几个段落去理解 cron 背后的历史。我们这里的目标不是搞混 UNIX cron 表达式和 Quartz 的 cron 表达式,但是你应该了解 Quartz 表达式的历史,并探索为什么他们运作起来很像。这儿明显有许多有意图的相似性。

有许多不同版本的 UNIX Cron

你会发现不同版本的 cron,每一种都有些微地差异。我们仅着眼于与 Quartz CronTrigger 的比较,因此我们只讨论不同版本 UNIX cron 共性的东西。

UNIX cron 守护进程每隔一分钟被唤醒一次去检查叫做 crontabs 的配置文件。(Crontab 是 CRON 和 TABLE 的连写,其中配置有 cron 守护进程的作业列表和其他的指令。)守护进程检查存储在 crontabs 中的命令并决定是否有要执行的任务。

·UNIX Cron 的格式

你可以认为 UNIX crontab 是 trigger 和 job 的组合,因为它们同时列出来执行计划和要执行的命令(job)。

Cron Expression 的格式

crontab 格式包含六段,前五段为执行计划,第六段为要执行的命令。(Quartz cron 表达式有七段。) 下面这些是执行计划的五个字段:

·分 (00-59)

·时 (00-23)

·日 (1-31)

·月 (1-12)

·周 (0-6 或 sun-sat)

UNIX cron 格式表达式中允许出现一些特殊的字符,例如星号(*),它用来匹配所有值。这作有一个 UNIX crontab 的例子:

0 8 * * * echo "WAKE UP" 2>$1 /dev/console

这一 crontab 条目在每天早上8点打印字符串 "WAKE UP" 到 UNIX 的设置 /dev/console 上。图 5.1 显示了这个动作。


图 5.1. UNIX Cron 执行 0 8 * * * echo "WAKE UP" 2>$1 /dev/console 表达式




2. 使用 Quartz CronTrigger

在现实世界里,作业计划通常比 SimpleTrigger 能支持的复杂得多。CronTrigger 就可用于指定非常复杂的计划,这种计划不错,因为也是我们发现需要这么做的。在我们深入到 CronTrigger 之前,让我们先看一个例子。代码 5.1 所示的是一个使用 CronTrigger (连同一个 Quartz cron 表达式) 来部署前面例子中的 PrintInfoJob。代码中的大部门内容与前面章节的例子相同。唯一不同点是我们使用了 CronTrigger 替代了 SimpleTrigger。正因为如此,我们不得不为它提供了一个 cron 表达式。

代码 5.1. 简单使用 CronTrigger 来部署一个 Job

public class Listing_5_1 {
  static Log logger = LogFactory.getLog(Listing_5_1.class);

  public static void main(String[] args) {
    Listing_5_1 example = new Listing_5_1();
    example.runScheduler();
  }

  public void runScheduler() {
    Scheduler scheduler = null;
    try {
      // Create a default instance of the Scheduler
      scheduler = StdSchedulerFactory.getDefaultScheduler();
      scheduler.start();
      logger.info("Scheduler was started at " + new Date());
      // Create the JobDetail
      JobDetail jobDetail = new JobDetail("PrintInfoJob", Scheduler.DEFAULT_GROUP, PrintInfoJob.class);
      // Create a CronTrigger
      try {
        // CronTrigger that fires @7:30am Mon - Fri
        CronTrigger trigger = new CronTrigger("CronTrigger", null, "0 30 7 ? * MON-FRI");
        scheduler.scheduleJob(jobDetail, trigger);
      } catch (ParseException ex) {
        logger.error("Error parsing cron expr", ex);
      }
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
}

代码 5.1 中的例子使用了如下 cron 表达式:

0 30 7 ? * MON-FRI

当被调度器解释后,它会引起 trigger 在星期一至星期五的早上 7:30 被激发。让我们来看看 Quartz CronTrigger 的 cron 表达式的格式。


三. cron 表达式的格式

Quartz cron 表达式的格式十分类似于 UNIX cron 格式,但还是有少许明显的区别。区别之一就是 Quartz 的格式向下支持到秒级别的计划,而 UNIX cron 计划仅支持至分钟级。许多我们的触发计划要基于秒级递增的(例如,每45秒),因此这是一个非常好的差异。

在 UNIX cron 里,要执行的作业(或者说命令)是存放在 cron 表达式中的,在第六个域位置上。Quartz 用 cron 表达式存放执行计划。引用了 cron 表达式的 CronTrigger 在计划的时间里会与 job 关联上。

另一个与 UNIX cron 表达式的不同点是在表达式中支持域的数目。UNIX 给出五个域(分、时、日、月和周),Quartz 提供七个域。表 5.1 列出了 Quartz cron 表达式支持的七个域。

表 5.1. Quartz Cron 表达式支持到七个域

名称 是否必须 允许值 特殊字符
秒 是 0-59 , - * /
分 是 0-59 , - * /
时 是 0-23 , - * /
日 是 1-31 , - * ? / L W C
月 是 1-12 或 JAN-DEC , - * /
周 是 1-7 或 SUN-SAT , - * ? / L C #
年 否 空 或 1970-2099 , - * /

月份和星期的名称是不区分大小写的。FRI 和 fri 是一样的。

域之间有空格分隔,这和 UNIX cron 一样。无可争辩的,我们能写的最简单的表达式看起来就是这个了:

* * * ? * *

这个表达会每秒钟(每分种的、每小时的、每天的)激发一个部署的 job。

·理解特殊字符

同 UNIX cron 一样,Quartz cron 表达式支持用特殊字符来创建更为复杂的执行计划。然而,Quartz 在特殊字符的支持上比标准 UNIX cron 表达式更丰富了。

* 星号

使用星号(*) 指示着你想在这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发这个 trigger。

表达式样例:

0 * 17 * * ?

意义:每天从下午5点到下午5:59中的每分钟激发一次 trigger。它停在下午 5:59 是因为值 17 在小时域上,在下午 6 点时,小时变为 18 了,也就不再理会这个 trigger,直到下一天的下午5点。

在你希望 trigger 在该域的所有有效值上被激发时使用 * 字符。

? 问号

? 号只能用在日和周域上,但是不能在这两个域上同时使用。你可以认为 ? 字符是 "我并不关心在该域上是什么值。" 这不同于星号,星号是指示着该域上的每一个值。? 是说不为该域指定值。

不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上,假定同时指定值的话,意义就会变得含混不清了:考虑一下,如果一个表达式在日域上有值11,同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号,且正好又是星期三那天被激发?还是在每个星期三的11号被激发呢?要去除这种不明确性的办法就是不能同时在这两个域上指定值。

只要记住,假如你为这两域的其中一个指定了值,那就必须在另一个字值上放一个 ?。

表达式样例:

0 10,44 14 ? 3 WEB

意义:在三月中的每个星期三的下午 2:10 和 下午 2:44 被触发。

, 逗号

逗号 (,) 是用来在给某个域上指定一个值列表的。例如,使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。

表达式样例:

0 0,15,30,45 * * * ?

意义:每刻钟触发一次 trigger。

/ 斜杠

斜杠 (/) 是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增,但是我们也能写成这样 0/15。

表达式样例:

0/15 0/30 * * * ?

意义:在整点和半点时每15秒触发 trigger。

- 中划线

中划线 (-) 用于指定一个范围。例如,在小时域上的 3-8 意味着 "3,4,5,6,7 和 8 点。" 域的值不允许回卷,所以像 50-10 这样的值是不允许的。

表达式样例:

0 45 3-8 ? * *

意义:在上午的3点至上午的8点的45分时触发 trigger。

L 字母

L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上,表示的是在月域上指定的月份的最后一天。例如,当月域上指定了 JAN 时,在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP,那么 L 会预示着在9月30号触发。换句话说,就是不管指定了哪个月,都是在相应月份的时最后一天触发 trigger。

表达式 0 0 8 L * ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 * 说明是 "每个月"。

当 L 字母用于周域上,指示着周的最后一天,就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger,你可以用这样的表达式 0 59 23 ? * L。

当使用于周域上,你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。

不要让范围和列表值与 L 连用

虽然你能用星期数(1-7)与 L 连用,但是不允许你用一个范围值和列表值与 L 连用。这会产生不可预知的结果。

W 字母

W 字符代表着平日 (Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最近的一个平日。大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。例如,日域中的 15W 意味着 "离该月15号的最近一个平日。" 假如15号是星期六,那么 trigger 会在14号(星期四)触发,因为距15号最近的是星期一,这个例子中也会是17号(译者Unmi注:不会在17号触发的,如果是15W,可能会是在14号(15号是星期六)或者15号(15号是星期天)触发,也就是只能出现在邻近的一天,如果15号当天为平日直接就会当日执行)。W 只能用在指定的日域为单天,不能是范围或列表值。

# 井号

# 字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。另一个例子 2#1 意思是某月的第一个星期一 (2=星期一,#1意味着月份中的第一周)。注意,假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。


四. 为 CronTrigger 使用起迄日期

Cron 表达式是用来决定一个 Trigger 被触发执行一个 Job 的日期和次数。当你创建一个 CronTrigger 实例,假如没为它指定一个开始时间,这个 Trigger 当然就会假定是在依赖于 Cron 表达式尽早的被触发。例如,如果你用这个表达式

0 * 14-20 * * ?

这个 Trigger 会在每天的从下午 2 点到下午的 7:59 间的每分钟触发一次。一旦你运行了这个表达式的 CronTrigger,假如当前是下午 2 点后(不能超过 7:59 PM--译者注),它将会立即触发。它会在每天无限期的被触发。

另一方面,倘若你希望这个计划直到下一天才开始,并且只执行两天,你就可以用 CronTrigger 的 setStartTime() 和 setEndTime() 方法来形成一个 "定时箱" 来触发。代码 5.2 描述了限定 CronTrigger 仅触发两天的例子。

代码 5.2. 你可以对 CronTrigger 用 startTime 和 endTime

 
public class Listing_5_2 {
  static Log logger = LogFactory.getLog(Listing_5_2.class);
  public static void main(String[] args) {
    Listing_5_2 example = new Listing_5_2();
    example.runScheduler();
  }
  public void runScheduler() {
    Scheduler scheduler = null;
    try {
      // Create a default instance of the Scheduler
      scheduler = StdSchedulerFactory.getDefaultScheduler();
      scheduler.start();
      logger.info("Scheduler was started at " + new Date());
      // Create the JobDetail
      JobDetail jobDetail = new JobDetail("PrintInfoJob", Scheduler.DEFAULT_GROUP, PrintInfoJob.class);
      // Create a CronTrigger
      try {
        // cron that fires every min from 2 8pm
        CronTrigger trigger = new CronTrigger("MyTrigger", null, "0 * 14-20 * * ?");
        Calendar cal = Calendar.getInstance();
        // Set the date to 1 day from now
        cal.add(Calendar.DATE, 1);
        trigger.setStartTime(cal.getTime());
        // Move ahead 2 days to set the end time
        cal.add(Calendar.DATE, 2);
        trigger.setEndTime(cal.getTime());
        scheduler.scheduleJob(jobDetail, trigger);
      } catch (ParseException ex) {
        logger.error("Couldn't parse cron expr", ex);
      }
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
}

代码 5.2 中的例子使用了 java.util.Calendar 来为 Trigger 选择一个开始和结束时间周期。在上面例子中,Trigger 将会在 Scheduler 启动后的下一天开始触发,并只在开始触发后的两天内有效。

使用 CronTrigger 的 startTime 和 endTime 属性的效果有点像 SimpleTrigger。

五. 为 CronTrigger 使用 TriggerUtils

在第四章,"安排 Job" 中介绍了 org.quartz 包中的 TriggerUtils 类,它简化了两种类型的 Trigger 的创建。只要可能的话,你应该尝试用 TriggerUtils 类的方法来创建你的 Trigger。

例如,假如你需要在每天的下午 5:30 执行一个 Job,你可以用下面的代码:

try {   
  
  // A CronTrigger that fires @ 5:30PM   
  CronTrigger trigger = new CronTrigger("CronTrigger", null, "0 30 17 ? * *");   
} catch (ParseException ex) {   
  logger.error("Couldn't parse cron expression", ex);   
}  

或者你能用上 TriggerUtils,如下:

  // A CronTrigger that fires @ 5:30PM   
Trigger trigger = TriggerUtils.makeDailyTrigger(17, 30);   
trigger.setName("CronTrigger");  

TriggerUtils 使得我们更简单方便的使用 Trigger,而又未放弃太多的灵活性。

六. 在 JobInitializationPlugin 中使用 CronTrigger

尽管我们要到第八章,"使用 Quartz 插件" 才会讲到插件,但还是值得提前展现一下 CronTrigger 如何应用于 quartz_jobs.xml 文件中来指定 Job 信息的。JobInitialzationPlugin 可用来从 XML 文件中加载 Job 的信息。

正如 SimpleTrigger 一样,你可在 XML 文件中指定 CronTrigger 的表达式,并且 Quartz 的 Scheduler 将会利用这一信息来安排你的 Job。这对于你想在你的程序代码之外声明你的 Job 信息时特别方便。代码 5.3 显示了 quartz_jobs.xml 文件内容,它被 JobInitializationPlugin 用来加作 Job 信息。

代码 5.3. CronTrigger 可在 XML 文件中指定,并由 JobInitializationPlugin 加载

<?xml version='1.0' encoding='utf-8'?>  
  
<quartz>  
  <job>  
    <job-detail>  
      <name>PrintInfoJob</name>  
    <group>DEFAULT</group>  
    <description>  
      A job that prints out some basic information.   
    </description>  
    <job-class>  
      org.cavaness.quartzbook.common.PrintInfoJob   
    </job-class>  
   </job-detail>  
  
   <trigger>  
    <cron>  
     <name>printJobInfoTrigger</name>  
     <group>DEFAULT</group>  
     <job-name>PrintInfoJob</job-name>  
     <job-group>DEFAULT</job-group>  
  
      <!-- Fire 7:30am Monday through Friday -->  
     <cron-expression>0 30 7 ? * MON-FRI</cron-expression>  
    </cron>  
   </trigger>  
  </job>  
</quartz>  

代码 5.3 中的 Cron 表达式与 代码 5.1 中。当 Quartz 加载这个 XML 后,就会安排 PrintInfoJob (也已在 XML 中列出) 在从星期一到星期五的早上 7:30 执行。关于 JobInitializationPlugin 更多的说明见第八章。


七. Cron 表达式 Cookbook

此处的 Cron 表达式 cookbook 旨在为常用的执行需求提供方案。尽管不可能列举出所有的表达式,但下面的应该为满足你的业务需求提供了足够的例子。

·分钟的 Cron 表达式

表 5.1. 包括了分钟频度的任务计划 Cron 表达式

用法 表达式
每天的从 5:00 PM 至 5:59 PM 中的每分钟触发 0 * 17 * * ?

每天的从 11:00 PM 至 11:55 PM 中的每五分钟触发 0 0/5 23 * * ?

每天的从 3:00 至 3:55 PM 和 6:00 PM 至 6:55 PM 之中的每五分钟触发 0 0/5 15,18 * * ?

每天的从 5:00 AM 至 5:05 AM 中的每分钟触发 0 0-5 5 * * ?


·日上的 Cron 表达式

表 5.2. 基于日的频度上任务计划的 Cron 表达式

用法 表达式
每天的 3:00 AM 0 0 3 * * ?
每天的 3:00 AM (另一种写法) 0 0 3 ? * *
每天的 12:00 PM (中午) 0 0 12 * * ?
在 2005 中每天的 10:15 AM 0 15 10 * * ? 2005

·周和月的 Cron 表达式

表 5.3. 基于周和/或月的频度上任务计划的 Cron 表达式

用法 表达式
在每个周一,二, 三和周四的 10:15 AM 0 15 10 ? * MON-FRI
每月15号的 10:15 AM 0 15 10 15 * ?
每月最后一天的 10:15 AM 0 15 10 L * ?
每天最后一个周五的 10:15 AM 0 15 10 ? * 6L
在 2002, 2003, 2004, 和 2005 年中的每月最后一个周五的 10:15 AM 0 15 10 ? * 6L 2002-2005
每月第三个周五的 10:15 AM 0 15 10 ? * 6#3
每月从第一天算起每五天的 12:00 PM (中午) 0 0 12 1/5 * ?
每一个 11 月 11 号的 11:11 AM 0 11 11 11 11 ?
三月份每个周三的 2:10 PM 和 2:44 PM 0 10,44 14 ? 3 WED

八. 创建一个即刻触发的 Trigger

有时候,你需要立即执行一个 job。例如,想像一下,你正在构建一个 GUI 程序并允许用户能立刻执行。另一个例子,你或许已经检测到了某个 Job 未执行成功,因此你想要即刻重跑一次。在 Quartz 1.5,有几个方法被加入到了 TriggerUtils 类中,使得实现那些事很容易了。代码 5.4 展示了如何部署一个 job,只让它立即执行一次。

代码 5.4. 你可以用 TriggerUtils 来立即执行一个 Job

 
public class Listing_5_4 {
  static Log logger = LogFactory.getLog(Listing_5_4.class);
  public static void main(String[] args) {
    Listing_5_4 example = new Listing_5_4();
    example.runScheduler();
  }
  public void runScheduler() {
    Scheduler scheduler = null;
    try {
      // Create a default instance of the Scheduler
      scheduler = StdSchedulerFactory.getDefaultScheduler();
      scheduler.start();
      logger.info("Scheduler was started at " + new Date());
      // Create the JobDetail
      JobDetail jobDetail = new JobDetail("PrintInfoJob", Scheduler.DEFAULT_GROUP, PrintInfoJob.class);
      // Create a trigger that fires once right away
      Trigger trigger = TriggerUtils.makeImmediateTrigger(0, 0);
      trigger.setName("FireOnceNowTrigger");
      scheduler.scheduleJob(jobDetail, trigger);
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
}

在代码 5.4 中,TriggerUtils 的 makeImmediateTrigger() 方法被用来立即执行一个 Job。第一个参数是将要触发的次数。第二个参数是执行的间隔时间。为方便起见,这个方法的签名显示如下:

public static Trigger  makeImmediateTrigger(int repeatCount, long repeatInterval);


TriggerUtils 类提供了许多便利的方法简化了 Trigger 的使用。确切地检查一下这个工具类中看看是否有你想要的东西。你还将在本书上看到更多的使用 TriggerUtils 的例子。

QuartzFigure5_1_M.jpg
快乐渡过每一天,减肥坚持每一天