現代的 Web 應用程序框架在范圍和復雜性方面都有所發展,應用程序的每個底層組件也必須相應地發展。作業調度是現代系統中對 Java 應用程序的一般要求,而且也是對 Java 開發人員一貫的要求。雖然目前的調度技術比起原始的數據庫觸發器標志和獨立的調度器線程來說,已經發展了許多,但是作業調度仍然不是個小問題。對這個問題最合適的解決方案就是來自 OpenSymphony 的 Quartz API。 Quartz 是個開源的作業調度框架,為在 Java 應用程序中進行作業調度提供了簡單卻強大的機制。Quartz 允許開發人員根據時間間隔(或天)來調度作業。它實現了作業和觸發器的多對多關系,還能把多個作業與不同的觸發器關聯。整合了 Quartz 的應用程序可以重用來自不同事件的作業,還可以為一個事件組合多個作業。雖然可以通過屬性文件(在屬性文件中可以指定 JDBC 事務的數據源、全局作業和/或觸發器偵聽器、插件、線程池,以及更多)配置 Quartz,但它根本沒有與應用程序服務器的上下文或引用集成在一起。結果就是作業不能訪問 Web 服務器的內部函數;例如,在使用 WebSphere 應用服務器時,由 Quartz 調度的作業并不能影響服務器的動態緩存和數據源。 |
要開始使用 Quartz,需要用 Quartz API 對項目進行配置。步驟如下:
- 下載 Quartz API。
- 解壓縮并把 quartz-x.x.x.jar 放在項目文件夾內,或者把文件放在項目的類路徑中。
- 把 core 和/或 optional 文件夾中的 jar 文件放在項目的文件夾或項目的類路徑中。
- 如果使用
JDBCJobStore
,把所有的 JDBC jar 文件放在項目的文件夾或項目的類路徑中。
為了方便讀者,我已經把所有必要的文件,包括 DB2 JDBC 文件,編譯到一個 zip 文件中。請下載代碼。
現在來看一下 Quartz API 的主要組件。
![]() ![]() |
Quartz 調度包的兩個基本單元是作業和觸發器。作業 是能夠調度的可執行任務,觸發器 提供了對作業的調度。雖然這兩個實體很容易合在一起,但在 Quartz 中將它們分離開來是有原因的,而且也很有益處。
通過把要執行的工作與它的調度分開,Quartz 允許在不丟失作業本身或作業的上下文的情況下,修改調度觸發器。而且,任何單個的作業都可以有多個觸發器與其關聯。
通過實現 org.quartz.job
接口,可以使 Java 類變成可執行的。清單 1 提供了 Quartz 作業的一個示例。這個類用一條非常簡單的輸出語句覆蓋了 execute(JobExecutionContext context)
方法。這個方法可以包含我們想要執行的任何代碼.
清單 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 方法接受一個 JobExecutionContext
對象作為參數。這個對象提供了作業實例的運行時上下文。特別地,它提供了對調度器和觸發器的訪問,這兩者協作來啟動作業以及作業的 JobDetail
對象的執行。Quartz 通過把作業的狀態放在 JobDetail
對象中并讓 JobDetail
構造函數啟動一個作業的實例,分離了作業的執行和作業周圍的狀態。JobDetail
對象儲存作業的偵聽器、群組、數據映射、描述以及作業的其他屬性。
觸發器可以實現對任務執行的調度。Quartz 提供了幾種不同的觸發器,復雜程度各不相同。清單 2 中的 SimpleTrigger
展示了觸發器的基礎:
清單 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); |
清單 2 開始時實例化一個 SchedulerFactory
,獲得此調度器。就像前面討論過的,創建 JobDetail
對象時,它的構造函數要接受一個 Job
作為參數。顧名思義,SimpleTrigger
實例相當原始。在創建對象之后,設置幾個基本屬性以立即調度任務,然后每 10 秒重復一次,直到作業被執行 100 次。
還有其他許多方式可以操縱 SimpleTrigger
。除了指定重復次數和重復間隔,還可以指定作業在特定日歷時間執行,只需給定執行的最長時間或者優先級(稍后討論)。執行的最長時間可以覆蓋指定的重復次數,從而確保作業的運行不會超過最長時間。
CronTrigger
支持比 SimpleTrigger
更具體的調度,而且也不是很復雜。基于 cron 表達式,CronTrigger
支持類似日歷的重復間隔,而不是單一的時間間隔 —— 這相對 SimpleTrigger
而言是一大改進。
Cron 表達式包括以下 7 個字段:
- 秒
- 分
- 小時
- 月內日期
- 月
- 周內日期
- 年(可選字段)
Cron 觸發器利用一系列特殊字符,如下所示:
- 反斜線(/)字符表示增量值。例如,在秒字段中“5/15”代表從第 5 秒開始,每 15 秒一次。
- 問號(?)字符和字母 L 字符只有在月內日期和周內日期字段中可用。問號表示這個字段不包含具體值。所以,如果指定月內日期,可以在周內日期字段中插入“?”,表示周內日期值無關緊要。字母 L 字符是 last 的縮寫。放在月內日期字段中,表示安排在當月最后一天執行。在周內日期字段中,如果“L”單獨存在,就等于“7”,否則代表當月內周內日期的最后一個實例。所以“0L”表示安排在當月的最后一個星期日執行。
- 在月內日期字段中的字母(W)字符把執行安排在最靠近指定值的工作日。把“1W”放在月內日期字段中,表示把執行安排在當月的第一個工作日內。
- 井號(#)字符為給定月份指定具體的工作日實例。把“MON#2”放在周內日期字段中,表示把任務安排在當月的第二個星期一。
- 星號(*)字符是通配字符,表示該字段可以接受任何可能的值。
所有這些定義看起來可能有些嚇人,但是只要幾分鐘練習之后,cron 表達式就會顯得十分簡單。
清單 3 顯示了 CronTrigger
的一個示例。請注意 SchedulerFactory
、Scheduler
和 JobDetail
的實例化,與 SimpleTrigger
示例中的實例化是相同的。在這個示例中,只是修改了觸發器。這里指定的 cron 表達式(“0/5 * * * * ?”)安排任務每 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 是個豐富而靈活的調度包,對于愿意研究它的人來說,它還提供了更多功能。下一節討論 Quartz 的一些高級特性。
Quartz 提供了兩種不同的方式用來把與作業和觸發器有關的數據保存在內存或數據庫中。第一種方式是 RAMJobStore
類的實例,這是默認設置。這個作業倉庫最易使用,而且提供了最佳性能,因為所有數據都保存在內存中。這個方法的主要不足是缺乏數據的持久性。因為數據保存在 RAM 中,所以應用程序或系統崩潰時,所有信息都會丟失。
為了修正這個問題,Quartz 提供了 JDBCJobStore
。顧名思義,作業倉庫通過 JDBC 把所有數據放在數據庫中。數據持久性的代價就是性能降低和復雜性的提高。
在前面的示例中,已經看到了 RAMJobStore
實例的工作情況。因為它是默認的作業倉庫,所以顯然不需要額外設置就能使用它。但是,使用 JDBCJobStore
需要一些初始化。
在應用程序中設置使用 JDBCJobStore
需要兩步:首先必須創建作業倉庫使用的數據庫表。 JDBCJobStore
與所有主流數據庫都兼容,而且 Quartz 提供了一系列創建表的 SQL 腳本,能夠簡化設置過程。可以在 Quartz 發行包的 “docs/dbTables”目錄中找到創建表的 SQL 腳本。第二,必須定義一些屬性,如表 1 所示:
表 1. JDBCJobStore 屬性
屬性名稱 | 值 |
---|---|
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
開始。然后,不再需要初始化作業和觸發器,而是要獲取觸發器群組名稱列表,之后對于每個群組名稱,獲取觸發器名稱列表。請注意,每個現有的作業都應當用 Scheduler.reschedule()
方法重新調度。僅僅重新初始化在先前的應用程序運行時終止的作業,不會正確地裝載觸發器的屬性。
清單 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(); } |
在第一次運行示例時,觸發器在數據庫中初始化。圖 1 顯示了數據庫在觸發器初始化之后但尚未擊發之前的情況。所以,基于 清單 4 中的 setRepeatCount()
方法,將 REPEAT_COUNT
設為 100,而 TIMES_TRIGGERED
是 0。在應用程序運行一段時間之后,應用程序停止。
圖 1. 使用 JDBCJobStore 時數據庫中的數據(運行前)

圖 2 顯示了數據庫在應用程序停止后的情況。在這個圖中,TIMES_TRIGGERED
被設為 19,表示作業運行的次數。
圖 2. 同一數據在 19 次迭代之后

當再次啟動應用程序時,REPEAT_COUNT
被更新。這在圖 3 中很明顯。在圖 3 中可以看到 REPEAT_COUNT
被更新為 81,所以新的 REPEAT_COUNT
等于前面的 REPEAT_COUNT
值減去前面的 TIMES_TRIGGERED
值。而且,在圖 3 中還看到新的 TIMES_TRIGGERED
值是 7,表示作業從應用程序重新啟動以來,又觸發了 7 次。
圖 3. 第 2 次運行 7 次迭代之后的數據

當再次停止應用程序之后,REPEAT_COUNT
值再次更新。如圖 4 所示,應用程序已經停止,還沒有重新啟動。同樣,REPEAT_COUNT
值更新成前一個 REPEAT_COUNT
值減去前一個 TIMES_TRIGGERED
值。
圖 4. 再次運行觸發器之前的初始數據

正如在使用 JDBCJobStore
時看到的,可以用許多屬性來調整 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 |
Quartz 作業調度框架所提供的 API 在兩方面都表現極佳:既全面強大,又易于使用。Quartz 可以用于簡單的作業觸發,也可以用于復雜的 JDBC 持久的作業存儲和執行。OpenSymphony 在開放源碼世界中成功地填補了一個空白,過去繁瑣的作業調度現在對開發人員來說不過是小菜一碟。
PS:一個解決思路
寫一個線程不停去掃描數據庫的任務表,然后從任務表中取得任務的執行時間,在調用quartz去定時執行任務.是常見的使用方法.(不依賴于web容器.)
說到底,還是要在后臺的服務器啟動一個進程的.