在Spring中使用Quartz進行任務調度

概述
    各種企業應用幾乎都會碰到任務調度的需求,就拿論壇來說:每隔半個小時生成精華文章的RSS文件,每天凌晨統計論壇用戶的積分排名,每隔30分鐘執行鎖定用戶解鎖任務。對於一個典型的MIS系統來說,在每月1號凌晨統計上個月各部門的業務數據生成月報表,每半個小時查詢用戶是否已經有快到期的待處理業務……,這樣的例子俯拾皆是,不勝枚舉。

    Quartz 在開源任務調度框架中的翹首,它提供了強大任務調度機制,難能可貴的是它同時保持了使用的簡單性。Quartz 允許開發人員靈活地定義觸發器的調度時間表,並可以對觸發器和任務進行關聯映射。此外,Quartz提供了調度運行環境的持久化機制,可以保存並恢復調度現場,即使系統因故障關閉,任務調度現場數據並不會丟失。此外,Quartz還提供了組件式的偵聽器、各種插件、線程池等功能。

    Spring爲創建Quartz的Scheduler、Trigger和JobDetail提供了便利的FactoryBean類,以便能夠在Spring 容器中享受注入的好處。此外Spring還提供了一些便利工具類直接將Spring中的Bean包裝成合法的任務。Spring進一步降低了使用Quartz的難度,能以更具Spring風格的方式使用Quartz。概括來說它提供了兩方面的支持:
    1)爲Quartz的重要組件類提供更具Bean風格的擴展類;
    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:

<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
<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>

    JobDetailBean封裝了MyJob任務類,併爲Job對應JobDataMap設置了一個size的數據。此外,通過指定applicationContextJobDataKey讓Job的JobDataMap持有Spring ApplicationContext的引用。
這樣,MyJob在運行時就可以通過JobDataMap訪問到size和ApplicationContext了。來看一下MyJob的代碼,如代碼清單 8所示:

    代碼清單 8 MyJob

package com.baobaotao.quartz;
…
import org.quartz.Job;
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。來看一個具體的例子:

<bean id="jobDetail_1"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<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的屬性指定任務的類型,能夠更直接地描述到任務執行的方式(有狀態的任務不能併發執行,無狀態的任務可併發執行),對於不熟悉Quartz內部機制的用戶來說,比起statefule,concurrent顯然更簡明達意些。
MyService服務類擁有一個doJob()方法,它的代碼如下所示:
package com.baobaotao.service; public class MyService { 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數據:

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<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,任務執行時必須通過以下方式獲取配置的值:

package com.baobaotao.quartz;
…
public class MyJob implements StatefulJob {
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相似,下面給出一個簡單的例子:

<bean id="checkImagesTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<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大致擁有以下的功能:
    1)以更具Bean風格的方式爲Scheduler提供配置信息;
    2)讓Scheduler和Spring容器的生命週期建立關聯,相生相息;
    3)通過屬性配置部分或全部代替Quartz自身的配置文件。
    來看一個SchedulerFactoryBean配置的例子:

    代碼清單 9 SchedulerFactoryBean配置

<bean id="scheduler" class="org.springframework.scheduling.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屬性的例子:

<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"><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 容器生命週期綁定的好處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章