多數讀者都較容易從一個簡明扼要的例子中明白一個東西。作為寫作者,要注意避免把一章的內容精簡地幾乎什么都沒了;作為讀者呢,需要有耐心并且要進一步相信其后相關的章節應該去閱讀,盡管這個例子看起來是如此之簡單。
有了這種初衷,這一章將為你介紹如何用 Quartz 框架創建一個簡單的應用程序,它展示了一個典型的應用。這個例子將讓你領略到創建和執行一個簡單應用的必要步驟。通過本章的學習,為你學習本書的后續章節打下了堅實的基礎。
2. 調度 Quartz ScanDirectoryJob
到目前為止,我們已經創建了一個 Quartz job,但還沒有決定怎么處置它--明顯地,我們需以某種方式為這個 Job 設置一個運行時間表。時間表可以是一次性的事件,或者我們可能會安裝它在除周日之外的每個午夜執行。你即刻將會看到,Quartz Schduler 是框架的心臟與靈魂。所有的 Job 都通過 Schduler 注冊;必要時,Scheduler 也會創建 Job 類的實例,并執行實例的
execute() 方法。
Scheduler 會為每一次執行創建新的 Job 實例
Scheduler 在每次執行時都會為 Job 創建新的實例。這就意味著 Job 的任何實例變量在執行結束之后便會丟失。與此相反概念則可用述語有狀態的(J2EE世界里常見語)來表達,但是應用 Quartz ,一個有狀態的 job 并不用多少開銷,而且很容易的配置。當你創建一個有狀態的 job 時,有一些東西對于 Quartz 來說是獨特的。最主要的就是不會出現兩個有著相同狀態的 Job 實例并發執行。這可能會影響到程序的伸縮性。這些或更多的問題將在以后的章節中詳細討論。 |
·創建并運行 Quartz Scheduler
在具體談論 ScanDirectoryJob 之前,讓我們大略討論一下如何實例化并運行 Quartz Scheduler 實例。代碼 3.3 描述了創建和啟動一個 Quartz Scheduler 實例的必要且基本的步驟。
代碼 3.3 運行一個簡單的 Quartz 調度器
- 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);
- }
- }
- }
運行上面 3.3 的代碼,會有日志輸出,你會看到類似如下的輸出:
INFO [main] (SimpleScheduler.java:30) - Scheduler started at Mon Sep 05 13:06:38 EDT 2005
關閉 Quartz Info 級別的日志信息
假如你搭配著 Log4J 使用 Commons Logging 日志框架,就像本書的例子那樣,你也許需要把除本書例子外,其他的所有 Info 級別以上的日志信息關閉掉。這是因為 Quartz 中 Debug 和 Info 級別的日志信息數量上大體相當。當你明白了 Quartz 在做什么的時候,你真正關注的信息卻淹沒在大量的日志信息中。為了不至于這樣,你可以創建一個文件 log4j.properties 指定只輸出 ERROR 級別信息,但是要為本書中的例子設置定顯示 INFO 級別的信息。這里有一個 log4j.properties 的例子文件來達到這個目的:
- # 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 配置了默認向標準輸出只輸出 ERROR 級別以上的日志信息,但是任何在包 org.cavaness.quartzbook 中的 INFO 以上級別的信息也會輸出。這有賴于以上屬性文件最后一行配置。 |
代碼 3.3 展示了啟動一個 Quartz 調度器是那么的簡單。當調度器起來之后,你可以利用它做很多事情或者獲取到它的許多信息。例如,你也許需要安排一些 Job 或者改變又安排在調度器上 Job 的執行次數。你也許需要讓調度器處于暫停模式,接著再次啟動它以便重新執行在其上安排的作業。當調度器處于暫停模式時,不執行任何作業,即使是作業到了它所期待的執行時間。代碼 3.4 展示了怎么把調度器置為暫停模式然后又繼續運行,這樣調度器會從中止處繼續執行。
代碼 3.4 設置調度器為暫停模式
- private void modifyScheduler(Scheduler scheduler) {
-
- try {
- if (!scheduler.isInStandbyMode()) {
-
- scheduler.standby();
- }
-
-
-
-
- scheduler.start();
-
- } catch (SchedulerException ex) {
- logger.error(ex);
- }
- }
代碼 3.4 中的片斷僅僅是一個最簡單的例子,說明了當你執有一個 Quartz 調度器的引用,你可以利用它做一些你有感興趣的事情。當然了,并非說 Scheduler 只有處于暫停模式才能很好的利用它。例如,你能在調度器處于運行狀態時,安排新的作業或者是卸下已存在的作業。我們將通過本書的一個調度器盡可能的去掌握關于 Quartz 更多的知識。
上面的例子看起來都很簡單,但千萬不要被誤導了。我們還沒有指定任何作業以及那些作業的執行時間表。雖然代碼 3.3 中的代碼確實能啟動運行,可是我們沒有指定任何作業來執行。這就是我們下一節要討論的。
·編程式安排一個 Quartz Job
所有的要 Quartz 來執行的作業必須通過調度器來注冊。大多情況下,這會在調度器啟動前做好。正如本章前面說過,這一操作也提供了聲明式與編程式兩種實現途徑的選擇。首先,我們講解如何用編程的方式;接下來在本章,我們會用聲明的方式重做這個練習。
因為每一個 Job 都必須用 Scheduler 來注冊,所以先定義一個 JobDetail,并關聯到這個 Scheduler 實例。見代碼 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 很好的例子。代碼首先調用
createScheduler() 方法從 Scheduler 工廠獲取一個 Scheduler 的實例。得到 Scheduler 實例之后,把它傳遞給
schedulerJob() 方法,由它把 job 同 Scheduler 進行關聯。
首先,創建了我們想要運行的 job 的
JobDetail 對象。
JobDetail 構造器的參數中包含指派給 job 的名稱,邏輯組名,和實現
org.quartz.Job 接口的全限類名稱。我們可以使用
JobDetail 的別的構造器。
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 實例中通過名稱和組名能唯一被標識。假如你增加兩個具體相同名稱和組名的 job,程序會拋出
ObjectAlreadyExistsException 的異常。
在本章前面有說過,
JobDetail 扮演著某一 job 定義的角色。它帶有 Job 實例的屬性,能在運行時被所關聯的 Job 訪問到。其中在使用
JobDetail 時,的一個最重要的東西就是
JobDataMap,它被用來存放 Job 實例的狀態和參數。在代碼 3.5 中,待掃描的目錄名稱就是通過
scheduleJob() 方法存入到
JobDataMap 中的。
·理解和使用 Quartz Trigger
Job 只是一個部分而已。注意到代碼 3.5,我們沒有在
JobDetail 對象中為 job 設定執行日期和次數。這是 Quartz trigger 該做的事。顧名思義,Trigger 的責任就是觸發一個 Job 去執行。當用 Scheduler 注冊一個 Job 的時候要創建一個 Trigger 與這個 Job 相關聯。Quartz 提供了四種類型的 Trigger,但其中兩種是最為常用的,它們就是在下面章節中要用到的
SimpleTrigger 和
CronTrigger.
SimpleTrigger 是兩個之中簡單的那個,它主要用來激發單事件的 Job,Trigger 在指定時間激發,并重復 n 次--兩次激發時間之間的延時為 m,然后結束作業。
CronTrigger 非常復雜且強大。它是基于通用的公歷,當需要用一種較復雜的時間表去執行一個 Job 時用到。例如,四月至九月的每個星期一、星期三、或星期五的午夜。
為更簡單的使用 Trigger,Quartz 包含了一個工具類,叫做
org.quartz.TriggerUtils.
TriggerUtils 提供了許多便捷的方法簡化了構造和配置 trigger. 本章的例子中有用的就是
TriggerUtils 類;
SimpleTrigger 和
CronTrigger 會在后面章節中用到。
正如你從代碼3.5中看到的那樣,調用了
TriggerUtils 的方法
makeSecondlyTrigger() 來創建一個每10秒種激發一次的 trigger(實際是由
TriggerUtils 生成了一個
SimpleTrigger 實例,但是我們的代碼并不想知道這些)。我們同樣要給這個 trigger 實例一個名稱并告訴它何時激發相應的 Job;在代碼3.5 中,與之關聯的 Job 會立即啟動,因為由方法
setStartTime() 設定的是當前時間。
代碼 3.5 演示的是如何向 Scheduler 注冊單一 job。假如你有不只一個個 job (你也許就是),你將需要為每一個 job 創建各自的
JobDetail。每一個
JobDetail 必須通過
scheduleJob() 方法一一注冊到 Scheduler 上。
注
回到代碼 3.1 中,我們從代碼中看到要掃描的目錄名屬性是從
JobDataMap 中獲取到的。再看代碼 3.5,你能發現這個屬性是怎么設置的。
如果你想重用了一個 job 類,讓它產生多個實例運行,那么你需要為每個實例都創建一個
JobDetail。例如,假如你想重用
ScanDirectoryJob 讓它檢查兩個不同的目錄,你需要創建并注冊兩個
JobDetail 實例。代碼 3.6 顯示了是如何做的。
代碼 3.6. 運行 ScanDirectoryJob 的多個實例
- 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 非常的類似,只存在一點小小的區別。主要的區別是代碼 3.6 中重構了允許多次調用
schedulerJob() 方法。在設置上比如 Job 名稱和掃描間隔名稱通過參數傳。因此從
createScheduler() 方法獲取到 Scheduler 實例后,兩個 job(同一個類) 用不同的參數就被安排到了 Scheduler 上了。(譯者注:當用調
createScheduler() 方法得到 Scheduler 實例后,都還沒有往上注冊 Job,何來兩個 job 呢)。
在 Scheduler 啟動之前還是之后安排 Job 代碼
3.5 中,我們在安排 job 之前就調用了 Scheduler 的 start() 方法。回到代碼 3.5 中,采用了另一種方式:我們是在 job 安排了之后調用了 start() 方法。Job 和 Trigger 可在任何時候在 Scheduler 添加或刪除 (除非是調用了它的 shutdown()方法)。 |
·運行代碼 3.6 中的程序
如果我們執行類 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