Quartz任務調度
了解Quartz體系結構
Quartz對任務調度的領域問題進行了高度的抽象,提出了調度器、任務和觸發器這3個核心的概念,并在org.quartz通過接口和類對重要的這些核心概念進行描述:
●Job:是一個接口,只有一個方法void execute(JobExecutionContext context),開發者實現該接口定義運行任務,JobExecutionContext類提供了調度上下文的各種信息。Job運行時的信息保存在 JobDataMap實例中;
●JobDetail:Quartz在每次執行Job時,都重新創建一個Job實例,所以它 不直接接受一個Job的實例,相反它接收一個Job實現類,以便運行時通過newInstance()的反射機制實例化Job。因此需要通過一個類來描述 Job的實現類及其它相關的靜態信息,如Job名字、描述、關聯監聽器等信息,JobDetail承擔了這一角色。
通過該類的構造函數可以更具體地了解它的功用:JobDetail (java.lang.String name, java.lang.String group, java.lang.Class jobClass),該構造函數要求指定Job的實現類,以及任務在Scheduler中的組名和Job名稱;
●Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有 SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔周期執行,SimpleTrigger是最適合的選 擇;而CronTrigger則可以通過Cron表達式定義出各種復雜時間規則的調度方案:如每早晨9:00執行,周一、周三、周五下午5:00執行等;
●Calendar:org.quartz.Calendar和 java.util.Calendar不同,它是一些日歷特定時間點的集合(可以簡單地將org.quartz.Calendar看作 java.util.Calendar的集合——java.util.Calendar代表一個日歷時間點,無特殊說明后面的Calendar即指 org.quartz.Calendar)。一個Trigger可以和多個Calendar關聯,以便排除或包含某些時間點。
假設,我們安排每周星期一早上10:00執行任務,但是如果碰到法定的節日,任務則不執行, 這時就需要在Trigger觸發機制的基礎上使用Calendar進行定點排除。針對不同時間段類型,Quartz在 org.quartz.impl.calendar包下提供了若干個Calendar的實現類,如AnnualCalendar、 MonthlyCalendar、WeeklyCalendar分別針對每年、每月和每周進行定義;
●Scheduler:代表一個Quartz的獨立運行容器,Trigger和 JobDetail可以注冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查找定位容器中某一對 象的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同類型 的)。Scheduler定義了多個接口方法,允許外部通過組及名稱訪問和控制容器中Trigger和JobDetail。
Scheduler可以將Trigger綁定到某一JobDetail中,這樣當 Trigger觸發時,對應的Job就被執行。一個Job可以對應多個Trigger,但一個Trigger只能對應一個Job。可以通過 SchedulerFactory創建一個Scheduler實例。Scheduler擁有一個SchedulerContext,它類似于 ServletContext,保存著Scheduler上下文信息,Job和Trigger都可以訪問SchedulerContext內的信息。 SchedulerContext內部通過一個Map,以鍵值對的方式維護這些上下文數據,SchedulerContext為保存和獲取數據提供了多個 put()和getXxx()的方法。可以通過Scheduler# getContext()獲取對應的SchedulerContext實例;
●ThreadPool:Scheduler使用一個線程池作為任務運行的基礎設施,任務通過共享線程池中的線程提高運行效率。
Job有一個StatefulJob子接口,代表有狀態的任務,該接口是一個沒有方法的標簽 接口,其目的是讓Quartz知道任務的類型,以便采用不同的執行方案。無狀態任務在執行時擁有自己的JobDataMap拷貝,對JobDataMap 的更改不會影響下次的執行。而有狀態任務共享共享同一個JobDataMap實例,每次任務執行對JobDataMap所做的更改會保存下來,后面的執行 可以看到這個更改,也即每次執行任務后都會對后面的執行發生影響。
正因為這個原因,無狀態的Job可以并發執行,而有狀態的StatefulJob不能并發執 行,這意味著如果前次的StatefulJob還沒有執行完畢,下一次的任務將阻塞等待,直到前次任務執行完畢。有狀態任務比無狀態任務需要考慮更多的因 素,程序往往擁有更高的復雜度,因此除非必要,應該盡量使用無狀態的Job。
如果Quartz使用了數據庫持久化任務調度信息,無狀態的JobDataMap僅會在Scheduler注冊任務時保持一次,而有狀態任務對應的JobDataMap在每次執行任務后都會進行保存。
Trigger自身也可以擁有一個JobDataMap,其關聯的Job可以通過 JobExecutionContext#getTrigger().getJobDataMap()獲取Trigger中的JobDataMap。不管 是有狀態還是無狀態的任務,在任務執行期間對Trigger的JobDataMap所做的更改都不會進行持久,也即不會對下次的執行產生影響。
Quartz擁有完善的事件和監聽體系,大部分組件都擁有事件,如任務執行前事件、任務執行后事件、觸發器觸發前事件、觸發后事件、調度器開始事件、關閉事件等等,可以注冊相應的監聽器處理感興趣的事件。
圖1描述了Scheduler的內部組件結構,SchedulerContext提供Scheduler全局可見的上下文信息,每一個任務都對應一個JobDataMap,虛線表達的JobDataMap表示對應有狀態的任務:
圖1 Scheduler結構圖
一個Scheduler可以擁有多個Triger組和多個JobDetail組,注冊 Trigger和JobDetail時,如果不顯式指定所屬的組,Scheduler將放入到默認組中,默認組的組名為 Scheduler.DEFAULT_GROUP。組名和名稱組成了對象的全名,同一類型對象的全名不能相同。
Scheduler本身就是一個容器,它維護著Quartz的各種組件并實施調度的規則。 Scheduler還擁有一個線程池,線程池為任務提供執行線程——這比執行任務時簡單地創建一個新線程要擁有更高的效率,同時通過共享節約資源的占用。 通過線程池組件的支持,對于繁忙度高、壓力大的任務調度,Quartz將可以提供良好的伸縮性。
提示: Quartz完整下載包examples目錄下擁有10多個實例,它們是快速掌握Quartz應用很好的實例。
使用SimpleTrigger
SimpleTrigger擁有多個重載的構造函數,用以在不同場合下構造出對應的實例:
●SimpleTrigger(String name, String group):通過該構造函數指定Trigger所屬組和名稱;
●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所屬組和名稱外,還可以指定觸發的開發時間;
●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,還可以指定結束時間、重復執行次數、時間間隔等參數;
●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):這是最復雜的一個構造函數,在指定觸發參數的同時,還通過jobGroup和jobName,讓該Trigger和 Scheduler中的某個任務關聯起來。
通過實現 org.quartz..Job 接口,可以使 Java 類化身為可調度的任務。代碼清單1提供了 Quartz 任務的一個示例:
代碼清單1 SimpleJob:簡單的Job實現類
package com.baobaotao.basic.quartz;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class SimpleJob implements Job {
①實例Job接口方法
public void execute(JobExecutionContext jobCtx)throws JobExecutionException {
System.out.println(jobCtx.getTrigger().getName()+ " triggered. time is:" + (new Date()));
}
}
這個類用一條非常簡單的輸出語句實現了Job接口的execute(JobExecutionContext context) 方法,這個方法可以包含想要執行的任何代碼。下面,我們通過SimpleTrigger對SimpleJob進行調度:
代碼清單2 SimpleTriggerRunner:使用SimpleTrigger進行調度
package com.baobaotao.basic.quartz;
import java.util.Date;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
public class SimpleTriggerRunner {
public static void main(String args[]) {
try {
①創建一個JobDetail實例,指定SimpleJob
JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);
②通過SimpleTrigger定義調度規則:馬上啟動,每2秒運行一次,共運行100次
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");
simpleTrigger.setStartTime(new Date());
simpleTrigger.setRepeatInterval(2000);
simpleTrigger.setRepeatCount(100);
③通過SchedulerFactory獲取一個調度器實例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, simpleTrigger);④ 注冊并進行調度
scheduler.start();⑤調度啟動
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先在①處通過JobDetail封裝SimpleJob,同時指定Job在Scheduler中所屬組及名稱,這里,組名為jGroup1,而名稱為job1_1。
在②處創建一個SimpleTrigger實例,指定該Trigger在Scheduler中所屬組及名稱。接著設置調度的時間規則。
最后,需要創建Scheduler實例,并將JobDetail和Trigger實例注冊到 Scheduler中。這里,我們通過StdSchedulerFactory獲取一個Scheduler實例,并通過scheduleJob (JobDetail jobDetail, Trigger trigger)完成兩件事:
1)將JobDetail和Trigger注冊到Scheduler中;
2)將Trigger指派給JobDetail,將兩者關聯起來。
當Scheduler啟動后,Trigger將定期觸發并執行SimpleJob的execute(JobExecutionContext jobCtx)方法,然后每 10 秒重復一次,直到任務被執行 100 次后停止。
還可以通過SimpleTrigger的setStartTime (java.util.Date startTime)和setEndTime(java.util.Date endTime)指定運行的時間范圍,當運行次數和時間范圍沖突時,超過時間范圍的任務運行不被執行。如可以通過 simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 60000L))指定60秒鐘以后開始。
除了通過scheduleJob(jobDetail, simpleTrigger)建立Trigger和JobDetail的關聯,還有另外一種關聯Trigger和JobDetail的方式:
JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");
…
simpleTrigger.setJobGroup("jGroup1");①-1:指定關聯的Job組名
simpleTrigger.setJobName("job1_1");①-2:指定關聯的Job名稱
scheduler.addJob(jobDetail, true);② 注冊JobDetail
scheduler.scheduleJob(simpleTrigger);③ 注冊指定了關聯JobDetail的Trigger
在這種方式中,Trigger通過指定Job所屬組及Job名稱,然后使用Scheduler的scheduleJob(Trigger trigger)方法注冊Trigger。有兩個值得注意的地方:
通過這種方式注冊的Trigger實例必須已經指定Job組和Job名稱,否則調用注冊Trigger的方法將拋出異常;
引用的JobDetail對象必須已經存在于Scheduler中。也即,代碼中①、②和③的先后順序不能互換。
在構造Trigger實例時,可以考慮使用org.quartz.TriggerUtils 工具類,該工具類不但提供了眾多獲取特定時間的方法,還擁有眾多獲取常見Trigger的方法,如makeSecondlyTrigger(String trigName)方法將創建一個每秒執行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute)將創建一個每星期某一特定時間點執行一次的Trigger。而getEvenMinuteDate(Date date)方法將返回某一時間點一分鐘以后的時間。
使用CronTrigger
CronTrigger 能夠提供比 SimpleTrigger 更有具體實際意義的調度方案,調度規則基于 Cron 表達式,CronTrigger 支持日歷相關的重復時間間隔(比如每月第一個周一執行),而不是簡單的周期時間間隔。因此,相對于SimpleTrigger而言, CronTrigger在使用上也要復雜一些。
Cron表達式
Quartz使用類似于Linux下的Cron表達式定義時間規則,Cron表達式由6或7個由空格分隔的時間字段組成,如表1所示:
表1 Cron表達式時間字段
位置 | 時間域名 | 允許值 | 允許的特殊字符 |
1 | 秒 | 0-59 | , - * / |
2 | 分鐘 | 0-59 | , - * / |
3 | 小時 | 0-23 | , - * / |
4 | 日期 | 1-31 | , - * ? / L W C |
5 | 月份 | 1-12 | , - * / |
6 | 星期 | 1-7 | , - * ? / L C # |
7 | 年(可選) | 空值1970-2099 | , - * / |
表示式 | 說明 |
"0 0 12 * * ? " | 每天12點運行 |
"0 15 10 ? * *" | 每天10:15運行 |
"0 15 10 * * ?" | 每天10:15運行 |
"0 15 10 * * ? *" | 每天10:15運行 |
"0 15 10 * * ? 2008" | 在2008年的每天10:15運行 |
"0 * 14 * * ?" | 每天14點到15點之間每分鐘運行一次,開始于14:00,結束于14:59。 |
"0 0/5 14 * * ?" | 每天14點到15點每5分鐘運行一次,開始于14:00,結束于14:55。 |
"0 0/5 14,18 * * ?" | 每天14點到15點每5分鐘運行一次,此外每天18點到19點每5鐘也運行一次。 |
"0 0-5 14 * * ?" | 每天14:00點到14:05,每分鐘運行一次。 |
"0 10,44 14 ? 3 WED" | 3月每周三的14:10分到14:44,每分鐘運行一次。 |
"0 15 10 ? * MON-FRI" | 每周一,二,三,四,五的10:15分運行。 |
"0 15 10 15 * ?" | 每月15日10:15分運行。 |
"0 15 10 L * ?" | 每月最后一天10:15分運行。 |
"0 15 10 ? * 6L" | 每月最后一個星期五10:15分運行。 |
"0 15 10 ? * 6L 2007-2009" | 在2007,2008,2009年每個月的最后一個星期五的10:15分運行。 |
"0 15 10 ? * 6#3" | 每月第三個星期五的10:15分運行。 |
CronTrigger實例
下面,我們使用CronTrigger對SimpleJob進行調度,通過Cron表達式制定調度規則,讓它每5秒鐘運行一次:
代碼清單3 CronTriggerRunner:使用CronTrigger進行調度
package com.baobaotao.basic.quartz;
import org.quartz.CronExpression;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
public class CronTriggerRunner {
public static void main(String args[]) {
try {
JobDetail jobDetail = new JobDetail("job1_2", "jGroup1",SimpleJob.class);
①-1:創建CronTrigger,指定組及名稱
CronTrigger cronTrigger = new CronTrigger("trigger1_2", "tgroup1");
CronExpression cexp = new CronExpression("0/5 * * * * ?");①-2:定義Cron表達式
cronTrigger.setCronExpression(cexp);①-3:設置Cron表達式
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
//②
} catch (Exception e) {
e.printStackTrace();
}
}
}
運行CronTriggerRunner,每5秒鐘將觸發運行SimpleJob一次。默認 情況下Cron表達式對應當前的時區,可以通過CronTriggerRunner的setTimeZone(java.util.TimeZone timeZone)方法顯式指定時區。你還也可以通過setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定開始和結束的時間。
在代碼清單3的②處需要通過Thread.currentThread.sleep()的方 式讓主線程睡眠,以便調度器可以繼續工作執行任務調度。否則在調度器啟動后,因為主線程馬上退出,也將同時引起調度器關閉,調度器中的任務都將相應銷毀, 這將導致看不到實際的運行效果。在單元測試的時候,讓主線程睡眠經常使用的辦法。對于某些長周期任務調度的測試,你可以簡單地調整操作系統時間進行模擬。
使用Calendar
在實際任務調度中,我們不可能一成不變地按照某個周期性的調度規則運行任務,必須考慮到實現生活中日歷上特定日期,就象習慣了大男人作風的人在2月14號也會有不同表現一樣。
下面,我們安排一個任務,每小時運行一次,并將五一節和國際節排除在外,其代碼如代碼清單4所示:
代碼清單4 CalendarExample:使用Calendar
package com.baobaotao.basic.quartz;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.quartz.impl.calendar.AnnualCalendar;
import org.quartz.TriggerUtils;
…
public class CalendarExample {
public static void main(String[] args) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
①法定節日是以每年為周期的,所以使用AnnualCalendar
AnnualCalendar holidays = new AnnualCalendar();
②五一勞動節
Calendar laborDay = new GregorianCalendar();
laborDay.add(Calendar.MONTH,5);
laborDay.add(Calendar.DATE,1);
holidays.setDayExcluded(laborDay, true); ②-1:排除的日期,如果設置為false則為包含
③國慶節
Calendar nationalDay = new GregorianCalendar();
nationalDay.add(Calendar.MONTH,10);
nationalDay.add(Calendar.DATE,1);
holidays.setDayExcluded(nationalDay, true);③-1:排除該日期
scheduler.addCalendar("holidays", holidays, false, false);④向Scheduler注冊日歷
Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 4);⑤4月1號 上午10點
JobDetail job = new JobDetail("job1", "group1", SimpleJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1",
runDate,
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 60L * 1000L);
trigger.setCalendarName("holidays");⑥讓Trigger應用指定的日歷規則
scheduler.scheduleJob(job, trigger);
scheduler.start();
//實際應用中主線程不能停止,否則Scheduler得不到執行,此處從略
}
}
由于節日是每年重復的,所以使用org.quartz.Calendar的 AnnualCalendar實現類,通過②、③的代碼,指定五一和國慶兩個節日并通過AnnualCalendar#setDayExcluded (Calendar day, boolean exclude)方法添加這兩個日期。exclude為true時表示排除指定的日期,如果為false時表示包含指定的日期。
在定制好org.quartz.Calendar后,還需要通過 Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)進行注冊,如果updateTriggers為true,Scheduler中已引用Calendar的Trigger將 得到更新,如④所示。
在⑥處,我們讓一個Trigger指定使用Scheduler中代表節日的Calendar,這樣Trigger就會避開五一和國慶這兩個特殊日子了。
任務調度信息存儲
在默認情況下Quartz將任務調度的運行信息保存在內存中,這種方法提供了最佳的性能,因為內存中數據訪問最快。不足之處是缺乏數據的持久性,當程序路途停止或系統崩潰時,所有運行的信息都會丟失。
比如我們希望安排一個執行100次的任務,如果執行到50次時系統崩潰了,系統重啟時任務的執行計數器將從0開始。在大多數實際的應用中,我們往往并不需要保存任務調度的現場數據,因為很少需要規劃一個指定執行次數的任務。
對于僅執行一次的任務來說,其執行條件信息本身應該是已經持久化的業務數據(如鎖定到期解鎖任務,解鎖的時間應該是業務數據),當執行完成后,條件信息也會相應改變。當然調度現場信息不僅僅是記錄運行次數,還包括調度規則、JobDataMap中的數據等等。
如果確實需要持久化任務調度信息,Quartz允許你通過調整其屬性文件,將這些信息保存到 數據庫中。使用數據庫保存任務調度信息后,即使系統崩潰后重新啟動,任務的調度信息將得到恢復。如前面所說的例子,執行50次崩潰后重新運行,計數器將從 51開始計數。使用了數據庫保存信息的任務稱為持久化任務。
通過配置文件調整任務調度信息的保存策略
其實Quartz JAR文件的org.quartz包下就包含了一個quartz.properties屬性配置文件并提供了默認設置。如果需要調整默認配置,可以在類路 徑下建立一個新的quartz.properties,它將自動被Quartz加載并覆蓋默認的設置。
先來了解一下Quartz的默認屬性配置文件:
代碼清單5 quartz.properties:默認配置
①集群的配置,這里不使用集群
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
②配置調度器的線程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
③配置任務調度現場數據保存機制
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
Quartz的屬性配置文件主要包括三方面的信息:
1)集群信息;
2)調度器線程池;
3)任務調度現場數據的保存。
如果任務數目很大時,可以通過增大線程池的大小得到更好的性能。默認情況下,Quartz采用org.quartz.simpl.RAMJobStore保存任務的現場數據,顧名思義,信息保存在RAM內存中,我們可以通過以下設置將任務調度現場數據保存到數據庫中:
代碼清單6 quartz.properties:使用數據庫保存任務調度現場數據
…
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_①數據表前綴
org.quartz.jobStore.dataSource = qzDS②數據源名稱
③定義數據源的具體屬性
org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i
org.quartz.dataSource.qzDS.user = stamen
org.quartz.dataSource.qzDS.password = abc
org.quartz.dataSource.qzDS.maxConnections = 10
要將任務調度數據保存到數據庫中,就必須使用 org.quartz.impl.jdbcjobstore.JobStoreTX代替原來的org.quartz.simpl.RAMJobStore 并提供相應的數據庫配置信息。首先①處指定了Quartz數據庫表的前綴,在②處定義了一個數據源,在③處具體定義這個數據源的連接信息。
你必須事先在相應的數據庫中創建Quartz的數據表(共8張),在Quartz的完整發布包的docs/dbTables目錄下擁有對應不同數據庫的SQL腳本。
查詢數據庫中的運行信息
任務的現場保存對于上層的Quartz程序來說是完全透明的,我們在src目錄下編寫一個如 代碼清單6所示的quartz.properties文件后,重新運行代碼清單2或代碼清單3的程序,在數據庫表中將可以看到對應的持久化信息。當調度程 序運行過程中途停止后,任務調度的現場數據將記錄在數據表中,在系統重啟時就可以在此基礎上繼續進行任務的調度。
代碼清單7 JDBCJobStoreRunner:從數據庫中恢復任務的調度
package com.baobaotao.basic.quartz;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
public class JDBCJobStoreRunner {
public static void main(String args[]) {
try {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
①獲取調度器中所有的觸發器組
String[] triggerGroups = scheduler.getTriggerGroupNames();
②重新恢復在tgroup1組中,名為trigger1_1觸發器的運行
for (int i = 0; i < triggerGroups.length; i++) {
String[] 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.getFullName().equals("tgroup1.trigger1_1")) {②-1:根據名稱判斷
②-1:恢復運行
scheduler.rescheduleJob(triggers[j], triggerGroups[i],tg);
}
}
}
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
當代碼清單2中的SimpleTriggerRunner執行到一段時間后非正常退出,我們 就可以通過這個JDBCJobStoreRunner根據記錄在數據庫中的現場數據恢復任務的調度。Scheduler中的所有Trigger以及 JobDetail的運行信息都會保存在數據庫中,這里我們僅恢復tgroup1組中名稱為trigger1_1的觸發器,這可以通過如②-1所示的代碼 進行過濾,觸發器的采用GROUP.TRIGGER_NAME的全名格式。通過Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)即可重新調度關聯某個Trigger的任務。
下面我們來觀察一下不同時期qrtz_simple_triggers表的數據:
1.運行代碼清單2的SimpleTriggerRunner一小段時間后退出:
REPEAT_COUNT表示需要運行的總次數,而TIMES_TRIGGER表示已經運行的次數。
2.運行代碼清單7的JDBCJobStoreRunner恢復trigger1_1的觸發器,運行一段時間后退出,這時qrtz_simple_triggers中的數據如下:
首先Quartz會將原REPEAT_COUNT-TIMES_TRIGGER得到新的REPEAT_COUNT值,并記錄已經運行的次數(重新從0開始計算)。
3.重新啟動JDBCJobStoreRunner運行后,數據又將發生相應的變化:
4.繼續運行直至完成所有剩余的次數,再次查詢qrtz_simple_triggers表:
這時,該表中的記錄已經變空。
值得注意的是,如果你使用JDBC保存任務調度數據時,當你運行代碼清單2的SimpleTriggerRunner然后退出,當再次希望運行SimpleTriggerRunner時,系統將拋出JobDetail重名的異常:
Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.
因為每次調用Scheduler#scheduleJob()時,Quartz都會將JobDetail和Trigger的信息保存到數據庫中,如果數據表中已經同名的JobDetail或Trigger,異常就產生了。
本文使用quartz 1.6版本,我們發現當后臺數據庫使用MySql時,數據保存不成功,該錯誤是Quartz的一個Bug,相信會在高版本中得到修復。因為HSQLDB不 支持SELECT * FROM TABLE_NAME FOR UPDATE的語法,所以不能使用HSQLDB數據庫。
小結
Quartz提供了最為豐富的任務調度功能,不但可以制定周期性運行的任務調度方案,還可以 讓你按照日歷相關的方式進行任務調度。Quartz框架的重要組件包括Job、JobDetail、Trigger、Scheduler以及輔助性的 JobDataMap和SchedulerContext。
Quartz擁有一個線程池,通過線程池為任務提供執行線程,你可以通過配置文件對線程池進 行參數定制。Quartz的另一個重要功能是可將任務調度信息持久化到數據庫中,以便系統重啟時能夠恢復已經安排的任務。此外,Quartz還擁有完善的 事件體系,允許你注冊各種事件的監聽器。
Quartz是一個開源的作業調度框架,它完全由java寫成,并設計用于J2SE和 J2EE應用中。它提供了巨大的靈活性而不犧牲簡單性。你能夠用它來為執行一個作業而創建簡單的或復雜的調度。它有很多特征,如:數據庫支持,集群,插 件,EJB作業預構建,JavaMail及其它,支持cron-like表達式等等。
本文內容
1. Quartz讓任務調度簡單
2. Quartz的發展史
3. 上手Quartz
4. Quartz內部架構
5. 作業
6. 作業管理和存儲
7. 有效作業存儲
8. 作業和觸發器
9. 調度一個作業
10. 用調度器(Scheduler)調用你的作業
11. 編程調度同聲明性調度
12. 有狀態和無狀態作業
13. Quartz框架的其他特征
14. Quartz下一步計劃
15. 了解更多Quartz特征
你曾經需要應用執行一個任務嗎?這個任務每天或每周星期二晚上11:30,或許僅僅每個月的 最后一天執行。一個自動執行而無須干預的任務在執行過程中如果發生一個嚴重錯誤,應用能夠知到其執行失敗并嘗試重新執行嗎?你和你的團隊是用java編程 嗎?如果這些問題中任何一個你回答是,那么你應該使用Quartz調度器。
旁注:Matrix目前就大量使用到了Quartz。比如,排名統計功能的實現,在Jmatrix里通過Quartz定義了一個定時調度作業,在每天凌晨一點,作業開始工作,重新統計大家的Karma和排名等。
還有,RSS文件的生成,也是通過Quartz定義作業,每隔半個小時生成一次RSS XML文件。
所以Quartz使用的地方很多,本文無疑是一篇很好的入門和進階的文章,在此,感謝David w Johnson的努力!
Quartz讓作業調度簡單
Quartz是一個完全由java編寫的開源作業調度框架。不要讓作業調度這個術語嚇著你。 盡管Quartz框架整合了許多額外功能, 但就其簡易形式看,你會發現它易用得簡直讓人受不了!。簡單地創建一個實現org.quartz.Job接口的java類。Job接口包含唯一的方法:
public void execute(JobExecutionContext context)
throws JobExecutionException;
在你的Job接口實現類里面,添加一些邏輯到execute()方法。一旦你配置好Job實 現類并設定好調度時間表,Quartz將密切注意剩余時間。當調度程序確定該是通知你的作業的時候,Quartz框架將調用你Job實現類(作業類)上的 execute()方法并允許做它該做的事情。無需報告任何東西給調度器或調用任何特定的東西。僅僅執行任務和結束任務即可。如果配置你的作業在隨后再次 被調用,Quartz框架將在恰當的時間再次調用它。
如果你使用了其它流行的開源框架象struts,你會對Quartz的設計和部件感到舒適。 雖然兩個開源工程是解決完全不同的問題,還是有很多相似的之處,就是開源軟件用戶每天感覺很舒適。Quartz能用在單機J2SE應用中,作為一個RMI 服務器,也可以用在web應用中,甚至也可以用在J2EE應用服務器中。
Quartz的發展史
盡管Quartz今年開始受到人們注意,但還是暫時流行。Quartz由James House創建并最初于2001年春天被加入sourceforge工程。接下來的幾年里,有許多新特征和版本出現,但是直到項目遷移到新的站點并成為 OpenSymphony項目家族的一員,才開始真正啟動并受到應有的關注。
James House仍然和幾個協助他的業余開發者參與大量開發工作。Quartz開發團隊今年能發布幾個新版本,包括當前正處在候選發布階段的1.5版。
上手Quartz
Quartz工程駐留在OpenSymphony站點上。在Quartz站點上可以找到許多有用的資源:JavaDocs,包含指南的文檔,CVS訪問,用戶和開發者論壇的連接,當然也有下載。
從下載連接取得Quartz的發布版本,并且解壓到到本地目錄。這個下載文件包含了一個預先 構建好的Quartz二進制文件(quartz.jar),你可以將它放進自己的應用中。Quartz框架只需要少數的第三方庫,并且這些三方庫是必需 的,你很可能已經在使用這些庫了。
你要把Quartz的安裝目錄的<quartz- install>/lib/core 和 <quartz-install>/lib/optional目錄中的第三方庫加進你自己的工程中。大多數第三方庫是我們所熟知和喜歡的標準 Jakarta Commons庫,像Commons Logging, Commons BeantUtils等等。
quartz.properties文件
Quartz有一個叫做quartz.properties的配置文件,它允許你修改框架運 行時環境。缺省是使用Quartz.jar里面的quartz.properties文件。當然,你應該創建一個quartz.properties文件 的副本并且把它放入你工程的classes目錄中以便類裝載器找到它。quartz.properties樣本文件如例1所示。
例1.quartz.properties文件允許修改Quartz運行環境:
#===============================================================
# Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
#===============================================================
# Configure ThreadPool
#===============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
#===============================================================
# Configure JobStore
#===============================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
一旦將Quartz.jar文件和第三方庫加到自己的工程里面并且quartz.properties文件在工程的classes目錄中,就可以創建作業了。然而,在做這之前,我們暫且回避一下先簡短討論一下Quartz架構。
Quartz內部架構
在規模方面,Quartz跟大多數開源框架類似。大約有300個java類和接口,并被組織 到12個包中。這可以和Apache Struts把大約325個類和接口以及組織到11個包中相比。盡管規模幾乎不會用來作為衡量框架質量的一個特性,但這里的關鍵是quarts內含很多功 能,這些功能和特性集是否成為、或者應該成為評判一個開源或非開源框架質量的因素。
Quartz調度器
Quartz框架的核心是調度器。調度器負責管理Quartz應用運行時環境。調度器不是靠 自己做所有的工作,而是依賴框架內一些非常重要的部件。Quartz不僅僅是線程和線程管理。為確保可伸縮性,Quartz采用了基于多線程的架構。啟動 時,框架初始化一套worker線程,這套線程被調度器用來執行預定的作業。這就是Quartz怎樣能并發運行多個作業的原理。Quartz依賴一套松耦 合的線程池管理部件來管理線程環境。本片文障中,我們會多次提到線程池管理,但Quartz里面的每個對象是可配置的或者是可定制的。所以,例如,如果你 想要插進自己線程池管理設施,我猜你一定能!
作業
用Quartz的行話講,作業是一個執行任務的簡單java類。任務可以是任何java代 碼。只需你實現org.quartz.Job接口并且在出現嚴重錯誤情況下拋出JobExecutionException異常即可。Job接口包含唯一 的一個方法execute(),作業從這里開始執行。一旦實現了Job接口和execute()方法,當Quartz確定該是作業運行的時候,它將調用你 的作業。Execute()方法內就完全是你要做的事情。下面有一些你要在作業里面做事情的例子:
· 用JavaMail(或者用其他的像Commons Net一樣的郵件框架)發送郵件
· 創建遠程接口并且調用在EJB上的方法
· 獲取Hibernate Session,查詢和更新關系數據庫里的數據
· 使用OSWorkflow并且從作業調用一個工作流
· 使用FTP和到處移動文件
· 調用Ant構建腳本開始預定構建
這種可能性是無窮的,正事這種無限可能性使得框架功能如此強大。Quartz給你提供了一個機制來建立具有不同粒度的、可重復的調度表,于是,你只需創建一個java類,這個類被調用而執行任務。
作業管理和存儲
作業一旦被調度,調度器需要記住并且跟蹤作業和它們的執行次數。如果你的作業是30分鐘后或 每30秒調用,這不是很有用。事實上,作業執行需要非常準確和即時調用在被調度作業上的execute()方法。Quartz通過一個稱之為作業存儲 (JobStore)的概念來做作業存儲和管理。
有效作業存儲
Quartz提供兩種基本作業存儲類型。第一種類型叫做RAMJobStore,它利用通常 的內存來持久化調度程序信息。這種作業存儲類型最容易配置、構造和運行。對許多應用來說,這種作業存儲已經足夠了。然而,因為調度程序信息是存儲在被分配 給JVM的內存里面,所以,當應用程序停止運行時,所有調度信息將被丟失。如果你需要在重新啟動之間持久化調度信息,則將需要第二種類型的作業存儲。
第二種類型的作業存儲實際上提供兩種不同的實現,但兩種實現一般都稱為JDBC作業存儲。兩 種JDBC作業存儲都需要JDBC驅動程序和后臺數據庫來持久化調度程序信息。這兩種類型的不同在于你是否想要控制數據庫事務或這釋放控制給應用服務器例 如BEA's WebLogic或Jboss。(這類似于J2EE領域中,Bean管理的事務和和容器管理事務之間的區別)
這兩種JDBC作業存儲是:
· JobStoreTX:當你想要控制事務或工作在非應用服務器環境中是使用
· JobStoreCMT:當你工作在應用服務器環境中和想要容器控制事務時使用。
JDBC作業存儲為需要調度程序維護調度信息的用戶而設計。
作業和觸發器
Quartz設計者做了一個設計選擇來從調度分離開作業。Quartz中的觸發器用來告訴調 度程序作業什么時候觸發。框架提供了一把觸發器類型,但兩個最常用的是SimpleTrigger和CronTrigger。SimpleTrigger 為需要簡單打火調度而設計。典型地,如果你需要在給定的時間和重復次數或者兩次打火之間等待的秒數打火一個作業,那么SimpleTrigger適合你。 另一方面,如果你有許多復雜的作業調度,那么或許需要CronTrigger。
CronTrigger是基于Calendar-like調度的。當你需要在除星期六和星期天外的每天上午10點半執行作業時,那么應該使用CronTrigger。正如它的名字所暗示的那樣,CronTrigger是基于Unix克隆表達式的。
作為一個例子,下面的Quartz克隆表達式將在星期一到星期五的每天上午10點15分執行一個作業。
0 15 10 ? * MON-FRI
下面的表達式
0 15 10 ? * 6L 2002-2005
將在2002年到2005年的每個月的最后一個星期五上午10點15分執行作業。
你不可能用SimpleTrigger來做這些事情。你可以用兩者之中的任何一個,但哪個跟合適則取決于你的調度需要。
調度一個作業
讓我們通過看一個例子來進入實際討論。現假定你管理一個部門,無論何時候客戶在它的FTP服 務器上存儲一個文件,都得用電子郵件通知它。我們的作業將用FTP登陸到遠程服務器并下載所有找到的文件。然后,它將發送一封含有找到和下載的文件數量的 電子郵件。這個作業很容易就幫助人們整天從手工執行這個任務中解脫出來,甚至連晚上都無須考慮。我們可以設置作業循環不斷地每60秒檢查一次,而且工作在 7×24模式下。這就是Quartz框架完全的用途。
首先創建一個Job類,將執行FTP和Email邏輯。下例展示了Quartz的Job類,它實現了org.quartz.Job接口。
例2.從FTP站點下載文件和發送email的Quartz作業
public class ScanFTPSiteJob implements Job {
private static Log logger = LogFactory.getLog(ScanFTPSiteJob.class);
/*
* Called the scheduler framework at the right time
*/
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDataMap();
try {
// Check the ftp site for files
File[] files = JobUtil.checkForFiles(jobDataMap);
JobUtil.sendEmail(jobDataMap, files);
} catch (Exception ex) {
throw new JobExecutionException(ex.getMessage());
}
}
}
我們故意讓ScanFTPSiteJob保持很簡單。我們為這個例子創建了一個叫做 JobUtil的實用類。它不是Quartz的組成部分,但對構建各種作業能重用的實用程序庫來說是有意義的。我們可以輕易將那種代碼組織進作業類中, quarts 調度器一樣好用,因為我們一直在使用quarts,所以那些代碼可繼續重用。
JobUtil.checkForFiles() and JobUtil.sendEmail()方法使用的參數是Quartz創建的JobDataMap的實例。實例為每個作業的執行而創建,它是向作業類傳遞配置參數的方法。
這里并沒有展示JobUtil的實現,但我們能用Jakarta上的Commons Net輕易地實現FTP和Email功能。
用調度器調用作業
首先創建一個作業,但為使作業能被調度器調用,你得向調度程序說明你的作業的調用時間和頻率。這個事情由與作業相關的觸發器來完成。因為我們僅僅對大約每60秒循環調用作業感興趣,所以打算使用SimpleTrigger。
作業和觸發器通過Quartz調度器接口而被調度。我們需要從調度器工廠類取得一個調度器的實例。最容易的辦法是調用StdSchedulerFactory這個類上的靜態方法getDefaultScheduler()。
使用Quartz框架,你需要調用start()方法來啟動調度器。例3的代碼遵循了大多數Quartz應用的一般模式:創建一個或多個作業,創建和設置觸發器,用調度器調度作業和觸發器,啟動調度器。
例3.Quartz作業通過Quartz調度器而被調度
public class MyQuartzServer {
public static void main(String[] args) {
MyQuartzServer server = new MyQuartzServer();
try {
server.startScheduler();
} catch (SchedulerException ex) {
ex.printStackTrace();
}
}
protected void startScheduler() throws SchedulerException {
// Use the factory to create a Scheduler instance
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// JobDetail holds the definition for Jobs
JobDetail jobDetail =
new JobDetail("ScanFTPJob", Scheduler.DEFAULT_GROUP,
ScanFTPSiteJob.class);
// Store job parameters to be used within execute()
jobDetail.getJobDataMap().put(
"FTP_HOST",
"""home""cavaness""inbound");
// Other neccessary Job parameters here
// Create a Trigger that fires every 60 seconds
Trigger trigger = TriggerUtils.makeSecondlyTrigger(60);
// Setup the Job and Trigger with the Scheduler
scheduler.scheduleJob(jobDetail, trigger );
// Start the Scheduler running
scheduler.start();
}
}
編程調度同聲明性調度
例3中,我們通過編程的方法調度我們的ScanFTPSiteJob作業。就是說,我們用java代碼來設置作業和觸發器。Quartz框架也支持在xml文件里面申明性的設置作業調度。申明性方法允許我們更快速地修改哪個作業什么時候被執行。
Quartz框架有一個插件,這個插件負責讀取xml配置文件。xml配置文件包含了關于啟 動Quartz應用的作業和觸發器信息。所有xml文件中的作業連同相關的觸發器都被加進調度器。你仍然需要編寫作業類,但配置那些作業類的調度器則非常 動態化。例4展示了一個用申明性方式執行與例3代碼相同的邏輯的xml配置文件。
例4.能使用xml文件調度的作業
ScanFTPSiteJob
DEFAULT
A job that scans an ftp site for files
ScanFTPSiteJob
FTP_HOST
"home"cavaness"inbound
ScanFTPSiteJobTrigger
DEFAULT
ScanFTPSiteJob
DEFAULT
2005-09-11 6:10:00 PM
-1
60000
你可以將xml文件中的元素跟例3代碼作個比較,它們從概念上來看是相同的。使用例4式的申明性方法的好處是維護變得極其簡單,只需改變xml配置文件和重新啟動Quartz應用即可。無須修改代碼,無須重新編譯,無須重新部署。
有狀態和無狀態作業
在本文中你所看到的作業到是無狀態的。這意味著在兩次作業執行之間,不會去維護作業執行時JobDataMap的狀態改變。如果你需要能增、刪,改JobDataMap的值,而且能讓作業在下次執行時能看到這個狀態改變,則需要用Quartz有狀態作業。
如果你是一個有經驗的EJB開發者的話,深信你會立即退縮,因為有狀態帶有負面含義。這主要 是由于EJB帶來的伸縮性問題。Quartz有狀態作業實現了org.quartz.StatefulJob接口。無狀態和有狀態作業的關鍵不同是有狀態 作業在每次執行時只有一個實例。大多數情況下,有狀態的作業不回帶來大的問題。然而,如果你有一個需要頻繁執行的作業或者需要很長時間才能完成的作業,那 么有狀態作業可能給你帶來伸縮性問題。
Quartz框架的其他特征
Quartz框架有一個豐富的特征集。事實上,quarts有太多特性以致不能在一種情況中全部領會,下面列出了一些有意思的特征,但沒時間在此詳細討論。
監聽器和插件
每個人都喜歡監聽和插件。今天,幾乎下載任何開源框架,你必定會發現支持這兩個概念。監聽是 你創建的java類,當關鍵事件發生時會收到框架的回調。例如,當一個作業被調度、沒有調度或觸發器終止和不再打火時,這些都可以通過設置來來通知你的監 聽器。Quartz框架包含了調度器監聽、作業和觸發器監聽。你可以配置作業和觸發器監聽為全局監聽或者是特定于作業和觸發器的監聽。
一旦你的一個具體監聽被調用,你就能使用這個技術來做一些你想要在監聽類里面做的事情。例 如,你如果想要在每次作業完成時發送一個電子郵件,你可以將這個邏輯寫進作業里面,也可以JobListener里面。寫進JobListener的方式 強制使用松耦合有利于設計上做到更好。
Quartz插件是一個新的功能特性,無須修改Quartz源碼便可被創建和添加進 Quartz框架。他為想要擴展Quartz框架又沒有時間提交改變給Quartz開發團隊和等待新版本的開發人員而設計。如果你熟悉Struts插件的 話,那么完全可以理解Quartz插件的使用。
與其Quartz提供一個不能滿足你需要的有限擴展點,還不如通過使用插件來擁有可修整的擴展點。
集群Quartz應用
Quartz應用能被集群,是水平集群還是垂直集群取決于你自己的需要。集群提供以下好處:
· 伸縮性
· 搞可用性
· 負載均衡
目前,Quartz只能借助關系數據庫和JDBC作業存儲支持集群。將來的版本這個制約將消失并且用RAMJobStore集群將是可能的而且將不需要數據庫的支持。
Quartz web應用
使用框架幾個星期或幾個月后,Quartz用戶所顯示的需求之一是需要集成Quartz到圖 形用戶界面中。目前Quartz框架已經有一些工具允許你使用Java servlet來初始化和啟動Quartz。一旦你可以訪問調度器實例,你就可以把它存儲在web容器的servlet上下文中 (ServletContext中)并且可以通過調度器接口管理調度環境。
幸運的是一些開發者已正影響著單機Quartz web應用,它用來更好地管理調度器環境。構建在若干個流行開源框架如Struts和Spring之上的圖形用戶界面支持很多功能,這些功能都被包裝進一個簡單接口。GUI的一個畫面如圖1所示:
圖1.Quartz Web應用允許比較容易地管理Quartz環境。
Quartz的下一步計劃
Quartz是一個活動中的工程。Quartz開發團隊明確表示不會停留在已有的榮譽上。Quartz下一個主要版本已經在啟動中。你可以在OpenSymphony的 wiki上體驗一下Quartz 2.0的設計和特征。
總之,Quartz用戶每天都自由地添加特性建議和設計創意以便能被核心框架考慮(看重)。
了解更多Quartz特征
當你開始使用Quartz框架的更多特性時,User and Developer Forum論壇變成一個回答問題和跟其他Quartz用戶溝通的極其有用的資源。經常去逛逛這個論壇時很有好處的,你也可以依靠James House來共享與你的需要相關的知識和意見。
這個論壇時免費的,你不必登陸便可以查找和查看歸檔文件。然而,如果你覺得這個論壇比較好而且需要向某人回復問題時,你必須得申請一個免費帳號并用該帳號登陸。