|
Michael Lipton (mwlipton@us.ibm.com), 軟件工程師, IBM Soobaek Jang (sjang@us.ibm.com), IT 架構師/集成師, IBM
2006 年 12 月 28 日
Quartz 是個(gè)開(kāi)放源碼項目,提供了豐富的作業(yè)調度集。在這篇文章中,軟件工程師 Michael Lipton 和 IT 架構師 Soobaek Jang 對 Quartz API 進(jìn)行了介紹,從對框架的一般概述開(kāi)始,并以一系列展示 Quart 基本特性的代碼示例作為結束。在閱讀完本文并看過(guò)代碼示例后,您應當能夠把 Quartz 的基本特性應用到任何 Java? 應用程序中。
現代的 Web 應用程序框架在范圍和復雜性方面都有所發(fā)展,應用程序的每個(gè)底層組件也必須相應地發(fā)展。作業(yè)調度是現代系統中對 Java 應用程序的一般要求,而且也是對 Java 開(kāi)發(fā)人員一貫的要求。雖然目前的調度技術(shù)比起原始的數據庫觸發(fā)器標志和獨立的調度器線(xiàn)程來(lái)說(shuō),已經(jīng)發(fā)展了許多,但是作業(yè)調度仍然不是個(gè)小問(wèn)題。對這個(gè)問(wèn)題最合適的解決方案就是來(lái)自 OpenSymphony 的 Quartz API。
Quartz 是個(gè)開(kāi)源的作業(yè)調度框架,為在 Java 應用程序中進(jìn)行作業(yè)調度提供了簡(jiǎn)單卻強大的機制。Quartz 允許開(kāi)發(fā)人員根據時(shí)間間隔(或天)來(lái)調度作業(yè)。它實(shí)現了作業(yè)和觸發(fā)器的多對多關(guān)系,還能把多個(gè)作業(yè)與不同的觸發(fā)器關(guān)聯(lián)。整合了 Quartz 的應用程序可以重用來(lái)自不同事件的作業(yè),還可以為一個(gè)事件組合多個(gè)作業(yè)。雖然可以通過(guò)屬性文件(在屬性文件中可以指定 JDBC 事務(wù)的數據源、全局作業(yè)和/或觸發(fā)器偵聽(tīng)器、插件、線(xiàn)程池,以及更多)配置 Quartz,但它根本沒(méi)有與應用程序服務(wù)器的上下文或引用集成在一起。結果就是作業(yè)不能訪(fǎng)問(wèn) Web 服務(wù)器的內部函數;例如,在使用 WebSphere 應用服務(wù)器時(shí),由 Quartz 調度的作業(yè)并不能影響服務(wù)器的動(dòng)態(tài)緩存和數據源。
本文使用一系列代碼示例介紹 Quartz API,演示它的機制,例如作業(yè)、觸發(fā)器、作業(yè)倉庫和屬性。
入門(mén)
要開(kāi)始使用 Quartz,需要用 Quartz API 對項目進(jìn)行配置。步驟如下:
- 下載 Quartz API。
- 解壓縮并把 quartz-x.x.x.jar 放在項目文件夾內,或者把文件放在項目的類(lèi)路徑中。
- 把 core 和/或 optional 文件夾中的 jar 文件放在項目的文件夾或項目的類(lèi)路徑中。
- 如果使用
JDBCJobStore,把所有的 JDBC jar 文件放在項目的文件夾或項目的類(lèi)路徑中。
為了方便讀者,我已經(jīng)把所有必要的文件,包括 DB2 JDBC 文件,編譯到一個(gè) zip 文件中。請參閱 下載 小節下載代碼。
現在來(lái)看一下 Quartz API 的主要組件。
作業(yè)和觸發(fā)器
Quartz 調度包的兩個(gè)基本單元是作業(yè)和觸發(fā)器。作業(yè) 是能夠調度的可執行任務(wù),觸發(fā)器 提供了對作業(yè)的調度。雖然這兩個(gè)實(shí)體很容易合在一起,但在 Quartz 中將它們分離開(kāi)來(lái)是有原因的,而且也很有益處。
通過(guò)把要執行的工作與它的調度分開(kāi),Quartz 允許在不丟失作業(yè)本身或作業(yè)的上下文的情況下,修改調度觸發(fā)器。而且,任何單個(gè)的作業(yè)都可以有多個(gè)觸發(fā)器與其關(guān)聯(lián)。
示例 1:作業(yè)
通過(guò)實(shí)現 org.quartz.job 接口,可以使 Java 類(lèi)變成可執行的。清單 1 提供了 Quartz 作業(yè)的一個(gè)示例。這個(gè)類(lèi)用一條非常簡(jiǎn)單的輸出語(yǔ)句覆蓋了 execute(JobExecutionContext context) 方法。這個(gè)方法可以包含我們想要執行的任何代碼(所有的代碼示例都基于 Quartz 1.5.2,它是編寫(xiě)這篇文章時(shí)的穩定發(fā)行版)。
清單 1. SimpleQuartzJob.java
package com.ibm.developerworks.quartz;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class SimpleQuartzJob implements Job {
public SimpleQuartzJob() {
}
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("In SimpleQuartzJob - executing its JOB at "
+ new Date() + " by " + context.getTrigger().getName());
}
}
|
請注意,execute 方法接受一個(gè) JobExecutionContext 對象作為參數。這個(gè)對象提供了作業(yè)實(shí)例的運行時(shí)上下文。特別地,它提供了對調度器和觸發(fā)器的訪(fǎng)問(wèn),這兩者協(xié)作來(lái)啟動(dòng)作業(yè)以及作業(yè)的 JobDetail 對象的執行。Quartz 通過(guò)把作業(yè)的狀態(tài)放在 JobDetail 對象中并讓 JobDetail 構造函數啟動(dòng)一個(gè)作業(yè)的實(shí)例,分離了作業(yè)的執行和作業(yè)周?chē)臓顟B(tài)。JobDetail 對象儲存作業(yè)的偵聽(tīng)器、群組、數據映射、描述以及作業(yè)的其他屬性。
示例 2:簡(jiǎn)單觸發(fā)器
觸發(fā)器可以實(shí)現對任務(wù)執行的調度。Quartz 提供了幾種不同的觸發(fā)器,復雜程度各不相同。清單 2 中的 SimpleTrigger 展示了觸發(fā)器的基礎:
清單 2. SimpleTriggerRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
// current time
long ctime = System.currentTimeMillis();
// Initiate JobDetail with job name, job group, and executable job class
JobDetail jobDetail =
new JobDetail("jobDetail-s1", "jobDetailGroup-s1", SimpleQuartzJob.class);
// Initiate SimpleTrigger with its name and group name
SimpleTrigger simpleTrigger =
new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
// set its start up time
simpleTrigger.setStartTime(new Date(ctime));
// set the interval, how often the job should run (10 seconds here)
simpleTrigger.setRepeatInterval(10000);
// set the number of execution of this job, set to 10 times.
// It will run 10 time and exhaust.
simpleTrigger.setRepeatCount(100);
// set the ending time of this job.
// We set it for 60 seconds from its startup time here
// Even if we set its repeat count to 10,
// this will stop its process after 6 repeats as it gets it endtime by then.
//simpleTrigger.setEndTime(new Date(ctime + 60000L));
// set priority of trigger. If not set, the default is 5
//simpleTrigger.setPriority(10);
// schedule a job with JobDetail and Trigger
scheduler.scheduleJob(jobDetail, simpleTrigger);
// start the scheduler
scheduler.start();
}
|
清單 2 開(kāi)始時(shí)實(shí)例化一個(gè) SchedulerFactory,獲得此調度器。就像前面討論過(guò)的,創(chuàng )建 JobDetail 對象時(shí),它的構造函數要接受一個(gè) Job 作為參數。顧名思義,SimpleTrigger 實(shí)例相當原始。在創(chuàng )建對象之后,設置幾個(gè)基本屬性以立即調度任務(wù),然后每 10 秒重復一次,直到作業(yè)被執行 100 次。
還有其他許多方式可以操縱 SimpleTrigger。除了指定重復次數和重復間隔,還可以指定作業(yè)在特定日歷時(shí)間執行,只需給定執行的最長(cháng)時(shí)間或者優(yōu)先級(稍后討論)。執行的最長(cháng)時(shí)間可以覆蓋指定的重復次數,從而確保作業(yè)的運行不會(huì )超過(guò)最長(cháng)時(shí)間。
示例 3: Cron 觸發(fā)器
CronTrigger 支持比 SimpleTrigger 更具體的調度,而且也不是很復雜?;?cron 表達式,CronTrigger 支持類(lèi)似日歷的重復間隔,而不是單一的時(shí)間間隔 —— 這相對 SimpleTrigger 而言是一大改進(jìn)。
Cron 表達式包括以下 7 個(gè)字段:
- 秒
- 分
- 小時(shí)
- 月內日期
- 月
- 周內日期
- 年(可選字段)
特殊字符
Cron 觸發(fā)器利用一系列特殊字符,如下所示:
- 反斜線(xiàn)(/)字符表示增量值。例如,在秒字段中“5/15”代表從第 5 秒開(kāi)始,每 15 秒一次。
- 問(wèn)號(?)字符和字母 L 字符只有在月內日期和周內日期字段中可用。問(wèn)號表示這個(gè)字段不包含具體值。所以,如果指定月內日期,可以在周內日期字段中插入“?”,表示周內日期值無(wú)關(guān)緊要。字母 L 字符是 last 的縮寫(xiě)。放在月內日期字段中,表示安排在當月最后一天執行。在周內日期字段中,如果“L”單獨存在,就等于“7”,否則代表當月內周內日期的最后一個(gè)實(shí)例。所以“0L”表示安排在當月的最后一個(gè)星期日執行。
- 在月內日期字段中的字母(W)字符把執行安排在最靠近指定值的工作日。把“1W”放在月內日期字段中,表示把執行安排在當月的第一個(gè)工作日內。
- 井號(#)字符為給定月份指定具體的工作日實(shí)例。把“MON#2”放在周內日期字段中,表示把任務(wù)安排在當月的第二個(gè)星期一。
- 星號(*)字符是通配字符,表示該字段可以接受任何可能的值。
所有這些定義看起來(lái)可能有些嚇人,但是只要幾分鐘練習之后,cron 表達式就會(huì )顯得十分簡(jiǎn)單。
清單 3 顯示了 CronTrigger 的一個(gè)示例。請注意 SchedulerFactory、Scheduler 和 JobDetail 的實(shí)例化,與 SimpleTrigger 示例中的實(shí)例化是相同的。在這個(gè)示例中,只是修改了觸發(fā)器。這里指定的 cron 表達式(“0/5 * * * * ?”)安排任務(wù)每 5 秒執行一次。
清單 3. CronTriggerRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
// current time
long ctime = System.currentTimeMillis();
// Initiate JobDetail with job name, job group, and executable job class
JobDetail jobDetail =
new JobDetail("jobDetail2", "jobDetailGroup2", SimpleQuartzJob.class);
// Initiate CronTrigger with its name and group name
CronTrigger cronTrigger = new CronTrigger("cronTrigger", "triggerGroup2");
try {
// setup CronExpression
CronExpression cexp = new CronExpression("0/5 * * * * ?");
// Assign the CronExpression to CronTrigger
cronTrigger.setCronExpression(cexp);
} catch (Exception e) {
e.printStackTrace();
}
// schedule a job with JobDetail and Trigger
scheduler.scheduleJob(jobDetail, cronTrigger);
// start the scheduler
scheduler.start();
}
|
高級 Quartz
如上所示,只用作業(yè)和觸發(fā)器,就能訪(fǎng)問(wèn)大量的功能。但是,Quartz 是個(gè)豐富而靈活的調度包,對于愿意研究它的人來(lái)說(shuō),它還提供了更多功能。下一節討論 Quartz 的一些高級特性。
作業(yè)倉庫
Quartz 提供了兩種不同的方式用來(lái)把與作業(yè)和觸發(fā)器有關(guān)的數據保存在內存或數據庫中。第一種方式是 RAMJobStore 類(lèi)的實(shí)例,這是默認設置。這個(gè)作業(yè)倉庫最易使用,而且提供了最佳性能,因為所有數據都保存在內存中。這個(gè)方法的主要不足是缺乏數據的持久性。因為數據保存在 RAM 中,所以應用程序或系統崩潰時(shí),所有信息都會(huì )丟失。
為了修正這個(gè)問(wèn)題,Quartz 提供了 JDBCJobStore。顧名思義,作業(yè)倉庫通過(guò) JDBC 把所有數據放在數據庫中。數據持久性的代價(jià)就是性能降低和復雜性的提高。
設置 JDBCJobStore
在前面的示例中,已經(jīng)看到了 RAMJobStore 實(shí)例的工作情況。因為它是默認的作業(yè)倉庫,所以顯然不需要額外設置就能使用它。但是,使用 JDBCJobStore 需要一些初始化。
在應用程序中設置使用 JDBCJobStore 需要兩步:首先必須創(chuàng )建作業(yè)倉庫使用的數據庫表。 JDBCJobStore 與所有主流數據庫都兼容,而且 Quartz 提供了一系列創(chuàng )建表的 SQL 腳本,能夠簡(jiǎn)化設置過(guò)程??梢栽?Quartz 發(fā)行包的 “docs/dbTables”目錄中找到創(chuàng )建表的 SQL 腳本。第二,必須定義一些屬性,如表 1 所示:
表 1. JDBCJobStore 屬性
| 屬性名稱(chēng) |
值 |
| org.quartz.jobStore.class |
org.quartz.impl.jdbcjobstore.JobStoreTX (or JobStoreCMT) |
| org.quartz.jobStore.tablePrefix |
QRTZ_ (optional, customizable) |
| org.quartz.jobStore.driverDelegateClass |
org.quartz.impl.jdbcjobstore.StdJDBCDelegate |
| org.quartz.jobStore.dataSource |
qzDS (customizable) |
| org.quartz.dataSource.qzDS.driver |
com.ibm.db2.jcc.DB2Driver (could be any other database driver) |
| org.quartz.dataSource.qzDS.url |
jdbc:db2://localhost:50000/QZ_SMPL (customizable) |
| org.quartz.dataSource.qzDS.user |
db2inst1 (place userid for your own db) |
| org.quartz.dataSource.qzDS.password |
pass4dbadmin (place your own password for user) |
| org.quartz.dataSource.qzDS.maxConnections |
30 |
清單 4 展示了 JDBCJobStore 提供的數據持久性。就像在前面的示例中一樣,先從初始化 SchedulerFactory 和 Scheduler 開(kāi)始。然后,不再需要初始化作業(yè)和觸發(fā)器,而是要獲取觸發(fā)器群組名稱(chēng)列表,之后對于每個(gè)群組名稱(chēng),獲取觸發(fā)器名稱(chēng)列表。請注意,每個(gè)現有的作業(yè)都應當用 Scheduler.reschedule() 方法重新調度。僅僅重新初始化在先前的應用程序運行時(shí)終止的作業(yè),不會(huì )正確地裝載觸發(fā)器的屬性。
清單 4. JDBCJobStoreRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
String[] triggerGroups;
String[] triggers;
triggerGroups = scheduler.getTriggerGroupNames();
for (int i = 0; i < triggerGroups.length; i++) {
triggers = scheduler.getTriggerNames(triggerGroups[i]);
for (int j = 0; j < triggers.length; j++) {
Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);
if (tg instanceof SimpleTrigger && tg.getName().equals("simpleTrigger")) {
((SimpleTrigger)tg).setRepeatCount(100);
// reschedule the job
scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);
// unschedule the job
//scheduler.unscheduleJob(triggersInGroup[j], triggerGroups[i]);
}
}
}
// start the scheduler
scheduler.start();
}
|
運行 JDBCJobStore
在第一次運行示例時(shí),觸發(fā)器在數據庫中初始化。圖 1 顯示了數據庫在觸發(fā)器初始化之后但尚未擊發(fā)之前的情況。所以,基于 清單 4 中的 setRepeatCount() 方法,將 REPEAT_COUNT 設為 100,而 TIMES_TRIGGERED 是 0。在應用程序運行一段時(shí)間之后,應用程序停止。
圖 1. 使用 JDBCJobStore 時(shí)數據庫中的數據(運行前)
圖 2 顯示了數據庫在應用程序停止后的情況。在這個(gè)圖中,TIMES_TRIGGERED 被設為 19,表示作業(yè)運行的次數。
圖 2. 同一數據在 19 次迭代之后
當再次啟動(dòng)應用程序時(shí),REPEAT_COUNT 被更新。這在圖 3 中很明顯。在圖 3 中可以看到 REPEAT_COUNT 被更新為 81,所以新的 REPEAT_COUNT 等于前面的 REPEAT_COUNT 值減去前面的 TIMES_TRIGGERED 值。而且,在圖 3 中還看到新的 TIMES_TRIGGERED 值是 7,表示作業(yè)從應用程序重新啟動(dòng)以來(lái),又觸發(fā)了 7 次。
圖 3. 第 2 次運行 7 次迭代之后的數據
當再次停止應用程序之后,REPEAT_COUNT 值再次更新。如圖 4 所示,應用程序已經(jīng)停止,還沒(méi)有重新啟動(dòng)。同樣,REPEAT_COUNT 值更新成前一個(gè) REPEAT_COUNT 值減去前一個(gè) TIMES_TRIGGERED 值。
圖 4. 再次運行觸發(fā)器之前的初始數據
使用屬性
正如在使用 JDBCJobStore 時(shí)看到的,可以用許多屬性來(lái)調整 Quartz 的行為。應當在 quartz.properties 文件中指定這些屬性。請參閱 參考資料 獲得可以配置的屬性的列表。清單 5 顯示了用于 JDBCJobStore 示例的屬性:
清單 5. quartz.properties
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# Using RAMJobStore
## if using RAMJobStore, please be sure that you comment out the following
## - org.quartz.jobStore.tablePrefix,
## - org.quartz.jobStore.driverDelegateClass,
## - org.quartz.jobStore.dataSource
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
# Using JobStoreTX
## Be sure to run the appropriate script(under docs/dbTables) first to create tables
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# Configuring JDBCJobStore with the Table Prefix
org.quartz.jobStore.tablePrefix = QRTZ_
# Using DriverDelegate
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# Using datasource
org.quartz.jobStore.dataSource = qzDS
# Define the datasource to use
org.quartz.dataSource.qzDS.driver = com.ibm.db2.jcc.DB2Driver
org.quartz.dataSource.qzDS.URL = jdbc:db2://localhost:50000/dbname
org.quartz.dataSource.qzDS.user = dbuserid
org.quartz.dataSource.qzDS.password = password
org.quartz.dataSource.qzDS.maxConnections = 30
|
結束語(yǔ)
Quartz 作業(yè)調度框架所提供的 API 在兩方面都表現極佳:既全面強大,又易于使用。Quartz 可以用于簡(jiǎn)單的作業(yè)觸發(fā),也可以用于復雜的 JDBC 持久的作業(yè)存儲和執行。OpenSymphony 在開(kāi)放源碼世界中成功地填補了一個(gè)空白,過(guò)去繁瑣的作業(yè)調度現在對開(kāi)發(fā)人員來(lái)說(shuō)不過(guò)是小菜一碟。
下載
| 描述 |
名字 |
大小 |
下載方法 |
| 帶有依賴(lài) jar 的基于 Quartz 的示例 Java 代碼 |
j-quartz-withJars.zip |
3173KB |
HTTP |
| 不帶依賴(lài) jar 的基于 Quartz 的示例 Java 代碼 |
j-quartz-noJars.zip |
10KB |
HTTP |
參考資料
學(xué)習
獲得產(chǎn)品和技術(shù)
|