多數(shù)讀者都較容易從一個簡明扼要的例子中明白一個東西。作為寫作者,要注意避免把一章的內(nèi)容精簡地幾乎什么都沒了;作為讀者呢,需要有耐心并且要進(jìn)一步相信其后相關(guān)的章節(jié)應(yīng)該去閱讀,盡管這個例子看起來是如此之簡單。
有了這種初衷,這一章將為你介紹如何用 Quartz 框架創(chuàng)建一個簡單的應(yīng)用程序,它展示了一個典型的應(yīng)用。這個例子將讓你領(lǐng)略到創(chuàng)建和執(zhí)行一個簡單應(yīng)用的必要步驟。通過本章的學(xué)習(xí),為你學(xué)習(xí)本書的后續(xù)章節(jié)打下了堅(jiān)實(shí)的基礎(chǔ)。
2. 調(diào)度 Quartz ScanDirectoryJob
到目前為止,我們已經(jīng)創(chuàng)建了一個 Quartz job,但還沒有決定怎么處置它--明顯地,我們需以某種方式為這個 Job 設(shè)置一個運(yùn)行時間表。時間表可以是一次性的事件,或者我們可能會安裝它在除周日之外的每個午夜執(zhí)行。你即刻將會看到,Quartz Schduler 是框架的心臟與靈魂。所有的 Job 都通過 Schduler 注冊;必要時,Scheduler 也會創(chuàng)建 Job 類的實(shí)例,并執(zhí)行實(shí)例的
execute() 方法。
Scheduler 會為每一次執(zhí)行創(chuàng)建新的 Job 實(shí)例
Scheduler 在每次執(zhí)行時都會為 Job 創(chuàng)建新的實(shí)例。這就意味著 Job 的任何實(shí)例變量在執(zhí)行結(jié)束之后便會丟失。與此相反概念則可用述語有狀態(tài)的(J2EE世界里常見語)來表達(dá),但是應(yīng)用 Quartz ,一個有狀態(tài)的 job 并不用多少開銷,而且很容易的配置。當(dāng)你創(chuàng)建一個有狀態(tài)的 job 時,有一些東西對于 Quartz 來說是獨(dú)特的。最主要的就是不會出現(xiàn)兩個有著相同狀態(tài)的 Job 實(shí)例并發(fā)執(zhí)行。這可能會影響到程序的伸縮性。這些或更多的問題將在以后的章節(jié)中詳細(xì)討論。 |
·創(chuàng)建并運(yùn)行 Quartz Scheduler
在具體談?wù)?ScanDirectoryJob 之前,讓我們大略討論一下如何實(shí)例化并運(yùn)行 Quartz Scheduler 實(shí)例。代碼 3.3 描述了創(chuàng)建和啟動一個 Quartz Scheduler 實(shí)例的必要且基本的步驟。
代碼 3.3 運(yùn)行一個簡單的 Quartz 調(diào)度器
- package org.cavaness.quartzbook.chapter3;
-
- package org.cavaness.quartzbook.chapter3;
-
- import java.util.Date;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.quartz.Scheduler;
- import org.quartz.SchedulerException;
- import org.quartz.impl.StdSchedulerFactory;
-
- public class SimpleScheduler {
- static Log logger = LogFactory.getLog(SimpleScheduler.class);
-
- public static void main(String[] args) {
- SimpleScheduler simple = new SimpleScheduler();
- simple.startScheduler();
- }
-
- public void startScheduler() {
- Scheduler scheduler = null;
-
- try {
-
- scheduler = StdSchedulerFactory.getDefaultScheduler();
-
-
- scheduler.start();
- logger.info("Scheduler started at " + new Date());
-
- } catch (SchedulerException ex) {
-
- logger.error(ex);
- }
- }
- }
運(yùn)行上面 3.3 的代碼,會有日志輸出,你會看到類似如下的輸出:
INFO [main] (SimpleScheduler.java:30) - Scheduler started at Mon Sep 05 13:06:38 EDT 2005
關(guān)閉 Quartz Info 級別的日志信息
假如你搭配著 Log4J 使用 Commons Logging 日志框架,就像本書的例子那樣,你也許需要把除本書例子外,其他的所有 Info 級別以上的日志信息關(guān)閉掉。這是因?yàn)?Quartz 中 Debug 和 Info 級別的日志信息數(shù)量上大體相當(dāng)。當(dāng)你明白了 Quartz 在做什么的時候,你真正關(guān)注的信息卻淹沒在大量的日志信息中。為了不至于這樣,你可以創(chuàng)建一個文件 log4j.properties 指定只輸出 ERROR 級別信息,但是要為本書中的例子設(shè)置定顯示 INFO 級別的信息。這里有一個 log4j.properties 的例子文件來達(dá)到這個目的:
- # Create stdout appender
- log4j.rootLogger=error, stdout
-
- # Configure the stdout appender to go to the Console
- log4j.appender.stdout=org.apache.log4j.ConsoleAppender
-
- # Configure stdout appender to use the PatternLayout
- log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
-
- # Pattern output the caller's filename and line #
- log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
-
- # Print messages of level INFO or above for examples
- log4j.logger.org.cavaness.quartzbook=INFO
文件 log4j.properties 配置了默認(rèn)向標(biāo)準(zhǔn)輸出只輸出 ERROR 級別以上的日志信息,但是任何在包 org.cavaness.quartzbook 中的 INFO 以上級別的信息也會輸出。這有賴于以上屬性文件最后一行配置。 |
代碼 3.3 展示了啟動一個 Quartz 調(diào)度器是那么的簡單。當(dāng)調(diào)度器起來之后,你可以利用它做很多事情或者獲取到它的許多信息。例如,你也許需要安排一些 Job 或者改變又安排在調(diào)度器上 Job 的執(zhí)行次數(shù)。你也許需要讓調(diào)度器處于暫停模式,接著再次啟動它以便重新執(zhí)行在其上安排的作業(yè)。當(dāng)調(diào)度器處于暫停模式時,不執(zhí)行任何作業(yè),即使是作業(yè)到了它所期待的執(zhí)行時間。代碼 3.4 展示了怎么把調(diào)度器置為暫停模式然后又繼續(xù)運(yùn)行,這樣調(diào)度器會從中止處繼續(xù)執(zhí)行。
代碼 3.4 設(shè)置調(diào)度器為暫停模式
- private void modifyScheduler(Scheduler scheduler) {
-
- try {
- if (!scheduler.isInStandbyMode()) {
-
- scheduler.standby();
- }
-
-
-
-
- scheduler.start();
-
- } catch (SchedulerException ex) {
- logger.error(ex);
- }
- }
代碼 3.4 中的片斷僅僅是一個最簡單的例子,說明了當(dāng)你執(zhí)有一個 Quartz 調(diào)度器的引用,你可以利用它做一些你有感興趣的事情。當(dāng)然了,并非說 Scheduler 只有處于暫停模式才能很好的利用它。例如,你能在調(diào)度器處于運(yùn)行狀態(tài)時,安排新的作業(yè)或者是卸下已存在的作業(yè)。我們將通過本書的一個調(diào)度器盡可能的去掌握關(guān)于 Quartz 更多的知識。
上面的例子看起來都很簡單,但千萬不要被誤導(dǎo)了。我們還沒有指定任何作業(yè)以及那些作業(yè)的執(zhí)行時間表。雖然代碼 3.3 中的代碼確實(shí)能啟動運(yùn)行,可是我們沒有指定任何作業(yè)來執(zhí)行。這就是我們下一節(jié)要討論的。
·編程式安排一個 Quartz Job
所有的要 Quartz 來執(zhí)行的作業(yè)必須通過調(diào)度器來注冊。大多情況下,這會在調(diào)度器啟動前做好。正如本章前面說過,這一操作也提供了聲明式與編程式兩種實(shí)現(xiàn)途徑的選擇。首先,我們講解如何用編程的方式;接下來在本章,我們會用聲明的方式重做這個練習(xí)。
因?yàn)槊恳粋€ Job 都必須用 Scheduler 來注冊,所以先定義一個 JobDetail,并關(guān)聯(lián)到這個 Scheduler 實(shí)例。見代碼 3.5。
代碼 3.5. 編程式安排一個 Job
- package org.cavaness.quartzbook.chapter3;
-
- import java.util.Date;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.quartz.JobDetail;
- import org.quartz.Scheduler;
- import org.quartz.SchedulerException;
- import org.quartz.Trigger;
- import org.quartz.TriggerUtils;
- import org.quartz.impl.StdSchedulerFactory;
-
- public class Listing_3_5 {
- static Log logger = LogFactory.getLog(Listing_3_5.class);
-
- public static void main(String[] args) {
- Listing_3_5 example = new Listing_3_5();
-
- try {
-
- Scheduler scheduler = example.createScheduler();
- example.scheduleJob(scheduler);
-
-
- scheduler.start();
-
- logger.info( "Scheduler started at " + new Date() )
-
- } catch (SchedulerException ex) {
- logger.error(ex);
- }
- }
-
-
-
-
- public Scheduler createScheduler() throws SchedulerException {
- return StdSchedulerFactory.getDefaultScheduler();
- }
-
-
- private void scheduleJob(Scheduler scheduler)
- throws SchedulerException {
-
-
- JobDetail jobDetail =
- new JobDetail("ScanDirectory",
- Scheduler.DEFAULT_GROUP,
- ScanDirectoryJob.class);
-
-
- jobDetail.getJobDataMap().put("SCAN_DIR",
- "c:\\quartz-book\\input");
-
-
- Trigger trigger = TriggerUtils.makeSecondlyTrigger(10);
- trigger.setName("scanTrigger");
-
- trigger.setStartTime(new Date());
-
-
- scheduler.scheduleJob(jobDetail, trigger);
- }
- }
上面程序提供了一個理解如何編程式安排一個 Job 很好的例子。代碼首先調(diào)用
createScheduler() 方法從 Scheduler 工廠獲取一個 Scheduler 的實(shí)例。得到 Scheduler 實(shí)例之后,把它傳遞給
schedulerJob() 方法,由它把 job 同 Scheduler 進(jìn)行關(guān)聯(lián)。
首先,創(chuàng)建了我們想要運(yùn)行的 job 的
JobDetail 對象。
JobDetail 構(gòu)造器的參數(shù)中包含指派給 job 的名稱,邏輯組名,和實(shí)現(xiàn)
org.quartz.Job 接口的全限類名稱。我們可以使用
JobDetail 的別的構(gòu)造器。
public JobDetail();
public JobDetail(String name, String group, Class jobClass);
public JobDetail(String name, String group, Class jobClass,
boolean volatility, boolean durability, boolean recover);
注
一個 job 在同一個 Scheduler 實(shí)例中通過名稱和組名能唯一被標(biāo)識。假如你增加兩個具體相同名稱和組名的 job,程序會拋出
ObjectAlreadyExistsException 的異常。
在本章前面有說過,
JobDetail 扮演著某一 job 定義的角色。它帶有 Job 實(shí)例的屬性,能在運(yùn)行時被所關(guān)聯(lián)的 Job 訪問到。其中在使用
JobDetail 時,的一個最重要的東西就是
JobDataMap,它被用來存放 Job 實(shí)例的狀態(tài)和參數(shù)。在代碼 3.5 中,待掃描的目錄名稱就是通過
scheduleJob() 方法存入到
JobDataMap 中的。
·理解和使用 Quartz Trigger
Job 只是一個部分而已。注意到代碼 3.5,我們沒有在
JobDetail 對象中為 job 設(shè)定執(zhí)行日期和次數(shù)。這是 Quartz trigger 該做的事。顧名思義,Trigger 的責(zé)任就是觸發(fā)一個 Job 去執(zhí)行。當(dāng)用 Scheduler 注冊一個 Job 的時候要創(chuàng)建一個 Trigger 與這個 Job 相關(guān)聯(lián)。Quartz 提供了四種類型的 Trigger,但其中兩種是最為常用的,它們就是在下面章節(jié)中要用到的
SimpleTrigger 和
CronTrigger.
SimpleTrigger 是兩個之中簡單的那個,它主要用來激發(fā)單事件的 Job,Trigger 在指定時間激發(fā),并重復(fù) n 次--兩次激發(fā)時間之間的延時為 m,然后結(jié)束作業(yè)。
CronTrigger 非常復(fù)雜且強(qiáng)大。它是基于通用的公歷,當(dāng)需要用一種較復(fù)雜的時間表去執(zhí)行一個 Job 時用到。例如,四月至九月的每個星期一、星期三、或星期五的午夜。
為更簡單的使用 Trigger,Quartz 包含了一個工具類,叫做
org.quartz.TriggerUtils.
TriggerUtils 提供了許多便捷的方法簡化了構(gòu)造和配置 trigger. 本章的例子中有用的就是
TriggerUtils 類;
SimpleTrigger 和
CronTrigger 會在后面章節(jié)中用到。
正如你從代碼3.5中看到的那樣,調(diào)用了
TriggerUtils 的方法
makeSecondlyTrigger() 來創(chuàng)建一個每10秒種激發(fā)一次的 trigger(實(shí)際是由
TriggerUtils 生成了一個
SimpleTrigger 實(shí)例,但是我們的代碼并不想知道這些)。我們同樣要給這個 trigger 實(shí)例一個名稱并告訴它何時激發(fā)相應(yīng)的 Job;在代碼3.5 中,與之關(guān)聯(lián)的 Job 會立即啟動,因?yàn)橛煞椒?
setStartTime() 設(shè)定的是當(dāng)前時間。
代碼 3.5 演示的是如何向 Scheduler 注冊單一 job。假如你有不只一個個 job (你也許就是),你將需要為每一個 job 創(chuàng)建各自的
JobDetail。每一個
JobDetail 必須通過
scheduleJob() 方法一一注冊到 Scheduler 上。
注
回到代碼 3.1 中,我們從代碼中看到要掃描的目錄名屬性是從
JobDataMap 中獲取到的。再看代碼 3.5,你能發(fā)現(xiàn)這個屬性是怎么設(shè)置的。
如果你想重用了一個 job 類,讓它產(chǎn)生多個實(shí)例運(yùn)行,那么你需要為每個實(shí)例都創(chuàng)建一個
JobDetail。例如,假如你想重用
ScanDirectoryJob 讓它檢查兩個不同的目錄,你需要創(chuàng)建并注冊兩個
JobDetail 實(shí)例。代碼 3.6 顯示了是如何做的。
代碼 3.6. 運(yùn)行 ScanDirectoryJob 的多個實(shí)例
- package org.cavaness.quartzbook.chapter3;
-
- import java.util.Date;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.quartz.JobDetail;
- import org.quartz.Scheduler;
- import org.quartz.SchedulerException;
- import org.quartz.Trigger;
- import org.quartz.TriggerUtils;
- import org.quartz.impl.StdSchedulerFactory;
-
- public class Listing_3_6 {
- static Log logger = LogFactory.getLog(Listing_3_6.class);
-
- public static void main(String[] args) {
- Listing_3_6 example = new Listing_3_6();
-
- try {
-
- Scheduler scheduler = example.createScheduler();
-
-
- scheduler.start();
-
- logger.info("Scheduler started at " + new Date());
-
-
- example.scheduleJob(scheduler, "ScanDirectory1",
- ScanDirectoryJob.class,
- "c:\\quartz-book\\input", 10);
-
-
- example.scheduleJob(scheduler, "ScanDirectory2",
- ScanDirectoryJob.class,
- "c:\\quartz-book\\input2", 15);
-
- } catch (SchedulerException ex) {
- logger.error(ex);
- }
- }
-
-
-
-
- public Scheduler createScheduler() throws SchedulerException {
- return StdSchedulerFactory.getDefaultScheduler();
- }
-
-
- private void scheduleJob(Scheduler scheduler, String jobName,
- Class jobClass, String scanDir, int scanInterval)
- throws SchedulerException {
-
-
- JobDetail jobDetail =
- new JobDetail(jobName,
- Scheduler.DEFAULT_GROUP, jobClass);
-
-
- jobDetail.getJobDataMap().put("SCAN_DIR", scanDir);
-
-
- Trigger trigger =
- TriggerUtils.makeSecondlyTrigger(scanInterval);
-
- trigger.setName(jobName + "-Trigger");
-
-
- trigger.setStartTime(new Date());
-
-
- scheduler.scheduleJob(jobDetail, trigger);
- }
- }
代碼 3.6 和代碼 3.5 非常的類似,只存在一點(diǎn)小小的區(qū)別。主要的區(qū)別是代碼 3.6 中重構(gòu)了允許多次調(diào)用
schedulerJob() 方法。在設(shè)置上比如 Job 名稱和掃描間隔名稱通過參數(shù)傳。因此從
createScheduler() 方法獲取到 Scheduler 實(shí)例后,兩個 job(同一個類) 用不同的參數(shù)就被安排到了 Scheduler 上了。(譯者注:當(dāng)用調(diào)
createScheduler() 方法得到 Scheduler 實(shí)例后,都還沒有往上注冊 Job,何來兩個 job 呢)。
在 Scheduler 啟動之前還是之后安排 Job 代碼
3.5 中,我們在安排 job 之前就調(diào)用了 Scheduler 的 start() 方法。回到代碼 3.5 中,采用了另一種方式:我們是在 job 安排了之后調(diào)用了 start() 方法。Job 和 Trigger 可在任何時候在 Scheduler 添加或刪除 (除非是調(diào)用了它的 shutdown()方法)。 |
·運(yùn)行代碼 3.6 中的程序
如果我們執(zhí)行類 Listing_3_6,會得到類似如下的輸出:
INFO [main] (Listing_3_6.java:35) - Scheduler started at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] ScanDirectory1 fired at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] - c:\quartz-book\input\order-145765.xml - Size: 0
INFO [QuartzScheduler_Worker-0] - ScanDirectory2 fired at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] - No XML files found in c:\quartz-book\input2
INFO [QuartzScheduler_Worker-1] - ScanDirectory1 fired at Mon Sep 05 15:12:25 EDT 2005
INFO [QuartzScheduler_Worker-1] - c:\quartz-book\input\order-145765.xml - Size: 0
INFO [QuartzScheduler_Worker-3] - ScanDirectory2 fired at Mon Sep 05 15:12:30 EDT 2005
INFO [QuartzScheduler_Worker-3] - No XML files found in c:\quartz-book\input2