零、Quartz是什么?能干什么?
Quartz是一个开源的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能,比如x年x月的每个星期五上午8点到9点,每隔10分钟执行1次。Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。Quartz完全使用Java开发,可以集成到各种规模的应用程序中。它能够承载成千上万的任务调度,并且支持集群。它支持将数据存储到数据库中以实现持久化,并支持绝大多数的数据库。它将任务与触发设计为松耦合,即一个任务可以对应多个触发器,这样能够轻松构造出极为复杂的触发策略。
本文是对Quartz Job Scheduler Tutorials的全文翻译,作为笔者自己的学习笔记。当前日期是2016年2月20日,最新版本是2.2.x,官方在线文档的后续更新本文不再跟进。
一、使用Quartz
在使用调度器之前,它需要被实例化,你可以使用SchedulerFactory来实现。部分Quartz 用户可能会在JNDI 存储中保存一个factory 实例,而部分用户可能会发现直接初始化并使用factory 实例是很简单的(就像例子中那样)。
当调度器实例化以后,它可以被启动,置为备用模式或停止。注意一旦它被停止是不能重新启动的,除非重新初始化。在调度器没启动时触发器是不会激活的,在暂停状态下也不会。
下面是一个示例代码片段,演示了实例化并启动调度器,然后调度任务执行。
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) .withIdentity("myJob", "group1") .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = newTrigger() .withIdentity("myTrigger", "group1") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger);
如你所见,quartz 的使用非常简单,在第2节我们将简要介绍任务和触发器,以及Quartz的API,到时候你就能对示例有更深的理解。
二、核心API以及任务和触发器介绍
2.1 Quartz API
Quartz API主要包含以下接口:
Scheduler:与调度器交互的主要API。
Job:一个接口,实现该接口的组件将被调度器运行。
JobDetail:用于定义Job实例。
Trigger:定义了一个Job如何被调度器所运行。
JobBuilder:用于定义/构建JobDetail 实例。
TriggerBuilder:用于定义/构建Trigger实例。
Scheduler由SchedulerFactory创建,并随着shutdown方法的调用而终止。创建后它将可被用来添加、删除或列出Job和Trigger,或执行一些调度相关的工作,(比如暂停)。只有通过start()方法启动后它才会真的工作。
Quartz提供一些列的Builder类来定义领域特定语言(也被称为流接口)。从示例代码中能看到,Job以及Trigger都可以通过Builder来创建。
各种”ScheduleBuilder”包含了创建各种类型调度器的方法。DateBuilder 类包含方便地创建指定时间点的日历实例(比如表示下一个整时的时间)的各种方法。
2.2 任务和触发器
Job是一个实现了Job接口的类,它只有execute这一个方法。它的形式如下所示:
package org.quartz; public interface Job { public void execute(JobExecutionContext context) throws JobExecutionException; }
当Job的Trigger激活时,该方法将被Scheduler的一个线程调用执行。JobExecutionContext 是传递给该方法的运行时环境信息,包括调用它的调度器、触发该执行的Trigger、JobDetail对象以及其他信息。
JobDetail是在Job被添加到Scheduler时由应用程序创建的,它包含了关于Job的各种属性信息,都在JobDataMap中。
Trigger用于触发任务的执行。它也会关联到的一个JobDataMap--当需要把数据传递给触发器特定的某个任务时这很有用。Quartz提供了各种触发器,然而最常用的是SimpleTrigger 和CronTrigger。
SimpleTrigger是非常好用的,如果你只需要让任务在指定的时间执行,或者让它在指定的时间执行,重复N次,以T为周期。CronTrigger用于进行类似于日历时间的触发,比如每个周五的下午,或者每个月10号的10点。
我们将任务和触发器设计为互相独立的,这种松耦合有许多好处:任务可以被创建和存储而不依赖于触发器,并且一个任务可以关联到多个触发器。另外,即使触发器已经过期,关联的任务仍然可以被重新配置,而不需要重新定义;同样,你也可以对触发器进行修改或替换而不需要重新定义关联的任务。
2.3 身份识别
任务和触发器注册到调度器时都会有一个识别KEY。任务和触发器都可以添加到组,因此它们的KEY名称在同一个组内必须是唯一的,完整的KEY名由KEY名+组名组成。
到这里你可能会对触发器和调度器有一个大概的了解,在下面两章将详细介绍。
三、Job & JobDetail更多细节
虽然你的Job代码知道如何执行真实的工作,但是Quartz需要被告知
那个任务所拥有的各种属性值,这是通过JobDetail提供的。JobDetail由JobBuilder类构造。
构造器每次执行execute方法时,它都会先创建一个新的实例,这一行为的一个影响是Job必须有一个无参数构造方法;另一个影响是,Job类所定义的的状态数据是无意义的,因为它们在执行时无法被保存。
现在你可能会问,如何向Job实例提供属性/配置?如何在执行函数中保存状态数据?答案是JobDataMap,它是JobDetail对象的一部分。
3.1 JobDataMap
JobDataMap是JAVA Map接口的一个实现,并添加了一些实现用于方便地存取基本类型数据。它能够存储任意规模的,你想要由Job在执行时使用的数据。
下面是一个在定义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();
下面是一个任务执行时从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); } }
Triggers也可以与JobDataMap进行关联。比方说,当一个任务关联到多个触发器时,你就可以为每一个不同的触发器提供不同的数据。
任务执行期间,可以由JobExecutionContext 的getMergedJobDataMap方法获取一个由JobDetail 和Trigger所提供的合并体的JobDataMap,且后者的同名KEY的值会覆盖前者。当然,你也可以使用context.getJobDetail().getJobDataMap()来获取JobDetail 的数据,使用context.getTrigger().getJobDataMap()来获取Trigger的数据。
3.2 Job实例
你可以只创建一个Job,然后创建多个JobDetail ,每一个都有自己的属性和JobDataMap,然后把它们都添加到调度器中,这样调度器内部就保存它的多个实例的定义。
比如说,你可以建立一个Job类叫做SalesReportJob,它会期望(通过JobDataMap)传递过来的参数指定了销售报告所针对的人员名称。他们可能会为这个Job创建多个定义,比如SalesReportForJoe和SalesReportForMike,分别指定了Joe和Mike。
当触发器激活时,与他关联的JobDetail 被加载,那么相关的Job类将被JobFactory 实例化。默认的JobFactory 只是简单地调用newInstance()方法,然后试图调用类内与JobDataMap的KEY名相匹配的setter方法。你可能想建立自己的JobFactory ,以便使你应用程序的IOC或DI container 来产生/初始化Job实例。
在Quartz语言中,我们将所有存储的JobDetail 称为“job 定义”或者“JobDetail 实例”;将所有的执行中的任务称为“job 实例”或者“job 定义的实例”;一般情况下如果我们使用Job这个词我们是指一个命名的定义或JobDetail 。当我们描述实现了Job接口的类时,一般会使用“job class”这个词。
3.3任务状态和并发
这里介绍一些关于任务状态和并发的额外的说明。有许多标注可以被添加到job class中,影响到Quartz's 在各方面的行为。
@DisallowConcurrentExecution
这个标注添加到job class中就是告诉Quartz,我不能被并行执行。拿前面的例子来说,如果SalesReportJob添加了这个标注,那么在同一时间只能有一个SalesReportForJoe的实例在执行,但是SalesReportForMike的实例可以同时执行。这个限制是基于JobDetail而不是 job class的实例。然而它被决定(在Quartz的设计中)要作用于类自身之上,因为它经常会影响到类的编写方式。
@PersistJobDataAfterExecution
它告诉Quartz,在成功执行完(没有抛出异常)之后要更新JobDetail的JobDataMap。这样下一次执行时该任务就会收到最新的数据。与@DisallowConcurrentExecution 一样,它也作用于一个 job definition 实例,而不是job类的实例。
@PersistJobDataAfterExecution
如果使用了本标注,那么你要强烈考虑同时使用DisallowConcurrentExecution 标注,以避免当同一个任务的2个实例并行执行时最终保存的数据是什么样的(竞争条件下)这种冲突。
3.4任务的其他属性
持久性
如果一个任务不是持久的,当不再有关联的活动触发器时它将从调度器中被删除。换句话说,非持久任务的生命期取决于它的触发器。
请求恢复
如果一个任务请求恢复,并且在执行中调度器遭到了硬关闭,那么当调度器重新启动时它将重新执行。在这种情况下,JobExecutionContext.isRecovering()将返回true。
3.5任务执行异常
最后,我们要告诉你Job.execute(..)的一些细节。在该方法中你只被允许抛出JobExecutionException这一种异常(包括运行时异常)。因此,你通常要把所有代码包裹在try-catch块中。你还需要花一点时间来查看JobExecutionException的文档,以便于你能够向调度器发出各种指令来根据你的意愿来处理异常。
四、触发器更多细节
Triggers与Job一样的易于使用,但是它包含了大量的可定制的选项。前面提到过,有不同类型的Triggers可供你选择以满足不同的需求。
4.1 触发器通用属性
除了用于标识身份的TriggerKey 外,还有许多各种Triggers所通用的属性,它们是在创建Triggers定义时通过TriggerBuilder 设置的。
下面列出这些通用属性:
jobKey:指定了Triggers激活时将被执行的job的身份;
startTime:指定了调度器何时开始作用。该值是一个java.util.Date对象。某些类型的Triggers确实会在startTime激活,而另一些Triggers仅简单地标记下调度器将来应当启动的时间。这意味着你可以存储一个Trigger和一个“1月的第5天”这样的调度器,如果startTime设在了4月1号,那么第一次启动时间要在几个月以后。
endTime:指定了Trigger的调度器在何时不再生效。换句话说,一个触发器的如果设置为“每月的第5天”并且endTime为“7月1日”那么它最后一次激活应该是在6月5日。
其它属性在以后的章节中介绍。
4.2 优先级
有时候当你有许多触发器(或者Quartz 线程池中的线程很少)时,Quartz 可能没有足够的资源来同时激活所有的触发器。这时,你可能想要控制让哪一个触发器先被触发。出于这个目的,你可以设置触发器的priority 属性。如果有N
个触发器将要同时被激活,然而Quartz 只有Z个线程,那么只有前Z个优先级最高的触发器将被激活。如果没有设置,优先级的默认值为5。优先级的取值适用于所有整数,包括正数和负数。
注意:仅当触发器将同时被激活时才会比较优先级。设定在10:59的触发器永远比设定在11:00的触发器先激活。
注意:如果检测到一个job要求恢复,那么恢复后的优先级不变。
4.3 激活失败指令(Misfire Instructions)
触发器的另一个重要属性是激活失败指令。当一个持久的触发器因为调度器被关闭或者线程池中没有可用的线程而错过了激活时间时,就会发生激活失败(Misfire)。不同类型的触发器具有不同的激活失败指令。默认情况下它们使用一个“聪明策略”指令,它具有基于触发器类型和配置的动态行为。当调度器启动时,它会搜索所有激活失败的持久触发器,然后根据各自已配置的激活失败指令来对它们进行更新。当你在项目中使用Quartz时,你应当熟悉相应触发器的激活失败指令,它们在JavaDoc中有解释。在本教程针对各种触发器的章节中有更详细的介绍。
4.4 日历
Quartz Calendar 对象(不是java.util.Calendar对象)可以在触发器被定义时被关联并存储到调度器。在从触发器的激活策略中排除时间块时Calendar 非常有用。比如说,你可以添加一个触发器,它在每天早上的9:30激活,然后添加一个Calendar 来排除掉所有的商业假日。
Calendar 可以是任何实现了Calendar 接口的可序列化对象。Calendar 接口如下所示:
package org.quartz; public interface Calendar { public boolean isTimeIncluded(long timeStamp); public long getNextIncludedTime(long timeStamp); }
注意上述方法的参数类型是long,就像你猜想的那样,它们是毫秒单位的时间戳。这意味着Calendar 能够以毫秒的精度来排除时间块。你很可能会对排除整日感兴趣,为了方便起见,Quartz 包含了org.quartz.impl.HolidayCalendar,它可以实现这个。
Calendars 必须被实例化并使用调度器的addCalendar(..) 进行注册。如果你使用HolidayCalendar,在初始化以后你必须使用 addExcludedDate(Date date) 来生成你想要排除的日期。同一个Calendar实例可以被不同的触发器使用,就像下面这样:
Calendar Example
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
触发器的创建/构造将在后面的章节中介绍,这里你只需要记住上面的代码生成了2个触发器,均在每天激活。然而,排除日期中的激活都会被跳过。
五、SimpleTrigger
SimpleTrigger 能够满足你这样的需求:你希望job在某一个特定的时间执行,或者在某刻执行后以一个指定的周期进行重复。比如说,你希望某个任务在2015年1月13日早上11:23:54执行,或者在该时间执行后每隔10秒又重复执行5次。
通过上面的描述,你不难发现SimpleTrigger 的属性应当包含:start-time,end-time, repeat count, repeat interval。
repeat count可以是0或者一个正整数,或者SimpleTrigger.REPEAT_INDEFINITELY值。repeat interval必须是0或者一个正的长整型数,代表毫秒数。注意,0表示任务的重复将并行(或以调度器的管理能力近似并行)执行。
如果你对Quartz的DateBuilder类还不熟悉,你会发现它对于根据startTime (或 endTime)计算触发器的激活时间非常有用。
endTime属性(如果被设置)将会覆盖repeat count属性。这很有用,如果你想让触发器每隔10秒触发一次直到给定的时间--相对于你自己根据startTime 和endTime来计算repeat count,你可以直接设置endTime,然后将 repeat count设置为REPEAT_INDEFINITELY 。
SimpleTrigger 的实例通过TriggerBuilder(包括SimpleTrigger的主要属性) 和SimpleScheduleBuilder (包括SimpleTrigger特有属性)来构建,为了以DSL风格引用以上类,使用如下的静态引用:
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();
构造一个构造器,在指定时刻激活,并以10秒为周期重复10次:
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次:
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停止:
trigger = newTrigger() .withIdentity("trigger7", "group1") .withSchedule(simpleSchedule() .withIntervalInMinutes(5) .repeatForever()) .endAt(dateOf(22, 0, 0)) .build();
构造一个构造器,在下一个整点激活,并以2小时为周期无限重复:
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所有可以函数,这样能够熟悉以上例子没有介绍到的可能会对你由于的选项。
注意,TriggerBuilder(以及Quartz的其它builder)通常会为你没有明确指定的属性选择一个合理的值。比如:如果你没有调用*withIdentity(..)*方法,那么TriggerBuilder 将为你的触发器生成一个随机的名字;如果你没有调用 *startAt(..)*方法,那么当前时间(立刻激活)将被赋值。
5.1 SimpleTrigger 激活失败指令
SimpleTrigger 有很多指令可用于通知Quartz 当发生激活失败时应如何处理(激活失败已经在第4节中介绍)。这些指令被定义为SimpleTrigger类的常数(JavaDoc描述了它们的行为)。这些指令包括:
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.MISFIRE_INSTRUCTION_SMART_POLICY指令,这是所有类型的触发器的默认指令。
如果使用了“聪明策略”,SimpleTrigger 将根据配置和触发器实例的状态从它的指令中动态选择。JavaDoc关于SimpleTriggerImpl .updateAfterMisfire() 方法的介绍解释了这一动态行为的细节,具体如下。
Repeat Count=0:instruction selected = MISFIRE_INSTRUCTION_FIRE_NOW;
Repeat Count=REPEAT_INDEFINITELY:instruction selected = MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
Repeat Count>0:instruction selected = MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
六、CronTriggers
如果你需要的调度器的任务循环是基于类似于日历那种,而不是以一个明确的周期为依据,那么CronTriggers往往比SimpleTrigger更有用。
有了CronTrigger,你可以指定“每个周五下午”或者“每个工作日早上9:30”,甚至是“一月的每个周一,周二和周五早上9:00--10:00之间每隔5分钟1次”这样的调度器。即使如此,与SimpleTrigger一样,CronTrigger也有一个startTime和一个(可选的)endTime,指定了调度器在应该在何时启动和停止。
6.1 Cron表达式
Cron表达式(Cron Expressions)用于配置CronTrigger实例。它是由7个子表达式组成的字符串,指定了调度器的每一个细节。这些子表达式由空格分隔,分别代表以下内容:
l 秒
l 分
l 时
l 日
l 月
l 星期
l 年(可选)
以 "0 0 12 ? * WED"为例,它表示每月每个周二的12点。每个子表达式都可以包含范围与/或列表。比如,前例中的”WED”部分可以替换为 "MON-FRI", "MON,WED,FRI", 甚至"MON-WED,SAT"。
掩码(“*”)代表任何允许的值。因此,“月”区域的“*”表示每个月;“星期”区域的“*”表示一周的每一天。
所有区域都有一些可分配的有效值,这些值都是非常显而易见的。
斜杠('/')表示值的增加。比如说,如果在“分钟”区域填写'0/15',它表示从0分开始并每隔15分钟。 '3/20'表示从3分开始并 每隔20分钟,即03,23,43分。 "/35"等同于"0/35"。注意,原文档说,“ "/35"不代表每隔35分钟,而是每个小时的第35分,及等同于'0,35'。”经测试发现,该说法不正确。应等同于"0/35"。
问号 '?' 可被“日”和“星期”域使用。它表示没有指定值。当你指定了这俩域的其中一个时,另一个域就可以使用问号。
字母'L' 可被“日”和“星期”域使用。它是”last”的缩写,但是在这两个域中有着不同的含义。比如,“日”区域中的"L"表示本月的最后一天,比如1月的31日或者平年2月的28日。如果只有它自己用在“星期”域中,它表示7或者星期六。如果在“星期”域跟在某个星期的后面,那它表示本月的上一个xx日。比如,"6L" 或者 "FRIL"都表示本月上一个星期五。你也可以为本月的最后一天指定个偏移量,比如 "L-3"表示本月的倒数第3天。当使用L选项时,非常重要的一点是不要同时指定列表或范围,否则你会得到混乱的结果。
字母'W'用于指定距离某天最近的工作日(周一到周五)。比如,你在“日”区域使用了"15W",那它表示距离本月15号最近的工作日。
井号 '#' 用于本月第xx个工作日。比如,“星期”域的"6#3" or "FRI#3"表示本月第3个星期五。
6.2 Cron表达式实例
下面是一些表达式实例及其含义。你可以在JavaDoc中org.quartz.CronExpression部分找到更多内容。
CronTrigger Example 1:"0 0/5 * * * ?"
每天,从00分开始每隔5分钟;
CronTrigger Example 2:"10 0/5 * * * ?"
每天,从00分开始每隔5分钟,在该分钟的10秒;
CronTrigger Example 3:"0 30 10-13 ? * WED,FRI"
每月星期二和星期五上午,10点至13点,期间的每个半点;
CronTrigger Example 4:"0 0/30 8-9 5,20 * ?"
每月5号和20号,早上8点至9点,期间的每个半点;注意不包含10:00。
注意,有一些调度要求用一个触发器来表达可能过于复杂,比如“早上9点至10点每隔5分钟,以及早上10点至13点每隔20分钟”。这时你可以构造2个触发器,然后绑定到同一个任务上。
6.3 构造CronTrigger
CronTrigger 的实例通过TriggerBuilder(包括CronTrigger 的主要属性) 和CronScheduleBuilder (包括CronTrigger 特有属性)来构建,为了以DSL风格引用以上类,使用如下的静态引用:
import static org.quartz.TriggerBuilder.*; import static org.quartz.CronScheduleBuilder.*; import static org.quartz.DateBuilder.*:
构造一个每天上午8点到下午五点之间每隔2分钟的CronTrigger :
trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(cronSchedule("0 0/2 8-17 * * ?")) .forJob("myJob", "group1") .build();
构造一个每天上午10:42触发的CronTrigger :
trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(dailyAtHourAndMinute(10, 42)) .forJob(myJobKey) .build();
或者
trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(cronSchedule("0 42 10 * * ?")) .forJob(myJobKey) .build();
构造一个非系统默认时区内每周二上午10:42触发的CronTrigger :
trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(weeklyOnDayAndHourAndMinute(DateBuilder.WEDNESDAY, 10, 42)) .forJob(myJobKey) .inTimeZone(TimeZone.getTimeZone("America/Los_Angeles")) .build();
或者
trigger = newTrigger() .withIdentity("trigger3", "group1") .withSchedule(cronSchedule("0 42 10 ? * WED")) .inTimeZone(TimeZone.getTimeZone("America/Los_Angeles")) .forJob(myJobKey) .build();
6.4 CronTrigger激活失败指令
下列实例用于CronTrigger发生激活失败(misfire)时通知Quartz 如何处理。这些指令被定义为CronTrigger类内部的常量,包括:
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
MISFIRE_INSTRUCTION_DO_NOTHING
MISFIRE_INSTRUCTION_FIRE_NOW
如前所述,Trigger.MISFIRE_INSTRUCTION_SMART_POLICY依然是CronTrigger的默认指令,然而它默认选择MISFIRE_INSTRUCTION_FIRE_NOW作为执行策略。更详细的解释请查看CronTriggerImpl类的updateAfterMisfire函数。
CronTrigger激活失败指令在构造CronTrigger实例时指定。
七、触发器监听器与任务监听器
触发器监听器(TriggerListeners)和任务监听器(JobListeners )分别接收关于Trigger和Job的事件。触发器相关的事件包括:触发器激活、激活失败以及激活完成(执行的任务运行完毕)。
TriggerListener 接口形式如下:
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); }
任务相关的事件包括:任务即将被执行的通知、任务执行完毕的通知。JobListener 接口形式如下:
public interface JobListener { public String getName(); public void jobToBeExecuted(JobExecutionContext context); public void jobExecutionVetoed(JobExecutionContext context); public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException); }
7.1 使用你自己的监听器
实现TriggerListener和/或JobListener即可实现你自己的监听器。监听器需要注册到调度器,且必须给他一个名称(或者说它们必须能够通过其getName方法来获取其名称)。
为了方便起见,你可以继承JobListenerSupport 类或者TriggerListenerSupport ,然后重载你感兴趣的方法即可。
监听器需要与一个匹配器一起注册到调度器,该匹配器用于指定监听器想要接收哪个触发器/任务的事件。监听器在运行期间注册到调度器,且并没有与触发器和任务一起存储到JobStore 中。这是因为监听器通常是你应用的一个结合点,因此每次应用运行时它都需要重新注册到调度器。
在指定任务中添加感兴趣的任务监听器方法如下:
scheduler.getListenerManager().addJobListener(myJobListener, KeyMatcher.jobKeyEquals(new JobKey("myJobName", "myJobGroup")));
通过以下对匹配器和Key的静态引用,可以使代码更整洁:
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.
它使代码变成这样:
scheduler.getListenerManager().addJobListener(myJobListener, jobKeyEquals(jobKey("myJobName", "myJobGroup")));
在组中为所有任务添加感兴趣的任务监听器方法如下:
scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));
在两个指定组中为所有任务添加感兴趣的任务监听器方法如下:
scheduler.getListenerManager().addJobListener(myJobListener, or(jobGroupEquals("myJobGroup"), jobGroupEquals("yourGroup")));
为所有任务添加感兴趣的任务监听器方法如下:
scheduler.getListenerManager().addJobListener(myJobListener, allJobs());
注册TriggerListeners 的方法与以上相同。大多数Quartz用户并不会用到监听器,然而应用程序需要得到事件通知的话它们是非常易用的,而且不需要任务本身显式地通知应用程序。
八、调度器监听器
调度器监听器(SchedulerListeners)与TriggerListeners 和JobListeners非常类似,除了它是接收来自于调度器自己的事件--不一定与某个特定的trigger或job相关。
Scheduler相关的事件包括:trigger/job的添加与删除、Scheduler内部的严重错误、调度器被关闭的通知,等等。
SchedulerListener 接口形式如下:
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(); }
SchedulerListeners 注册到调度器的ListenerManager。基本上任何实现了SchedulerListener 的类都可以作为调度器监听器。
添加SchedulerListener的方法如下:
scheduler.getListenerManager().addSchedulerListener(mySchedListener);
删除SchedulerListener的方法如下:
scheduler.getListenerManager().removeSchedulerListener(mySchedListener);
九、JobStores
JobStore的职责是记录所有你提供给调度器的“工作数据”:任务、触发器、日历等等。为你的调度器实例选择合适的JobStore的一个非常重要的步骤。幸运的是,一旦你理解了它们之间的不同,选择起来是非常容易的。你在配置文件中声明将要选择哪个JobStore,以此将它提供给SchedulerFactory,因为你的调度器实例是由它生成的。
永远不要在代码中直接使用JobStore实例。有的人因为某些原因想要这样做。JobStore是给Quartz 在后台使用的。你需要(通过配置)告诉Quartz 使用哪一个JobStore,然后你只应将代码集中在调度器接口上。
9.1 RAMJobStore
RAMJobStore 是最易于使用的obStore,也是性能最好的(从CPU时间的角度)。RAMJobStore 的名字就很说明问题:它将数据保存在内存中。这就是它为何快如闪电且已易于配置。它的缺点在于如果你的应用程序结束了或崩溃了,那么所有数据都将丢失,这说明RAMJobStore与触发器和任务设置中的”non-volatility”并不匹配。在某些应用中这是可以接受的甚至是要求的行为,但对于另一些来说这可能是灾难性的。
要使用RAMJobStore (假设你使用的是StdSchedulerFactory)只需要在配置文件中将JobStore class属性值设置为org.quartz.simpl.RAMJobStore即可。配置Quartz使用RAMJobStore的方法如下:
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
9.2 JDBCJobStore
JDBCJobStore 的作用也是显而易见的--它将所有数据通过JDBC保存在数据库中。正是如此它的配置要比RAMJobStore更困难一些,也没有它快。然而性能的缺陷也不是特别的糟糕,尤其是你的数据表使用主键作为索引。在相当现代且网络环境相当好(在调度器与数据库之间)的机器上,获取并更新一个激活的触发器所需要的时间通常小于 10毫秒。
JDBCJobStore 几乎兼容所有数据区,它被广泛使用到Oracle, PostgreSQL, MySQL, MS SQLServer, HSQLDB, 以及DB2上。使用JDBCJobStore时,你必须首先为quartz创建一个表格,你在Quartz 分发包的 "docs/dbTables" 目录下能够找到创建表格的SQL脚本。如果还没有针对你的数据库的脚本,那就随便看一个,并以任何方式进行修改。有一件事情要注意,在这些脚本中所有表格都是以QRTZ开头(比如"QRTZ_TRIGGERS"和 "QRTZ_JOB_DETAIL")。这个前缀可以是任何值,你只需要告诉JDBCJobStore 即可(在配置文件中)。在同一个数据库中为不同的调度器实例使用不同的数据表时,使用不同的前缀是有用处的。
一旦建立了数据表,在配置和使用JDBCJobStore 之前你还需要做一个重要的决定。你需要决定你的应用使用什么类型的事务(transaction)。如果你不需要将你的调度指令(例如增加或删除触发器)绑定到其他的事务,那你可以使用JobStoreTX (这是最常用的选项)让Quartz 来管理事务。
如果你要让Quartz 与其它事务(比如与一个J2EE应用服务器)一起工作,那你应当使用JobStoreCMT ,Quartz 将使用APP服务器容器来管理这些事务。
最后一部分是设置DataSource ,JDBCJobStore 要从它这里获取到你数据库的连接。DataSources 在属性文件中定义,且有一些不同的定义方式。一种是让Quartz 自己来创建和管理DataSource ,提供所有到数据库的连接信息。还有一种是让Quartz 使用自身所在的应用服务器提供的DataSource ,向JDBC提供DataSource的JNDI 名称。具体的配置请参阅 "docs/config"文件夹中的配置文件。
要使用JDBCJobStore (假设你使用的是StdSchedulerFactory)你首先要将配置文件中的JobStore class 设置为org.quartz.impl.jdbcjobstore.JobStoreTX 或者org.quartz.impl.jdbcjobstore.JobStoreCMT二者其一,取决于你对以上选项的选择。
配置Quartz使用JobStoreTx
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
接下来你需要选择供JobStore使用的DriverDelegate。DriverDelegate 的职责是处理你的数据库所需要的JDBC相关的工作。StdJDBCDelegate 使用了"vanilla" JDBC代码(以及SQL语句)来做这些工作。如果没有为你的数据库指定其它的Delegate你可以尝试这个--我们仅为看起来使用StdJDBCDelegate 会有问题的数据库提供了特定的Delegate。其它的Delegate位于 "org.quartz.impl.jdbcjobstore"包或其子包内,包括: DB2v6Delegate (for DB2 version 6 及以前), HSQLDBDelegate (for HSQLDB),MSSQLDelegate (for Microsoft SQLServer), PostgreSQLDelegate (for PostgreSQL), WeblogicDelegate (for using JDBC drivers made by Weblogic), OracleDelegate (for using Oracle),等等。
当选择了Delegate以后,你需要在配置文件中设置它的名称。
配置JDBCJobStore 使用DriverDelegate
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
接下来你需要通知JobStore 你使用的数据表前缀是什么。
配置JDBCJobStore 使用的数据表前缀:
org.quartz.jobStore.tablePrefix = QRTZ_
最后,你需要设置JobStore使用的DataSource 。你填写的DataSource 必须在配置文件中有定义,因此我们指定Quartz 使用"myDS"(已经在配置文件的某处定义)。
配置JDBCJobStore 使用的DataSource 名称:
org.quartz.jobStore.dataSource = myDS
如果你的调度器很繁忙(比如在运行任务数量几乎总是等于线程池的容量)那么你可能需要将DataSource 的连接数设置为线程池的容量+2。
"org.quartz.jobStore.useProperties"配置参数默认为false,为了使JDBCJobStore 的JobDataMaps的所有值均为String类型,而不是存储为更复杂的可序列化对象,该值可以设置为true。长远来看这样更加安全,因为你避免了将非字符串类序列化为BLOB类是的类版本问题。
9.3 TerracottaJobStore
TerracottaJobStore 提供了一种不使用数据库情况下的量化和鲁棒性手段。这意味着你的数据库可以与Quartz的负载无关,并将其节省下来的资源用于你的其它应用。
TerracottaJobStore 可被集群化或非集群化使用,在这些场景下都会为你的任务数据提供一个存储介质,它在你的应用重启期间也具有持久性,因为数据存储在Terracotta 服务器。它的性能比使用基于JDBCJobStore 的数据库要好得多(大约一个数量级),但仍比RAMJobStore慢得多。
要使用TerracottaJobStore (假设你使用的是StdSchedulerFactory),只需要将配置文件中的JobStore class 设置为org.terracotta.quartz.TerracottaJobStore,并额外添加一行配置指定Terracotta 服务器的位置。
配置Quartz 使用TerracottaJobStore的方法:
org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore
org.quartz.jobStore.tcConfigUrl = localhost:9510
关于JobStore和Terracotta的详细信息请参阅http://www.terracotta.org/quartz。
十、配置,资源使用及调度器工厂
Quartz 的结构是模块化的,因此要让它运行起来的话各个组件之间必须很好地结合起来。在运行Quartz 之前必须先进行配置的主要组件有:
l 线程池
l JobStore
l DataSources (如果需要的话)
l 调度器自身
线程池为Quartz 提供了一个线程集以便在执行任务时使用。线程池中线程越多,可以并发执行的任务数越多。然而,过多的线程可能会是你的系统变慢。许多Quartz 用户发现5个左右的线程就足够了--因为在任意时刻的任务数都不会超过100,而且通常它们都不会要求要同时执行,而且这些任务的生命期都很短(很快就结束)。另一些用户发现他们需要10,15,50或者甚至100个线程,因为他们在任意时刻都有成千上万的触发器和大量的调度器--它们都平均至少有10-100个任务需要在任一时刻运行。为你的调度器池找到合适容量完全取决于你如何使用它。并没有严格的规律,除了保持线程数量尽可能的少(为了节约你机器的资源)--然而你要确保足以按时激活你的任务。注意,如果一个触发器达到了激活时间而没有可用线程,Quartz 将阻塞(暂停)直到有一个可用的线程,然后任务将被执行--在约定时间的数个毫秒之后。这甚至会导致线程的激活失败--如果在配置的"misfire threshold"期间都没有可用的线程。
在org.quartz.spi 包中定义了一个线程池接口,你可以根据你的洗好创建一个线程池应用。Quartz 包含一个简单(但非常令人满意的)线程池叫做org.quartz.simpl.SimpleThreadPool。这个线程池只是维护一个固定的线程集合--从不会增多也不会减少。但它非常健壮并且得到了非常好的测试--几乎所有使用Quartz 的用户都会使用它。
JobStores 和DataSources 在第9章已经讨论过。这里需要提醒的是一个事实,所有的JobStores 都实现了org.quartz.spi.JobStore接口--如果某个打包好的JobStores 不满足你的需要,你可以自己创建一个。
最后,你需要创建你自己的调度器实例。调度器自身需要一个名称,告知它的RMI参数并把JobStore 和线程池的实例传递给它。RMI 参数包含了调度器是否应该将自己创建为一个RMI的服务对象(使它对远程连接可用),使用哪个主机和端口等等。StdSchedulerFactory 也可以生产调度器实例,它实际上是到远端进程创建的调度器的代理。
10.1 StdSchedulerFactory
StdSchedulerFactory是一个org.quartz.SchedulerFactory接口的实现。它使用一系列的属性 (java.util.Properties)来创建和初始化Quartz 调度器。属性通常存储在文件中并从它加载,也可以由你的应用程序产生并直接传递到factory。直接调用factory 的getScheduler() 方法将产生调度器,初始化它(以及它的线程池、JobStore 和DataSources),然后返回它的公共接口的句柄。
在Quartz 分发包的"docs/config"目录下有一些示例配置(包含属性的描述)。你可以Quartz 文档的"Reference"章节的配置手册中寻找完整的文档。
10.2 DirectSchedulerFactory
DirectSchedulerFactory是另一个SchedulerFactory 实现。它对于那些想要以更程序化的方式来创建调度器实例的人来说是有用的。它一般不建议使用,因为:(1)它要求用户非常明白他在做什么(2)它不允许声明式的配置--换句话说,你需要对调度器的设置进行硬编码。
10.3 日志
Quartz 使用SLF4J 框架来满足所有的日志需要。为了调整日志设置(比如输出量,以及输出路径),你需要理解SLF4J 框架,这超出了本文的范围。如果你想要获取关于触发器激活和任务执行的额外信息,你可能会对如何启用org.quartz.plugins.history.LoggingJobHistoryPlugin和/或rg.quartz.plugins.history.LoggingTriggerHistoryPlugin感兴趣。
十一、高级(商业) 特性
11.1 集群
集群目前与JDBC-Jobstore (JobStoreTX 或 JobStoreCMT) 以及TerracottaJobStore一同工作。这些特性包括负载均衡和任务容错(如果JobDetail的"request recovery" 设置为true)。
使用JobStoreTX 或 JobStoreCMT的集群
通过将"org.quartz.jobStore.isClustered"属性值设置为true来启用集群。集群中的每个实例都应使用quartz.properties文件的同一份拷贝。使用唯一配置文件的例外情况包含以下可允许的例外:线程池容量不同,以及 "org.quartz.scheduler.instanceId" 属性值不同。集群中的每个节点都必须有一个唯一的实例ID,将该值设为"AUTO"即可轻易实现(不需要不同的配置文件)。
用于不要在不同的机器上使用集群,除非它们使用了某种时钟同步服务实现了时钟同步,且运行的非常规律(每个机器的时钟必须在同一秒内)。如果你还不太熟悉怎么实现可以看这里http://www.boulder.nist.gov/timefreq/service/its.htm。
如果已经有集群实例使用了一组数据表,永远不要激活一个同样使用该组数据表的非集群实例,可能发生严重的数据冲突,并且运行状态一定是不稳定的。
每个任务每次只会被一个节点激活。我的意思是,如果某任务有一个触发器,它每隔10秒钟激活一次,那么在12:00:00只有一个节点执行该任务,在12:00:10也有一个节点执行该任务,以此类推。并不需要每次都是同一个节点--具体是哪个多少有一些随机性。对于繁忙调度器(有大量触发器)的负载均衡机制是近似随机的,但对于非繁忙调度器(只有一两个触发器)每次都是相同的活跃节点。
使用TerracottaJobStore的集群
只需要将调度器配置为使用TerracottaJobStore (第9章已经介绍),你的调度器就会全部配置为集群。也许你还想考虑如何设置你的Terracotta 服务器,尤其是如何开启持久性之类选项的配置,并且为了高可用而运行一批Terracotta 服务器。商业版的TerracottaJobStore 提供了Quartz 的高级特性,允许智能地将任务定位到合适的集群节点。关于JobStore 和Terracotta 的更多信息请查看 http://www.terracotta.org/quartz。
11.2 JTA 事务
在第9章已经解释过,JobStoreCMT 允许Quartz 的调度器在JTA 事务中运行。通过将"org.quartz.scheduler.wrapJobExecutionInUserTransaction"属性设置为true,任务也可以在JTA 事务中运行(UserTransaction)。设置该值后,一个JTA 事务的begin()将在任务的execute 方法执行前被调用,并且在commit() 结束时commit() 被调用。这适用于所有任务。
如果你想为每个任务指定是否要由JTA 事务来包装它的执行,那你应当在该任务的类中使用@ExecuteInJTATransaction标注。
当使用JobStoreCMT时,除了JTA 事务自动包装任务执行之外,你在调度器接口上所进行的调用也会参与到事务中。你只需要确认在调度器上调用方法前已经启动了一个事务即可。你可以使用UserTransaction来直接完成,或通过将你使用调度器的代码放到一个使用了容器管理事务的SessionBean 中。
十二、其它特性
12.1 插件
Quartz 为插入额外功能提供了一个接口(org.quartz.spi.SchedulerPlugin)。Quartz包含的插件提供了各种工具特性,你在org.quartz.plugins包中能找到。它们提供了诸如任务自动调度、记录历史任务和触发器事件、确保在JVM存在时调度器干净的关闭等特性。
12.2 任务工厂(JobFactory)
当触发器激活时,相关的任务将被调度器配置的JobFactory 实例化。默认的JobFactory 只是简单的调用任务类的newInstance()方法。你可能想要创建你自己的JobFactory 实现以实现诸如让你应用程序的IoC 或DI容器来产生或初始化任务实例。查看org.quartz.spi.JobFactory接口,以及相关的Scheduler.setJobFactory(fact) 方法。
12.3 'Factory-Shipped' Jobs
Quartz还提供了大量工具任务,你可以使用它们来完成诸如发送邮件和激活EJB等工作。这些开箱即用的任务在org.quartz.jobs 包中可以找到。