概述
各種企業應用幾乎都會碰到任務調度的需求,就拿論壇來說:每隔半個小時生成精華文章的RSS文件,每天凌晨統計論壇用戶的積分排名,每隔30分鐘執行鎖定用戶解鎖任務。對于一個典型的MIS系統來說,在每月1號凌晨統計上個月各部門的業務數據生成月報表,每半個小時查詢用戶是否已經有快到期的待處理業務……
Quartz 是開源任務調度框架中的翹首,它提供了強大任務調度機制,同時保持了使用的簡單性。Quartz 允許開發人員靈活地定義觸發器的調度時間表,并可以對觸發器和任務進行關聯映射。此外,Quartz提供了調度運行環境的持久化機制,可以保存并恢復調度現場,即使系統因故障關閉,任務調度現場數據并不會丟失。此外,Quartz還提供了組件式的偵聽器、各種插件、線程池等功能。
Spring為創建Quartz的Scheduler、Trigger和JobDetail提供了便利的FactoryBean類,以便能夠在Spring 容器中享受注入的好處。此外Spring還提供了一些便利工具類直接將Spring中的Bean包裝成合法的任務。Spring進一步降低了使用Quartz的難度,能以更具Spring風格的方式使用Quartz。概括來說它提供了兩方面的支持:
2)提供創建Scheduler的BeanFactory類,方便在Spring環境下創建對應的組件對象,并結合Spring容器生命周期進行啟動和停止的動作。
創建JobDetail
可以直接使用Quartz的JobDetail在Spring中配置一個JobDetail Bean,但是JobDetail使用帶參的構造函數,對于習慣通過屬性配置的Spring用戶來說存在使用上的不便。為此Spring通過擴展JobDetail提供了一個更具Bean風格的JobDetailBean。此外,Spring提供了一個MethodInvokingJobDetailFactoryBean,通過這個FactoryBean可以將Spring容器中Bean的方法包裝成Quartz任務,這樣開發者就不必為Job創建對應的類。
JobDetailBean
JobDetailBean擴展于Quartz的JobDetail。使用該Bean聲明JobDetail時,Bean的名字即是任務的名字,如果沒有指定所屬組,即使用默認組。除了JobDetail中的屬性外,還定義了以下屬性:
● jobClass:類型為Class,實現Job接口的任務類;
● beanName:默認為Bean的id名,通過該屬性顯式指定Bean名稱,對應任務的名稱;
● jobDataAsMap:類型為Map,為任務所對應的JobDataMap提供值。之所以需要提供這個屬性,是因為除非你手工注冊一 個編輯器,你不能直接配置JobDataMap類型的值,所以Spring通過jobDataAsMap設置JobDataMap的值;
●applicationContextJobDataKey:你可以將Spring ApplicationContext的引用保存到JobDataMap中,以便在Job的代碼中訪問ApplicationContext。為了達到這個目的,你需要指定一個鍵,用以在jobDataAsMap中保存ApplicationContext,如果不設置此鍵,JobDetailBean就不將ApplicationContext放入到JobDataMap中;
●jobListenerNames:類型為String[],指定注冊在Scheduler中的JobListeners名稱,以便讓這些監聽器對本任務的事件進行監聽。
下面配置片斷使用JobDetailBean在Spring中配置一個JobDetail:
<property name="jobClass" value="com.baobaotao.quartz.MyJob" />
<property name="jobDataAsMap">①
<map>
<entry key="size" value="10" />
</map>
</property>
<property name="applicationContextJobDataKey" value="applicationContext"/>②
</bean>
MyJob
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
public class MyJob implements Job

public void execute(JobExecutionContext jctx) throws JobExecutionException

Map dataMap = jctx.getJobDetail().getJobDataMap(); ①獲取JobDetail關聯的JobDataMap
String size =(String)dataMap.get("size"); ②
ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext"); ③
System.out.println("size:"+size);
dataMap.put("size",size+"0"); ④對JobDataMap所做的更改是否被會持久,取決于任務的類型
//do sth

}
}
在②處獲取size值,在③處還可以根據鍵“applicationContext”獲取ApplicationContext,有了ApplicationContext的引用,Job就可以毫無障礙訪問Spring容器中的任何Bean了。MyJob可以在execute()方法中對JobDataMap進行更改,如④所示。如果MyJob實現Job接口,這個更改對于下一次執行是不可見的,如果MyJob實現StatefulJob接口,這種更改對下一次執行是可見的。
MethodInvokingJobDetailFactoryBean
通常情況下,任務都定義在一個業務類方法中。這時,為了滿足Quartz Job接口的規定,還需要定義一個引用業務類方法的實現類。為了避免創建這個只包含一行調用代碼的Job實現類,Spring為我們提供了MethodInvokingJobDetailFactoryBean,借由該FactoryBean,我們可以將一個Bean的某個方法封裝成滿足Quartz要求的Job。來看一個具體的例子:
<property name="targetObject" ref="myService" /> ① 引用一個Bean
<property name="targetMethod" value="doJob" /> ② 指定目標Bean的方法
<property name="concurrent" value="false" /> ③ 指定最終封裝出的任務是否有狀態
<bean id="myService" class="com.baobaotao.service.MyService"/>
jobDetail_1將MyService#doJob()封裝成一個任務,同時通過concurrent屬性指定任務的類型,默認情況下封裝為無狀態的任務,如果希望目標封裝為有狀態的任務,僅需要將concurrent設置為false就可以了。Spring通過名為concurrent的屬性指定任務的類型,能夠更直接地描述到任務執行的方式(有狀態的任務不能并發執行,無狀態的任務可并發執行)。
MyService服務類擁有一個doJob()方法,它的代碼如下所示:

public void doJob()

System.out.println("in MyService.dojob().");
}
}
doJob()方法即可以是static也可以是非static的,但不能擁有方法入參。通過MethodInvokingJobDetailFactoryBean產生的JobDetail不能被序列化,所以不能被持久化到數據庫中的,如果希望使用持久化任務,則你只能創建正規的Quartz的Job實現類了。
創建Trigger
Quartz中另一個重要的組件就是Trigger,Spring按照相似的思路分別為SimpleTrigger和CronTrigger提供了更具Bean風格的SimpleTriggerBean和CronTriggerBean擴展類,通過這兩個擴展類更容易在Spring中以Bean的方式配置Trigger。
SimpleTriggerBean
默認情況下,通過SimpleTriggerBean配置的Trigger名字即為Bean的名字,并屬于默認組Trigger組。SimpleTriggerBean在SimpleTrigger的基礎上,新增了以下屬性:
● jobDetail:對應的JobDetail;
● beanName:默認為Bean的id名,通過該屬性顯式指定Bean名稱,它對應Trigger的名稱;
● jobDataAsMap:以Map類型為Trigger關聯的JobDataMap提供值;
● startDelay:延遲多少時間開始觸發,單位為毫秒,默認為0;
● triggerListenerNames:類型為String[],指定注冊在Scheduler中的TriggerListener名稱,以便讓這些監聽器對本觸發器的事件進行監聽。
下面的實例使用SimpleTriggerBean定義了一個Trigger,該Trigger和jobDetail相關聯,延遲10秒后啟動,時間間隔為20秒,重復執行100次。此外,我們還為Trigger設置了JobDataMap數據:
<property name="jobDetail" ref="jobDetail" />
<property name="startDelay" value="1000" />
<property name="repeatInterval" value="2000" />
<property name="repeatCount" value="100" />
<property name="jobDataAsMap"> ①
<map>
<entry key="count" value="10" />
</map>
</property>
</bean>
需要特別注意的是,①處配置的JobDataMap是Trigger的JobDataMap,任務執行時必須通過以下方式獲取配置的值:

public void execute(JobExecutionContext jctx) throws JobExecutionException

Map dataMap = jctx.getTrigger().getJobDataMap(); ①獲取Trigger的JobDataMap
String count = dataMap.get("count");
dataMap.put("count","30"); ② 對JobDataMap的更改不會被持久,不影響下次的執行
…
}
}
CronTriggerBean
CronTriggerBean擴展于CronTrigger,觸發器的名字即為Bean的名字,保存在默認組中。在CronTrigger的基礎上,新增的屬性和SimpleTriggerBean大致相同,配置的方法也和SimpleTriggerBean相似,下面給出一個簡單的例子:
<property name="jobDetail" ref="jobDetail "/>
<property name="cronExpression" value="0/5 * * * * ?"/>
</bean>
創建Scheduler
Quartz的SchedulerFactory是標準的工廠類,不太適合在Spring環境下使用。此外,為了保證Scheduler能夠感知Spring容器的生命周期,完成自動啟動和關閉的操作,必須讓Scheduler和Spring容器的生命周期相關聯。以便在Spring容器啟動后,Scheduler自動開始工作,而在Spring容器關閉前,自動關閉Scheduler。為此,Spring提供SchedulerFactoryBean,這個FactoryBean大致擁有以下的功能:
2)讓Scheduler和Spring容器的生命周期建立關聯,相生相息;
3)通過屬性配置部分或全部代替Quartz自身的配置文件。
SchedulerFactoryBean配置
<property name="triggers"> ①注冊多個Trigger
<list>
<ref bean="simpleTrigger" />
</list>
</property>
<property name="schedulerContextAsMap"> ②以Map類型設置SchedulerContext數據
<map>
<entry key="timeout" value="30" />
</map>
</property>
③顯式指定Quartz的配置文件地址
<property name="configLocation" value="classpath:com/baobaotao/quartz/quartz.properties" />
</bean>
SchedulerFactoryBean的triggers屬性為Trigger[]類型,可以通過該屬性注冊多個Trigger,在①處,我們注冊了一個Trigger。Scheduler擁有一個類似于ServletContext的SchedulerContext。SchedulerFactoryBean允許你以Map的形式設置SchedulerContext的參數值,如②所示。默認情況下,Quartz在類路徑下查詢quartz.properties配置文件,你也可以通過configLocation屬性顯式指定配置文件位置,如③所示。
除了實例中所用的屬性外,SchedulerFactoryBean還擁有一些常見的屬性:
●calendars:類型為Map,通過該屬性向Scheduler注冊Calendar;
●jobDetails:類型為JobDetail[],通過該屬性向Scheduler注冊JobDetail;
●autoStartup:SchedulerFactoryBean在初始化后是否馬上啟動Scheduler,默認為true。如果設置為false,需要手工啟動Scheduler;
●startupDelay:在SchedulerFactoryBean初始化完成后,延遲多少秒啟動Scheduler,默認為0,表示馬上啟動。如果并非馬上擁有需要執行的任務,可通過startupDelay屬性讓Scheduler延遲一小段時間后啟動,以便讓Spring能夠更快初始化容器中剩余的Bean;
SchedulerFactoryBean的一個重要功能是允許你將Quartz配置文件中的信息轉移到Spring配置文件中,帶來的好處是,配置信息的集中化管理,同時我們不必熟悉多種框架的配置文件結構。回憶一個Spring集成JPA、Hibernate框架,就知道這是Spring在集成第三方框架經常采用的招數之一。SchedulerFactoryBean通過以下屬性代替框架的自身配置文件:
●dataSource:當需要使用數據庫來持久化任務調度數據時,你可以在Quartz中配置數據源,也可以直接在Spring中通過dataSource指定一個Spring管理的數據源。如果指定了該屬性,即使quartz.properties中已經定義了數據源,也會被此dataSource覆蓋;
●transactionManager:可以通過該屬性設置一個Spring事務管理器。在設置dataSource時,Spring強烈推薦你使用一個事務管理器,否則數據表鎖定可能不能正常工作;
●nonTransactionalDataSource:在全局事務的情況下,如果你不希望Scheduler執行化數據操作參與到全局事務中,則可以通過該屬性指定數據源。在Spring本地事務的情況下,使用dataSource屬性就足夠了;
●quartzProperties:類型為Properties,允許你在Spring中定義Quartz的屬性。其值將覆蓋quartz.properties配置文件中的設置,這些屬性必須是Quartz能夠識別的合法屬性,在配置時,你可以需要查看Quartz的相關文檔。下面是一個配置quartzProperties屬性的例子:
…
<property name="quartzProperties">
<props>
<prop key="org.quartz.threadPool.class"> ①Quartz屬性項1
org.quartz.simpl.SimpleThreadPool
</prop>
<prop key="org.quartz.threadPool.threadCount">10</prop> ①Quartz屬性項2
</props>
</property>
</bean>
在實際應用中,我們并不總是在程序部署的時候就可能確定需要哪些任務,往往需要在運行期根據業務數據動態產生觸發器和任務。你完全可以在運行期通過代碼調用SchedulerFactoryBean獲取Scheduler實例,進行動態的任務注冊和調度。
小結
Spring為Quartz的JobDetail和Trigger提供了更具Bean風格的支持類,這使我們能夠更地方便地在Spring中通過配置定制這些組件實例。Spring的SchedulerFactoryBean讓我們可以脫離Quartz自身的配置體系,而以更具Spring風格的方式定義Scheduler。此外,還可以享受Scheduler生命周期和Spring 容器生命周期綁定的好處。