Quartz是一個開源的任務調度框架。基於定時、按期的策略來執行任務是它的核心功能,好比x年x月的每一個星期五上午8點到9點,每隔10分鐘執行1次。Quartz有3個核心要素:調度器(Scheduler)、任務(Job)、觸發器(Trigger)。Quartz徹底使用Java開發,能夠集成到各類規模的應用程序中。它可以承載成千上萬的任務調度,而且支持集羣。它支持將數據存儲到數據庫中以實現持久化,並支持絕大多數的數據庫。它將任務與觸發設計爲鬆耦合,即一個任務能夠對應多個觸發器,這樣可以輕鬆構造出極爲複雜的觸發策略。html
本文是對Quartz Job Scheduler Tutorials的全文翻譯,做爲筆者本身的學習筆記。當前日期是2016年2月20日,最新版本是2.2.x,官方在線文檔的後續更新本文再也不跟進。java
在使用調度器以前,它須要被實例化,你可使用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,到時候你就能對示例有更深的理解。網絡
Quartz API主要包含如下接口:併發
Scheduler:與調度器交互的主要API。負載均衡
Job:一個接口,實現該接口的組件將被調度器運行。框架
JobDetail:用於定義Job實例。ide
Trigger:定義了一個Job如何被調度器所運行。
JobBuilder:用於定義/構建JobDetail 實例。
TriggerBuilder:用於定義/構建Trigger實例。
Scheduler由SchedulerFactory建立,並隨着shutdown方法的調用而終止。建立後它將可被用來添加、刪除或列出Job和Trigger,或執行一些調度相關的工做,(好比暫停)。只有經過start()方法啓動後它纔會真的工做。
Quartz提供一些列的Builder類來定義領域特定語言(也被稱爲流接口)。從示例代碼中能看到,Job以及Trigger均可以經過Builder來建立。
各類」ScheduleBuilder」包含了建立各類類型調度器的方法。DateBuilder 類包含方便地建立指定時間點的日曆實例(好比表示下一個整時的時間)的各類方法。
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點。
咱們將任務和觸發器設計爲互相獨立的,這種鬆耦合有許多好處:任務能夠被建立和存儲而不依賴於觸發器,而且一個任務能夠關聯到多個觸發器。另外,即便觸發器已通過期,關聯的任務仍然能夠被從新配置,而不須要從新定義;一樣,你也能夠對觸發器進行修改或替換而不須要從新定義關聯的任務。
任務和觸發器註冊到調度器時都會有一個識別KEY。任務和觸發器均可以添加到組,所以它們的KEY名稱在同一個組內必須是惟一的,完整的KEY名由KEY名+組名組成。
到這裏你可能會對觸發器和調度器有一個大概的瞭解,在下面兩章將詳細介紹。
3、Job & JobDetail更多細節
雖然你的Job代碼知道如何執行真實的工做,可是Quartz須要被告知
那個任務所擁有的各類屬性值,這是經過JobDetail提供的。JobDetail由JobBuilder類構造。
構造器每次執行execute方法時,它都會先建立一個新的實例,這一行爲的一個影響是Job必須有一個無參數構造方法;另外一個影響是,Job類所定義的的狀態數據是無心義的,由於它們在執行時沒法被保存。
如今你可能會問,如何向Job實例提供屬性/配置?如何在執行函數中保存狀態數據?答案是JobDataMap,它是JobDetail對象的一部分。
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的數據。
你能夠只建立一個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」這個詞。
這裏介紹一些關於任務狀態和併發的額外的說明。有許多標註能夠被添加到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個實例並行執行時最終保存的數據是什麼樣的(競爭條件下)這種衝突。
持久性
若是一個任務不是持久的,當再也不有關聯的活動觸發器時它將從調度器中被刪除。換句話說,非持久任務的生命期取決於它的觸發器。
請求恢復
若是一個任務請求恢復,而且在執行中調度器遭到了硬關閉,那麼當調度器從新啓動時它將從新執行。在這種狀況下,JobExecutionContext.isRecovering()將返回true。
最後,咱們要告訴你Job.execute(..)的一些細節。在該方法中你只被容許拋出JobExecutionException這一種異常(包括運行時異常)。所以,你一般要把全部代碼包裹在try-catch塊中。你還須要花一點時間來查看JobExecutionException的文檔,以便於你可以向調度器發出各類指令來根據你的意願來處理異常。
Triggers與Job同樣的易於使用,可是它包含了大量的可定製的選項。前面提到過,有不一樣類型的Triggers可供你選擇以知足不一樣的需求。
除了用於標識身份的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日。
其它屬性在之後的章節中介紹。
有時候當你有許多觸發器(或者Quartz 線程池中的線程不多)時,Quartz 可能沒有足夠的資源來同時激活全部的觸發器。這時,你可能想要控制讓哪個觸發器先被觸發。出於這個目的,你能夠設置觸發器的priority 屬性。若是有N
個觸發器將要同時被激活,然而Quartz 只有Z個線程,那麼只有前Z個優先級最高的觸發器將被激活。若是沒有設置,優先級的默認值爲5。優先級的取值適用於全部整數,包括正數和負數。
注意:僅當觸發器將同時被激活時纔會比較優先級。設定在10:59的觸發器永遠比設定在11:00的觸發器先激活。
注意:若是檢測到一個job要求恢復,那麼恢復後的優先級不變。
觸發器的另外一個重要屬性是激活失敗指令。當一個持久的觸發器由於調度器被關閉或者線程池中沒有可用的線程而錯過了激活時間時,就會發生激活失敗(Misfire)。不一樣類型的觸發器具備不一樣的激活失敗指令。默認狀況下它們使用一個「聰明策略」指令,它具備基於觸發器類型和配置的動態行爲。當調度器啓動時,它會搜索全部激活失敗的持久觸發器,而後根據各自已配置的激活失敗指令來對它們進行更新。當你在項目中使用Quartz時,你應當熟悉相應觸發器的激活失敗指令,它們在JavaDoc中有解釋。在本教程針對各類觸發器的章節中有更詳細的介紹。
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 可以知足你這樣的需求:你但願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(..)*方法,那麼當前時間(馬上激活)將被賦值。
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每每比SimpleTrigger更有用。
有了CronTrigger,你能夠指定「每一個週五下午」或者「每一個工做日早上9:30」,甚至是「一月的每一個週一,週二和週五早上9:00--10:00之間每隔5分鐘1次」這樣的調度器。即便如此,與SimpleTrigger同樣,CronTrigger也有一個startTime和一個(可選的)endTime,指定了調度器在應該在什麼時候啓動和中止。
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個星期五。
下面是一些表達式實例及其含義。你能夠在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個觸發器,而後綁定到同一個任務上。
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();
下列實例用於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); }
實現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);
JobStore的職責是記錄全部你提供給調度器的「工做數據」:任務、觸發器、日曆等等。爲你的調度器實例選擇合適的JobStore的一個很是重要的步驟。幸運的是,一旦你理解了它們之間的不一樣,選擇起來是很是容易的。你在配置文件中聲明將要選擇哪一個JobStore,以此將它提供給SchedulerFactory,由於你的調度器實例是由它生成的。
永遠不要在代碼中直接使用JobStore實例。有的人由於某些緣由想要這樣作。JobStore是給Quartz 在後臺使用的。你須要(經過配置)告訴Quartz 使用哪個JobStore,而後你只應將代碼集中在調度器接口上。
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
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類是的類版本問題。
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 也能夠生產調度器實例,它其實是到遠端進程建立的調度器的代理。
StdSchedulerFactory是一個org.quartz.SchedulerFactory接口的實現。它使用一系列的屬性 (java.util.Properties)來建立和初始化Quartz 調度器。屬性一般存儲在文件中並從它加載,也能夠由你的應用程序產生並直接傳遞到factory。直接調用factory 的getScheduler() 方法將產生調度器,初始化它(以及它的線程池、JobStore 和DataSources),而後返回它的公共接口的句柄。
在Quartz 分發包的"docs/config"目錄下有一些示例配置(包含屬性的描述)。你能夠Quartz 文檔的"Reference"章節的配置手冊中尋找完整的文檔。
DirectSchedulerFactory是另外一個SchedulerFactory 實現。它對於那些想要以更程序化的方式來建立調度器實例的人來講是有用的。它通常不建議使用,由於:(1)它要求用戶很是明白他在作什麼(2)它不容許聲明式的配置--換句話說,你須要對調度器的設置進行硬編碼。
Quartz 使用SLF4J 框架來知足全部的日誌須要。爲了調整日誌設置(好比輸出量,以及輸出路徑),你須要理解SLF4J 框架,這超出了本文的範圍。若是你想要獲取關於觸發器激活和任務執行的額外信息,你可能會對如何啓用org.quartz.plugins.history.LoggingJobHistoryPlugin和/或rg.quartz.plugins.history.LoggingTriggerHistoryPlugin感興趣。
集羣目前與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。
在第9章已經解釋過,JobStoreCMT 容許Quartz 的調度器在JTA 事務中運行。經過將"org.quartz.scheduler.wrapJobExecutionInUserTransaction"屬性設置爲true,任務也能夠在JTA 事務中運行(UserTransaction)。設置該值後,一個JTA 事務的begin()將在任務的execute 方法執行前被調用,而且在commit() 結束時commit() 被調用。這適用於全部任務。
若是你想爲每一個任務指定是否要由JTA 事務來包裝它的執行,那你應當在該任務的類中使用@ExecuteInJTATransaction標註。
當使用JobStoreCMT時,除了JTA 事務自動包裝任務執行以外,你在調度器接口上所進行的調用也會參與到事務中。你只須要確認在調度器上調用方法前已經啓動了一個事務便可。你可使用UserTransaction來直接完成,或經過將你使用調度器的代碼放到一個使用了容器管理事務的SessionBean 中。
Quartz 爲插入額外功能提供了一個接口(org.quartz.spi.SchedulerPlugin)。Quartz包含的插件提供了各類工具特性,你在org.quartz.plugins包中能找到。它們提供了諸如任務自動調度、記錄歷史任務和觸發器事件、確保在JVM存在時調度器乾淨的關閉等特性。
當觸發器激活時,相關的任務將被調度器配置的JobFactory 實例化。默認的JobFactory 只是簡單的調用任務類的newInstance()方法。你可能想要建立你本身的JobFactory 實現以實現諸如讓你應用程序的IoC 或DI容器來產生或初始化任務實例。查看org.quartz.spi.JobFactory接口,以及相關的Scheduler.setJobFactory(fact) 方法。
Quartz還提供了大量工具任務,你可使用它們來完成諸如發送郵件和激活EJB等工做。這些開箱即用的任務在org.quartz.jobs 包中能夠找到。