特别教程-CronTrigger教程

介绍

cron表达式是一个已经存在了很长时间的UNIX工具,因此它的调度功能非常强大且已经经过验证。CronTrigger类的功能是基于cron的调度功能实现的。

CronTrigger使用"cron表达式",可以创建诸如“每周一至周五上午8:00”或“每月最后一个星期五上午1:30”的触发调度时间表(调度计划)。

cron表达式很强大,但是它使用起来也有可能相当混乱(这里大概的意思是说cron的用法相对复杂,容易出错)。本教程旨在解决创建cron表达式的一些谜题,为用户提供一个资源,让他们可以在论坛或邮件列表中提问之前访问这个教程(减少在论坛或者邮件中的提问)。

格式

cron表达式是由空格分隔的6或7个字段组成的字符串。字段可以包含任何允许的值,以及该字段允许的特殊字符的各种组合。这些字段如下所示:

字段名称 是否必须 允许的值 允许的特殊字符
秒种(Seconds) 0-59 , - * /
分钟(Minutes) 0-59 , - * /
小时(Hours) 0-23 , - * /
日(Day of month) 1-31 , - * ? / L W
月(Month) 1-12 or JAN-DEC , - * /
星期(Day of week) 1-7 or SUN-SAT , - * ? / L #
年(Year) empty(也就是不填任何值), 1970-2099 , - * /

译者注:这里可以看出一个cron表达式中年(Year)字段是非必须的,如果不填写此字段可以认为它其实就是*值。

所以cron表达式可以像这样简单:* * * * ? *,或更复杂,如这个例子所示:0/5 14,18,39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010

特殊字符

  • *:代表所有值 - 用于选择一个字段中的所有值。例如,分钟字段(Minutes)中的"*"表示 “每分钟”。
  • ?:代表没有具体的值 - 当你需要定义两个字段中的其中一个,另一个不需要定义id时候就十分有用(其实主要就是用在Day of month和Day of week的互斥关系中)。例如,例如我想我的触发器每月的某一日(例如第十日)触发,但是我不需要关注当天是星期几,因此我只需要把’10’设置在Day of month字段,把’?'设置在Day of week字段即可。可以参阅下面的例子来进一步了解。
  • -:用于指定范围值。例如Hours字段中的"10-12"表示"10,11和12"小时(就是一个范围值)。
  • ,:用于指定附加值。例如Day of week字段中的"MON,WED,FRI"表示 “星期一,星期三和星期五”。
  • /:用于指定增量(格式是:“初始值/增量”)。例如在Seconds字段中"0/15"表示秒数范围取值"0,15,30和45",Seconds字段中"5/15"表示秒数范围取值"5,20,35和50"。上一个例子说明了,你可以在"/"前取非零值(其实也就是初始值不为0),例如Day of month字段中"1/3"表示从月份的第一天起每三天(触发一次)。
  • L:英文单词"last"的缩写,含义和last一致。
  • W:英文单词"weekday"的缩写,即工作日(星期一到星期五)。

注意:

1
2
3
'L''W'字符可以Day of month字段合并使用,也就是在Day of month字段中使用'LW',转换为“月份的最后一个工作日”。

例如:"0 0 12 1LW * ?"表示每个月的最后一个工作日中午12点触发。
  • #:用于指定月份的"第n个"星期XXX*(格式:n#p,表示月份的第p个星期n,n由1开始,1表示星期日)。例如,Day of week字段的中"6#3"表示该月的第三个星期五(6表示星期五,#3表示第三个星期)。例如:“2#1"表示月份的第一个星期一,“4#5"表示月份的第五个星期三。注意最后这个例子,如果你指定了”#5”,并且月份的星期数不超过5个,那么该月份不会触发任何调度。

注意:

1
月份和星期缩写对应的合法字符不区分大小写。例如:MON与mon相同。

例子

这里有一些完整的例子:

表达式 含义
0 0 12 * * ? 每天中午12点(中午)触发
0 15 10 ? * * 每天上午10点15分触发
0 15 10 * * ? 每天上午10点15分触发
0 15 10 * * ? * 每天上午10点15分触发
0 15 10 * * ? 2005 2005年每天上午10点15分触发
0 * 14 * * ? 每天下午2点开始,每天下午2点59分结束
0 0/5 14 * * ? 每天下午2点开始,每天下午2点55分结束,每5分钟触发一次
0 0/5 14,18 * * ? 从下午2点开始,每天5分钟触发,结束于下午2点55分,每天5点钟触发,每天晚上6点开始,结束于下午6点55分
0 0-5 14 * * ? 每天下午2点开始,结束于下午2点05分,每分钟触发
0 10,44 14 ? 3 WED 在3月份的每个星期三下午2点10分和下午2点44分触发
0 15 10 ? * MON-FRI 每周一,周二,周三,周四和周五上午10点15分触发
0 15 10 15 * ? 在每个月的第15天上午10点15分触发
0 15 10 L * ? 在每个月的最后一天上午10点15分触发
0 15 10 L-2 * ? 在每个月的倒数第二天的上午10点15分触发
0 15 10 ? * 6L 每个月的最后一个星期五上午10点15分触发
0 15 10 ? * 6L 2002-2005 2002年,2003年,2004年和2005年每个月的最后一个星期五上午10点15分触发
0 15 10 ? * 6#3 每个月的第三个星期五上午10点15分触发
0 0 12 1/5 * ? 从每月的第一天开始,每个月每隔5天下午12点(中午)触发
0 11 11 11 11 ? 每11月11日上午11点11分触发

注意:

1
请注意'?''*'在日(Day of month)和星期(Day of week)中的作用。

注意事项

  • 目前不完全支持同时定义Day of week和Day of month两个字段(你必须在这两个字段其中之一使用’?’。其实这样做的目的是这两个字段是互斥的)。
  • 你需要注意如果触发时间设置在凌晨的几个小时,你的语言环境(locale)有可能会因为“夏令时”(对于美国地区,这通常是凌晨2点之前和之后的一个小时)而发生变化 - 此时,时间迁移有可能会发生跳跃或者重复(这里是指同一个时刻重复两次,因为时间发生了回拨),这取决于时间是向后移动还是向前移动。你可以从维基百科上查找资料确定你所在地区的具体情况,链接是:https://secure.wikimedia.org/wikipedia/en/wiki/Daylight_saving_time_around_the_world

原文链接:crontrigger

第十二章:其他特性

插件

Quartz提供了一个用于插入附加功能的接口org.quartz.spi.SchedulerPlugin

你可以从org.quartz.plugins包中找到提供各种实用功能的Quartz插件。它们提供诸如在调度器启动时自动调度Job的功能,记录Job和Trigger相关事件的历史,并确保当JVM退出时调度器能够彻底关闭。

Job工厂

当Trigger触发时,通过Scheduler上配置的JobFactory实例化与之关联的Job。默认的JobFactory只是在Job类上(反射)调用newInstance()。你可能需要创建自己的JobFactory实现,以完成诸如让应用程序的IoC或DI容器生成/初始化Job实例等等的操作。

请参阅org.quartz.spi.JobFactory接口以及Scheduler#setJobFactory(fact)等相关方法。

Factory-Shipped Jobs(这个不知道怎么翻译)

Quartz还提供了许多实用Job类型,你可以在应用程序中用于执行诸如发送电子邮件和调用EJB等Job实现。这些开箱即用的Job类型可以在org.quartz.jobs包中找到(要引入依赖quartz-jobs)。

原文链接:tutorial-lesson-12

第十一章:高级(企业级)特性

集群

Quartz集群目前与JDBC-Jobstore(JobStoreTXJobStoreCMT)和(<–译者注:其实我觉得这里应该是"或")TerracottaJobStore一起使用。功能包括负载均衡和Job故障转移(如果JobDetail的“请求恢复(request recovery)”标志设置为true)。

使用JobStoreTX或JobStoreCMT通过将org.quartz.jobStore.isClustered属性设置为true来启用集群模式。**集群中的每个实例都应该使用相同的quartz.properties文件。**这些相同的配置文件中,也允许下面几项属性配置是可以不相同的:不同的线程池大小,以及org.quartz.scheduler.instanceId设置不同的属性值。集群中的每个节点必须具有唯一的instanceId,通过将AUTO作为此属性的值,可以轻松完成此定义(这样就不需要使用不同的配置文件)。下面是注意事项:

1
不要在各自独立的机器上各自开启集群模式,除非它们的时钟使用某种形式的时间同步服务(守护进程)进行同步,而这些时间同步服务(守护进程)运行得非常有规律(各个机器的时钟差距必须在一秒内)。 如果你不熟悉如何执行此操作, 请参阅`http://www.boulder.nist.gov/timefreq/service/its.htm`。
1
非集群模式下,两个不同的实例千万不要使用同一套Quartz的表。否则,你一定会遇到不正常的(调度)行为,有可能会遭遇严重的数据损坏。

集群中触发机制是:Job每次只能被一个节点触发(也就是虽然集群中每台机器都跑着Quartz的调度器,但是Job需要被触发的时刻只有一台机器会进行触发)。我的意思是,如果Job有一个重复的Trigger,告诉它每10秒钟触发一次,那么在12:00:00,正好一个节点将执行这个Job,在12:00:10,也是由一个节点将执行此Job等。并不一定是每次由相同的节点执行Job - 由哪个节点执行它是随机的。负载均衡机制对于繁忙的调度程序(大量的Trigger)来说是近乎随机的。但是对于非繁忙(只有一两个Trigger)的调度器集群来说,有可能偏向于由同一个节点执行。(译者注:其实从源码上看,主要是看哪个节点先获取到独占锁。)

使用TerracottaJobStore建立Quartz集群只需要将把Scheduler的JobStore配置为TerracottaJobStore(第九章:JobStores中介绍过)即可,然后你的调度器就会被设置为集群模式。

您可能还需要考虑如何设置Terracotta服务器,特别是开启持久化特性等功能的配置选项,以及搭建一些列高可用的Terracotta服务器。

TerracottaJobStore的企业版提供了高级的"Quartz Where"功能,允许将作业的智能定位到适当的Quartz集群节点。

有关JobStore和Terracotta的更多信息,请访问http://www.terracotta.org/quartz

JTA事务

正如第九章:JobStores所述,JobStoreCMT允许在较大的JTA事务中执行Quartz调度操作。

通过将org.quartz.scheduler.wrapJobExecutionInUserTransaction属性设置为true,Job也可以在JTA事务(UserTransaction)内执行。使用此选项集,一个JTA事务将在Job的execute方法被调用之前调用其begin()方法,并且在Job执行完成调用之后调用其commit()。这适用于所有的Job。

如果你希望指定每个Jobs是否包裹在JTA事务内执行,那么你应该在Job类上使用@ExecuteInJTATransaction注解。

除了Quartz自动将Job执行包装到JTA事务中,使用JobStoreCMT之后在Scheduler接口方法也可以使用事务处理。你可以明确一点:在使用Scheduler接口方法之前已经开启了一个事务。你可以直接通过使用UserTransaction或将使用调度程序的代码放在使用容器管理事务的SessionBean中来执行此操作。

原文链接:tutorial-lesson-11

第十章:配置、资源的使用以及SchedulerFactory

Quartz的架构设计是模块化的,因此要运行它需要把几个组件组合在一起使用。幸运的是,有一些工具就是为了完成这个目标。

Quartz在能够正常运作之前,下面的几个核心组件必须配置好:

  • ThreadPool
  • JobStore
  • DataSources(如果需要的话)
  • Scheduler(调度器本身,这个是核心)

线程池提供了一组Quartz在执行Job时使用的线程。线程池中的线程越多,并发运行的Job数越多。但是,太多的线程可能会破坏你的系统。大多数Quartz的用户发现,5个左右的线程是充足的 - 因为在任何给定时间,他们的Job数量少于100个,通常不会同时运行这些Job,而且这些Job是短暂的(快速完成)。其他用户认为他们需要10个,15个,50个甚至100个线程,因为他们具有数万个具有各种调度计划的Trigger,最终在任何给定时刻平均执行10到100个Job。为调度器的线程池找到正确的线程池大小完全取决于你需要使用调度器来完成什么工作(这里意思应该是需要按照工作量去定义线程池大小)。没有真正的规则,除了保持线程数量尽可能小(为了你昂贵的服务器资源) - 但需要确保线程数量已足够让你的Job按时启动。请注意,如果Trigger的触发时间到达,并且线程池中没有可用的线程,Quartz将阻塞(暂停)直到线程可用,然后Job将执行 - 也就是在它本该执行的时间点延后一个时间段执行。这甚至可能导致线程的错过触发 - 如果在调度器中配置的“错过触发阈值”的持续时间内没有可用的线程。

ThreadPool接口在org.quartz.spi包中定义,你可以按照你喜欢的方式创建ThreadPool实现。Quartz带有一个名为org.quartz.simpl.SimpleThreadPool(虽然简单,但非常令人满意)的线程池。这个ThreadPool只是在它的线程池中维护一个固定的线程集 - 永远不会增长,永远不会缩小。但是它是非常强大的,测试结果令人满意 - 几乎所有使用Quartz的人都使用这个线程池。

JobStores和DataSource在本教程的第九章:JobStores讨论过。值得注意的是,所有JobStore都实现了org.quartz.spi.JobStore接口 - 任何一个JobStore实现都不符合你的需求,那么你可以自己创建并且自行实现org.quartz.spi.JobStore接口。

最后,你需要创建你的Scheduler实例。Scheduler本身需要被赋予一个名字,告诉其RMI设置,并且设置JobStore和ThreadPool的实例。RMI设置包括调度程序是否应将自身创建为RMI的服务器对象(使其可用于远程连接),要使用的主机和端口等。StdSchedulerFactory(下面将讨论)还可以创建Scheduler实例,这些Scheduler实例可以是创建在远程程序中的Scheduler的代理(RMI存根)。

StdSchedulerFactory

StdSchedulerFactory是org.quartz.SchedulerFactory接口的一个实现。它使用一组属性(java.util.Properties)来创建和初始化Quartz的Scheduler实例。属性通常存储在文件中并从文件中加载,但也可以由程序创建并直接传递到工厂类实例。简单地调用工厂中的getScheduler()将生成Scheduler实例,并初始化它(和它的ThreadPool,JobStore和DataSource)并返回一个公共接口org.quartz.Scheduler的句柄。

在Quartz发行版的"docs/config"目录中有一些示例配置(包括属性的描述)。你可以在Quartz文档的“参考”部分的“配置”手册中找到完整的文档。

DirectSchedulerFactory

DirectSchedulerFactory是另一个SchedulerFactory实现。对于希望编程式创建其Scheduler实例的用户是有用的。通常不鼓励使用它,原因如下:

  • (1)DirectSchedulerFactory要求用户更好地了解他们正在做什么。
  • (2)它不允许声明性配置(不能使用配置文件) - 换句话说,你需要硬编码Scheduler实例的所有属性项。

日志

Quartz使用SLF4J框架来满足所有的日志记录需求。为了“调整”日志记录设置(例如输出量以及输出位置),你需要了解SLF4J框架,这超出了本文档的范围。

如果需要捕获Trigger启动和Job执行的额外信息,可以启用org.quartz.plugins.history.LoggingJobHistoryPluginorg.quartz.plugins.history.LoggingTriggerHistoryPlugin

原文链接:tutorial-lesson-10

第九章:JobStores

JobStore负责记录你提供到调度器的所有“工作数据”:所有的Job、所有的Trigger、所有的Calendar(org.quartz.Calendar)等等。为你的Quartz调度器选择一个合适的JobStore是一个重要的步骤。幸运的是,一旦你明白不同的JobStore之间的差异,那么作出合适的选择是十分简单的。

你声明你提供给用于生成调度器实例对应的SchedulerFactory实例时候用到的属性文件(或对象)中,应该指定你的调度器应使用哪个类型的JobStore(以及它的相关配置)。注意一点:

1
切勿在代码中直接使用JobStore实例。由于某些原因,许多使用者试图这样做。JobStore应该只用于Quartz的后台。你必须告诉Quartz(通过配置)使用哪个类型的JobStore,你在代码中应该只能使用Scheduler接口。

RAMJobStore

RAMJobStore是使用最简单的JobStore,它也是性能最高的(在CPU时间方面)。RAMJobStore的功能显然和它的名字相关:它将其所有数据保存在RAM(内存)中,这就是为什么它是闪电般的快,也是为什么它的的配置这么简单。缺点是当你的应用程序结束(或崩溃)时,所有调度信息都将丢失 - 这意味着RAMJobStore无法履行作业和Trigger上的“非易失性”设置。对于某些应用程序,这是可以接受的 - 甚至是所需的行为,但对于其他应用程序,这可能是灾难性的。

要使用RAMJobStore(并假设您使用的是StdSchedulerFactory),只需将Quartz的JobStore类属性配置指定为org.quartz.simpl.RAMJobStore:

1
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

除此之外没有其他需要担心的配置项。

JDBCJobStore

JDBCJobStore的命名也相当恰当 - 它通过JDBC将其所有数据保存在数据库中。因此,它配置比RAMJobStore要复杂一点,而且也不是那么快。但是,它性能下降并不是很糟糕,特别是如果你使用的数据库表在相应的主键或者外键加上索引。在相对主流的并且有一个像样的局域网(在调度器和数据库之间)的机器上,检索和更新一个触发中的Trigger的时间通常将小于10毫秒。

JDBCJobStore几乎可以在任何类型的任何数据库中使用,已被广泛应用于Oracle,PostgreSQL,MySQL,MS SQLServer,HSQLDB和DB2。要使用JDBCJobStore,必须首先创建一组数据库表以供Quartz使用。你可以在Quartz发行版的"docs/dbTables"目录中找到表创建SQL脚本。如果你的数据库类型尚未有脚本,请查看其中一个脚本,然后以数据库所需的任何方式进行修改。需要注意的一点是,在这些脚本中,所有的表都以前缀"QRTZ_“开始(如表"QRTZ_TRIGGERS"和"QRTZ_JOB_DETAIL”)。你可以通知JDBCJobStore表的前缀是什么(在你的Quartz属性配置中),也就是你也可以修改这个表前缀的值。对于多个调度程序实例,使用不同的前缀可能有助于同一个数据库中的多个调度器实例创建多组表。

创建表后,在配置和启动JDBCJobStore之前,你还有一个重要的决定。你需要确定应用程序需要哪种类型的事务。如果你不需要将调度命令(例如添加和删除Trigger)绑定到其他代码逻辑的事务中,那么可以通过使用JobStoreTX对JobStore进行事务管理(这是最常见的选择)。

如果你需要Quartz与其他事务(即J2EE应用程序服务器)一起工作,那么你应该使用JobStoreCMT - 在这种情况下,Quartz将让应用程序服务器容器管理事务。

最后一个难题是设置一个DataSource,使得JDBCJobStore可以从中获取数据库的连接。定义Quartz的DataSource有下面的几种方式。一种方法是让Quartz创建和管理DataSource本身 - 通过提供数据库的所有连接信息。另一种方法是通过提供JDBCJobStore的DataSource的JNDI名称,让Quartz使用由Quartz正在运行的应用程序服务器来管理DataSource。有关属性的详细信息,请参阅发行版本中"docs/config"文件夹中的示例配置文件。

要使用JDBCJobStore(假设你使用的是StdSchedulerFactory),首先需要将Quartz配置文件中的的JobStore类属性设置为org.quartz.impl.jdbcjobstore.JobStoreTX或org.quartz.impl.jdbcjobstore.JobStoreCMT - 具体可以参考上一段文字的描述,不过决定权还是在你手中。

配置Quartz以使用JobStoreTx

1
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

接下来,你需要为JobStore选择一个DriverDelegate。DriverDelegate负责执行特定数据库可能需要的任何JDBC相关的工作。StdJDBCDelegate是一个使用“vanilla(原意识香草味的,这里大概的意思是原生的)”JDBC代码(和SQL语句)来工作的。如果Quartz没有为你的数据库类型提供特定的DriverDelegate,请尝试使用此DriverDelegate - 我们仅仅为那些使用StdJDBCDelegate(几乎是最常使用)出现了问题的数据库类型提供了特殊的订造的DriverDelegate。其他DriverDelegate可以在"org.quartz.impl.jdbcjobstore"包或其子包中找到。其他DriverDelegate包括DB2v6Delegate(用于DB2版本6及更早版本),HSQLDBDelegate(用于HSQLDB),MSSQLDelegate(用于Microsoft SQLServer),PostgreSQLDelegate(用于PostgreSQL)),WeblogicDelegate(用于使用Weblogic创建的JDBC驱动程序)。

一旦你选择了DriverDelegate后,将其类名设置为JDBCJobStore的对应属性以便JDBCJobStore能够使用它。

为JDBCJobStore配置DriverDelegate

1
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

接下来,你需要通知JobStore你正在使用的表前缀(如上所述)。

配置JDBCJobStore对应的表前缀

1
org.quartz.jobStore.tablePrefix = QRTZ_

最后,你需要设置JobStore应该使用哪个DataSource。DataSource的命名也必须在Quartz配置文件中的属性中定义。在这种情况下,我们指定Quartz应该使用DataSource名称"myDS"(在配置属性中的其他位置也是用这个名称去定义)。

配置JDBCJobStore对应的DataSource

1
org.quartz.jobStore.dataSource = myDS

注意事项一:

1
如果你的调度器一直处于忙碌的状态(满负载)(正在执行的Job数量几乎与线程池大小相同),那么你应该将DataSource中的连接数设置为线程池容量+2

注意事项二:

1
可以将"org.quartz.jobStore.useProperties"配置参数设置为"true"(默认为false),以指示JDBCJobStore将JobDataMap中的所有值都作为字符串,从而JobDataMap中的属性可以作为键值对存储,而不是使用BLOB类型,因为BLOB类型是为了以其序列化形式存储更多复杂的对象。从长远来看,这是更安全的,因为你避免了将非String类序列化为BLOB类型的版本问题。

TerracottaJobStore

TerracottaJobStore提供了一种伸缩性和鲁棒性的手段,它并不使用数据库。这意味着你的数据库可以免受Quartz的负载,可以将数据库所有资源分配给应用程序的其余部分。

TerracottaJobStore可以运行在群集或非群集环境在集群或者非集群环境下,TerracottaJobStore都可以为应用程序的作业数据提供存储介质,即便是应用程序重启的间隙,因为数据是存储在Terracotta服务器中。它的性能比基于使用数据库的JDBCJobStore要好得多(约一个数量级),但比RAMJobStore要慢。

要使用TerracottaJobStore(假设你使用的是StdSchedulerFactory),只需将配置文件中的类名称org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore指定为Quartz的JobStore类属性,并添加一个额外的配置项来指定Terracotta服务器的位置即可:

使用TerracottaJobStore配置Quartz

1
2
org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore
org.quartz.jobStore.tcConfigUrl = localhost:9510

更多关于JobStore和Terracotta的内容可以参阅http://www.terracotta.org/quartz

原文链接:tutorial-lesson-09

第八章:Scheduler监听器

SchedulerListener和TriggerListener或JobListener十分相似,它接收调度器的相关事件,调度器的相关事件不一定和特定的Trigger或者Job相关。

与Scheduler相关的事件包括:添加Job/Trigger、删除Job/Trigger、调度器中的严重错误以及关闭调度器的通知等等。

org.quartz.SchedulerListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface SchedulerListener {

public void jobScheduled(Trigger trigger);

public void jobUnscheduled(String triggerName, String triggerGroup);

public void triggerFinalized(Trigger trigger);

public void triggersPaused(String triggerName, String triggerGroup);

public void triggersResumed(String triggerName, String triggerGroup);

public void jobsPaused(String jobName, String jobGroup);

public void jobsResumed(String jobName, String jobGroup);

public void schedulerError(String msg, SchedulerException cause);

public void schedulerStarted();

public void schedulerInStandbyMode();

public void schedulerShutdown();

public void schedulingDataCleared();
}

SchedulerListener也是通过ListenerManager注册到调度器。SchedulerListener实例几乎可以是任何实现了org.quartz.SchedulerListener接口的对象(<–译者吐槽:这句话怎么看都觉得有点多余)。

添加SchedulerListener

1
scheduler.getListenerManager().addSchedulerListener(mySchedListener);

删除SchedulerListener

1
scheduler.getListenerManager().removeSchedulerListener(mySchedListener);

原文链接:tutorial-lesson-08

第七章:Trigger监听器和Job监听器

监听器(listener)是你创建的对象,主要作用是接收和处理调度器回调的事件(event)。TriggerListener接收到与触发器(Trigger)相关的事件,JobListener接收与调度任务(Job)相关的事件。

与触发器相关的事件包括:触发器正要触发,触发器错失触发,触发器触发完成(调度任务已被触发开始执行,触发器完成当次触发)。

org.quartz.TriggerListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TriggerListener {

public String getName();

public void triggerFired(Trigger trigger, JobExecutionContext context);

public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);

public void triggerMisfired(Trigger trigger);

public void triggerComplete(Trigger trigger, JobExecutionContext context,
int triggerInstructionCode);
}

与调度任务相关的事件包括:Job即将执行时的通知以及Job执行完成时的通知。

org.quartz.JobListener接口

1
2
3
4
5
6
7
8
9
10
11
12
public interface JobListener {

public String getName();

public void jobToBeExecuted(JobExecutionContext context);

public void jobExecutionVetoed(JobExecutionContext context);

public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException);

}

使用自定义的监听器

要创建一个Listener,只需创建一个实现org.quartz.TriggerListener或org.quartz.JobListener接口实现的实例。然后,Listener需要在运行时注册到调度器中。Listener必须指定一个名称(通过实现JobListener接口的getName()方法来传入监听器的名字)。

为了方便起见,自定义监听器也可以继承自JobListenerSupport类或TriggerListenerSupport类,并且只需覆盖你感兴趣的方法。

Listener通过调度器的ListenerManager进行注册,可以通过一个匹配器(Matcher)实例去匹配监听你感兴趣的Job或者Trigger。注意下面的事项:

1
Listener在运行时才注册到调度器中,并且不与Job或者触发器一起存储在JobStore中。这是因为通常监听器是直接集成到应用程序之中(这里的意思大概是监听器中会有应用程序里面相关的逻辑)。因此每当应用程序启动的时候,所有的监听器需要重新注册到调度器中。

添加匹配的特定Job的JobListener

1
scheduler.getListenerManager().addJobListener(myJobListener, KeyMatcher.jobKeyEquals(new JobKey("myJobName", "myJobGroup")));

你可能需要为匹配器和关键类使用静态导入,这将使你定义匹配器的逻辑更简洁:

1
2
3
4
5
6
7
import static org.quartz.JobKey.*;
import static org.quartz.impl.matchers.KeyMatcher.*;
import static org.quartz.impl.matchers.GroupMatcher.*;
import static org.quartz.impl.matchers.AndMatcher.*;
import static org.quartz.impl.matchers.OrMatcher.*;
import static org.quartz.impl.matchers.EverythingMatcher.*;
...etc.

使用静态导入后上面的例子变成这样:

1
scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));

添加对两个特定Group的所有Job感兴趣的JobListener:

1
scheduler.getListenerManager().addJobListener(myJobListener, or(jobGroupEquals("myJobGroup"), jobGroupEquals("yourGroup")));

添加对所有Job感兴趣的JobListener:

1
scheduler.getListenerManager().addJobListener(myJobListener, allJobs());

注册TriggerListener的工作原理和JobListener基本相同。

大多数Quartz的使用者并不使用Listener。但是当应用程序有获取任务调度相关的事件通知的需求时,如果使用了Listener可以避免在Job的逻辑里面加入额外的通知逻辑,这一点对于使用者而言是很方便的。

原文链接:tutorial-lesson-07

第六章:CronTrigger

CronTrigger通常比SimpleTrigger更有用,如果你需要一个基于类似日历的概念重复出现的工作调度计划,而不是SimpleTrigger的精确指定时间间隔。使用CronTrigger,你可以指定任务触发的时间表,例如“每周五中午”或“每个工作日和上午9:30”,甚至“每周一至周五上午9:00至10点之间每5分钟”和1月份的星期五”。即使如此,和SimpleTrigger一样,CronTrigger有一个startTime,它指定何时生效,以及一个(可选的)endTime,用于指定何时停止任务调度。

Cron表达式

Cron-Expressions用于配置CronTrigger的实例。Cron表达式是由七个子表达式组成的字符串,用于描述调度计划的各个细节。这些子表达式用空格分隔,各个部分表示如下:

  • 1.Seconds
  • 2.Minutes
  • 3.Hours
  • 4.Day-of-Month
  • 5.Month
  • 6.Day-of-Week
  • 7.Year (optional field -> 可选的)

一个完整的Cron-Expressions的例子是字符串"0 0 12 ? * WED" - 这意味着“每个星期三下午12:00”。单个子表达式可以包含范围值、"/“或列表。例如,可以用"MON-FRI”、“MON,WED,FRI"或甚至"MON-WED,SAT"代替前一个(例如"WED”)示例中的星期几字段。

通配符('*',在官方文档中是'',估计是官方文档有误,cron不支持空字符串)可用于说明该字段可以取任何值。因此,前一个例子的Month字段中的’‘字符仅仅是“每个月”。因此,Day-of-Month字段中的’'显然意味着“每周的每一天”。

所有字段都有一组可以指定的有效值。这些值应该是相当明显的 - 例如Seconds和Minutes只允许数字0到59,Hours只允许数字0到23。Day-of-Month可以是1-31的任何值,但是你需要注意在给定的月份中有多少天!Month可以指定为0到11之间的值,或者使用字符串JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC。Day-of-Week可以指定为1到7(1 = 星期日)之间的值,或者使用字符串SUN,MON,TUE,WED,THU,FRI和SAT。

'/'字符可用于指定值的增量。例如,如果在Minutes字段中输入"0/15",则表示“从第0分钟开始开始,每隔15分钟”。如果你在Minutes字段中使用"3/20",则意味着“从第3分钟开始,每隔20分钟” - 换句话说,它与Minutes字段中定义的"3,243,43"意义相同。请注意"/35"的细微之处并不代表“每35分钟” - 这意味着“从第0分钟开始,每隔35分钟” - 或者换句话说,与指定"0,35"相同。

‘?‘字符只允许使用在Day-of-Month和Day-of-Week字段中。用于表示“没有特定的值”。当您需要在Day-of-Month和Day-of-Week两个字段之一定义其中一个字段的确切值,那么另一个字段可以用’?’,这点十分有用。请参阅下面的示例(和CronTrigger JavaDoc)以进行说明。

‘L’字符只允许用于Day-of-Month和Day-of-Week字段中。这个字符其实就是’last’的缩写,但是在Day-of-Month和Day-of-Week各自的字段中有不同的含义。例如,Day-of-Month字段中的"L"表示“月的最后一天” - 例如1月31日,非闰年2月28日。如果在Day-of-Week字段中使用它,它只是意味着"7"或"SAT"。但是在使用了Day-of-Month前提下,在Day-of-Week中使用’L’,就意味着“xxx月的最后一个星期xxx”,例如"6L"或"FRIL"都意味着“月的最后一个星期五”。你还可以指定月份最后一天的偏移量,例如"L-3",这意味着一个月份的倒数第三天。当使用’L’选项时,切记不要指定列表或范围值,因为你会得到混乱或者意外的结果。

'W’用于指定给定日期最相近的工作日(星期一至星期五)。例如,如果将"15W"指定为Day-of-Month字段的值,则意思是:“距离本月15日最近的工作日”。

'#‘用于指定月份的“第n个”星期XXX,格式是’n#p’,表示月的第p个星期n。例如,Day-of-Week字段中的"6#3"或"FRI#3"的值表示“月的第三个星期五”。

以下是一些表达式及其含义的示例 - 你可以在JavaDoc的org.quartz.CronExpression中找到更多的资料。

译者注:这里有疑惑的就是’*‘和’?'的区别,总结如下:

  • 问号(?)的作用是指明该字段“没有特定的值”,星号(*)指明该字段“代表所有可能值”
  • 星号(*)和其它值,比如数字,都是给该字段指明特定的值,只不过用星号(*)代表所有可能值
  • Cron-Expression对日期和星期字段的处理规则是它们必须互斥,即只能且必须有一个字段有特定的值,另一个字段必须是“没有特定的值”
  • 问号(?)就是用来对日期和星期字段做互斥的

Cron表达式示例

CronTrigger示例1 - 创建一个触发器的表达式,每5分钟就会触发一次

"0 0/5 * * * ?"

CronTrigger示例2 - 创建一个触发器的表达式,每5分钟触发一次,分钟后10秒(即上午10时10分,上午10:05:10等)。

"10 0/5 * * * ?"

CronTrigger示例3 - 创建一个触发器的表达式,在每个星期三和星期五的10:30,11:30,12:30和13:30创建触发器的表达式。

"0 30 10-13 ? * WED,FRI"

CronTrigger示例4 - 创建一个触发器的表达式,每个月5日和20日上午8点至10点之间每半小时触发一次。请注意,触发器将不会在上午10点开始,仅在8:00,8:30,9:00和9:30

"0 0/30 8-9 5,20 * ?"

请注意,一些调度要求太复杂,无法用单一触发表示 - 例如“每上午9:00至10:00之间每5分钟,下午1:00至晚上10点之间每20分钟”一次。在这种情况下的解决方案是简单地创建两个触发器,并用它们来触发相同的Job。

构建CronTrigger

CronTrigger实例使用TriggerBuilder(用于设置触发器的主要属性)和CronScheduleBuilder(对于CronTrigger特定的属性)构建。要以DSL风格使用这些构建器,请使用静态导入:

1
2
3
import static org.quartz.TriggerBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.DateBuilder.*:

建立一个触发器,每天上午8点至下午5点之间每隔一分钟触发一次

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

建立一个触发器,将在上午10:42每天触发一次

1
2
3
4
5
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(dailyAtHourAndMinute(10, 42))
.forJob(myJobKey)
.build();

或者:

1
2
3
4
5
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(cronSchedule("0 42 10 * * ?"))
.forJob(myJobKey)
.build();

建立一个触发器,将在星期三上午10:42在TimeZone(系统默认值)之外触发

1
2
3
4
5
6
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(weeklyOnDayAndHourAndMinute(DateBuilder.WEDNESDAY, 10, 42))
.forJob(myJobKey)
.inTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))
.build();

或者:

1
2
3
4
5
6
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(cronSchedule("0 42 10 ? * WED"))
.inTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))
.forJob(myJobKey)
.build();

CronTrigger Misfire说明

以下错过触发配置可以用于通知Quartz当CronTrigger发生错失触发时应该做什么。(Misfire策略的介绍可以参考第四章:关于Trigger的更多细节)。这些策略以常量的形式在CronTrigger接口中定义(常量上包括描述其行为的JavaDoc)。这些常量包括:

1
2
3
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
MISFIRE_INSTRUCTION_DO_NOTHING
MISFIRE_INSTRUCTION_FIRE_NOW

所有触发器都可以使用Trigger.MISFIRE_INSTRUCTION_SMART_POLICY错过触发策略,它就是所有触发器默认的错失触发策略。“SMART POLICY”策略在使用了CronTrigger的情况下解释为MISFIRE_INSTRUCTION_FIRE_NOW。CronTrigger.updateAfterMisfire()方法的JavaDoc解释了此行为的确切细节。

在构建CronTriggers时,你可以将misfire指令指定为cron schedule(cron 调度)的一部分(通过CronSchedulerBuilder):

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

原文链接:tutorial-lesson-06

第五章:SimpleTrigger

SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行并且以指定的间隔重复执行若干次(其实永远重复也可以)。比如,你有一个Trigger,你可以设置它在2015年1月13日的上午11:23:54准时触发,或者在当前这个时间点触发,并且每隔2秒触发一次,一共重复5次。

根据描述,你可能已经发现了,SimpleTrigger的属性包括:开始时间、结束时间、重复次数以及重复的间隔。这些属性的含义与你所期望的是一致的,只是关于结束时间有一些地方需要注意。

重复次数,可以是0、正整数,以及常量SimpleTrigger.REPEAT_INDEFINITELY(实际值为-1,即无限重复执行次数)。重复的间隔,必须是0,或者long型的正数,单位是毫秒。注意,如果重复间隔为0,Trigger将会以重复次数并发执行(或者以Scheduler可以处理的近似并发数并发执行)。

如果你还不熟悉Quartz的DateBuilder类,了解后你会发现使用它可以非常方便地构造基于startTime(或endTime)的调度策略。

endTime属性的值会覆盖设置重复次数的属性值;比如,你可以创建一个Trigger,在终止时间之前每隔10秒执行一次,你不需要去计算在开始时间和终止时间之间的重复次数,只需要设置终止时间并将重复次数设置为REPEAT_INDEFINITELY(当然,你也可以将重复次数设置为一个很大的值,并保证该值比Trigger在终止时间之前实际触发的次数要大即可)。

SimpleTrigger实例通过TriggerBuilder设置主要的属性,通过SimpleScheduleBuilder设置与SimpleTrigger相关的属性。要使用这些builder的静态方法,需要静态导入:

1
2
3
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.DateBuilder.*:

下面的例子,是基于简单调度(simple schedule)创建的Trigger。建议都看一下,因为每个例子都包含一个不同的实现:

指定时间开始触发,不重复触发

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

指定时间触发,每隔10秒触发一次,重复10次

1
2
3
4
5
6
7
8
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();

5分钟以后开始触发,仅触发一次

1
2
3
4
5
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();

立即触发,每个5分钟触发一次,直到22:00

1
2
3
4
5
6
7
trigger = newTrigger()
.withIdentity("trigger7", "group1")
.withSchedule(simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever())
.endAt(dateOf(22, 0, 0))
.build();

建立一个触发器,将在下一个小时的整点触发,然后每2小时重复触发一次

1
2
3
4
5
6
7
8
9
10
11
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提供的方法,以便了解对上述示例中未提到的选项。

1
TriggerBuilder(以及Quartz的其它builder)会为那些没有被显式设置的属性选择合理的默认值。比如:如果你没有调用withIdentity(..)方法,TriggerBuilder会为Trigger生成一个随机的名称;如果没有调用startAt(..)方法,则默认使用当前时间,即Trigger立即生效。

SimpleTrigger Misfire策略

SimpleTrigger有几个misfire相关的策略,告诉quartz当misfire发生的时候应该如何处理。(Misfire策略的介绍可以参考第四章:关于Trigger的更多细节)。这些策略以常量的形式在SimpleTrigger中定义(JavaDoc中介绍了它们的功能)。这些策略包括:

SimpleTrigger的Misfire策略常量

1
2
3
4
5
6
7
MISFIRE_INSTRUCTION_SMART_POLICY 
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

译者补充:其实这些错过触发策略基本都可以从名称得出它们的实际操作,总结如下:

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire的错过触发,并且不会影响现有的调度时间。比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20次触发,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性并发触发20次。
  • MISFIRE_INSTRUCTION_FIRE_NOW:忽略已经MisFire的触发,并且立即触发一次。这通常只适用于只执行一次的任务。
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT:将startTime设置当前时间,立即重新触发,包括MisFire的触发。
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT:将startTime设置当前时间,立即重新触发,不包括MisFire的触发。
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT:在下一次调度时间点触发,包括MisFire的的触发。
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT:在下一次调度时间点触发,不包括MisFire的的触发。
  • MISFIRE_INSTRUCTION_SMART_POLICY:默认策略,大致意思是“把处理逻辑交给聪明的Quartz去决定”。
    • 如果是只执行一次的调度,使用MISFIRE_INSTRUCTION_FIRE_NOW。
    • 如果是无限次的调度(repeatCount是无限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT。
    • 否则,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT。

其实,错过触发的策略相对复杂,可以参考Quartz Scheduler Misfire Instructions Explained

回顾一下,所有的Trigger都有一个Trigger.MISFIRE_INSTRUCTION_SMART_POLICY策略可以使用,该策略也是所有Trigger的默认策略。

如果使用smart policy,SimpleTrigger会根据实例的配置及状态,在所有MISFIRE策略中动态选择一种Misfire策略。可以从SimpleTrigger.updateAfterMisfire()的JavaDoc中解释了该动态行为的具体细节。

在使用SimpleTrigger构造Trigger时,misfire策略作为简单调度(simple schedule)的一部分进行配置(通过SimpleSchedulerBuilder设置),例子如下:

1
2
3
4
5
6
7
trigger = newTrigger()
.withIdentity("trigger7", "group1")
.withSchedule(simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever()
.withMisfireHandlingInstructionNextWithExistingCount())
.build();

原文链接:tutorial-lesson-05

第四章:关于Trigger的更多细节

与Job一样,Trigger也很容易使用,但是还有一些扩展选项需要理解,以便更好地使用Qartz。Trigger也有很多类型,我们可以根据实际需要来选择。

最常用的两种Trigger会分别在第五章:SimpleTrigger第六章:CronTrigger中讲到。

Trigger的公共属性

所有类型的Trigger都有TriggerKey这个属性,表示Trigger的身份(唯一标识);除此之外,Trigger还有很多其它的公共属性。这些属性,在构建Trigger的时候可以通过TriggerBuilder设置。Trigger的公共属性有:

  • jobKey属性:当Trigger触发时被执行的Job的身份(唯一标识);
  • startTime属性:设置trigger第一次触发的时间点,该属性的值是java.util.Date类型,表示某个指定的时间点;有些类型的Trigger,会在设置的startTime时立即触发,有些类型的Trigger,表示其触发是在startTime之后开始生效。比如,现在是1月份,你设置了一个Trigger–“在每个月的第5天执行”,然后你将startTime属性设置为4月1号,则该Trigger第一次触发会是在几个月以后了(即4月5号)。
  • endTime属性:表示Trigger失效的时间点,该属性的值是java.util.Date类型。比如,”每月第5天执行”的trigger,如果其endTime是7月1号,则其最后一次执行时间是6月5号。

其它的属性,会在下文中解释。

Priority(优先级)

有时候,当你有很多Trigger实例(或者你的Quartz线程池中只有有少量工作线程,不足以触发所有的触发器)时,Quartz可能没有足够的资源来立即触发所有计划同时触发的触发器。在这种情况下,你可能想要控制哪些Trigger可以优先使用(在当前可用的)Quartz工作线程。为此,你可以在触发器上设置优先级属性。如果N个触发器同时触发,但当前只有Z个工作线程可用,则首先执行具有最高优先级的Z个触发器。如果您没有在触发器上设置优先级,那么它将使用默认优先级5。priority属性的值可以是任意整数,正数或者负数都是允许的。

  • 注意:只有同时触发的Trigger之间才会比较优先级。10:59触发的Trigger总是在11:00触发的Trigger之前执行。
  • 注意:如果Trigger是可恢复的,在恢复后再调度时,优先级与原Trigger是一样的。

Misfire Instructions(错过触发策略)

Trigger还有一个重要的属性misfire(这里应该称为“错过触发策略”更合理,本质就是一次处理触发器错失触发的策略);如果Scheduler关闭了,或者Quartz线程池中没有可用的线程来执行jJb,此时持久性的Trigger就会错过(miss)其触发时间,即错过触发(misfire)。不同类型的Trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据Trigger的类型和配置动态调整行为。当Scheduler启动的时候,查询所有错过触发(misfire)的持久化的Trigger。然后根据它们各自的misfire机制更新Trigger的信息。当你在项目中使用Quartz时,你应该对各种类型的Trigger的misfire机制都比较熟悉,这些misfire机制在JavaDoc中有说明。关于misfire机制的细节,会在讲到具体的Trigger时再作介绍。

Calendar(日历)

Quartz的org.quartz.Calendar对象(不是java.util.Calendar对象)可以在定义和存储Trigger的时候与Trigger进行关联。Calendar用于从Trigger的调度计划中排除时间段。比如,可以创建一个Trigger,每个工作日的上午9:30执行,然后增加一个Calendar,排除掉所有的业务假期(也就是工作日中的法定假期)。

任何实现了Calendar接口的可序列化对象都可以作为Calendar对象,Calendar接口如下:

1
2
3
4
5
6
7
8
9
package org.quartz;

public interface Calendar {

public boolean isTimeIncluded(long timeStamp);

public long getNextIncludedTime(long timeStamp);

}

译者吐槽:这里贴出来的Calendar接口的源码估计是老版本的org.quartz.Calendar接口,对比了下2.2.x版本的同一个接口并不是长这样的。

注意到这些方法的参数类型为long。你也许猜到了,他们就是毫秒单位的时间戳。即Calendar排除时间段的单位可以精确到毫秒。你也许对“排除一整天”的Calendar比较感兴趣。Quartz提供的org.quartz.impl.HolidayCalendar类可以很方便地实现。

Calendar必须先实例化,然后通过addCalendar()方法注册到Scheduler。如果使用HolidayCalendar,实例化后,需要调用addExcludedDate(Date date)方法从调度计划中排除时间段。以下示例是将同一个Calendar实例用于多个Trigger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

接下来的几个章节将介绍触发器的构建/构建细节。现在,只要明确上面的代码会创建两个触发器,每个触发器都计划每天触发一次。但是,在日历(Calendar)排除的期间内发生的任何触发都将被跳过。

请参阅org.quartz.impl.calendar包,了解适合你的Calendar实现。

原文链接:tutorial-lesson-04