概述
各種企業(yè)應(yīng)用幾乎都會(huì)碰到任務(wù)調(diào)度的需求,就拿論壇來(lái)說(shuō):每隔半個(gè)小時(shí)生成精華文章的RSS文件,每天凌晨統(tǒng)計(jì)論壇用戶的積分排名,每隔30分鐘執(zhí)行鎖定用戶解鎖任務(wù)。對(duì)于一個(gè)典型的MIS系統(tǒng)來(lái)說(shuō),在每月1號(hào)凌晨統(tǒng)計(jì)上個(gè)月各部門的業(yè)務(wù)數(shù)據(jù)生成月報(bào)表,每半個(gè)小時(shí)查詢用戶是否已經(jīng)有快到期的待處理業(yè)務(wù)……
Quartz 是開源任務(wù)調(diào)度框架中的翹首,它提供了強(qiáng)大任務(wù)調(diào)度機(jī)制,同時(shí)保持了使用的簡(jiǎn)單性。Quartz 允許開發(fā)人員靈活地定義觸發(fā)器的調(diào)度時(shí)間表,并可以對(duì)觸發(fā)器和任務(wù)進(jìn)行關(guān)聯(lián)映射。此外,Quartz提供了調(diào)度運(yùn)行環(huán)境的持久化機(jī)制,可以保存并恢復(fù)調(diào)度現(xiàn)場(chǎng),即使系統(tǒng)因故障關(guān)閉,任務(wù)調(diào)度現(xiàn)場(chǎng)數(shù)據(jù)并不會(huì)丟失。此外,Quartz還提供了組件式的偵聽器、各種插件、線程池等功能。
Spring為創(chuàng)建Quartz的Scheduler、Trigger和JobDetail提供了便利的FactoryBean類,以便能夠在Spring 容器中享受注入的好處。此外Spring還提供了一些便利工具類直接將Spring中的Bean包裝成合法的任務(wù)。Spring進(jìn)一步降低了使用Quartz的難度,能以更具Spring風(fēng)格的方式使用Quartz。概括來(lái)說(shuō)它提供了兩方面的支持:
2)提供創(chuàng)建Scheduler的BeanFactory類,方便在Spring環(huán)境下創(chuàng)建對(duì)應(yīng)的組件對(duì)象,并結(jié)合Spring容器生命周期進(jìn)行啟動(dòng)和停止的動(dòng)作。
創(chuàng)建JobDetail
可以直接使用Quartz的JobDetail在Spring中配置一個(gè)JobDetail Bean,但是JobDetail使用帶參的構(gòu)造函數(shù),對(duì)于習(xí)慣通過(guò)屬性配置的Spring用戶來(lái)說(shuō)存在使用上的不便。為此Spring通過(guò)擴(kuò)展JobDetail提供了一個(gè)更具Bean風(fēng)格的JobDetailBean。此外,Spring提供了一個(gè)MethodInvokingJobDetailFactoryBean,通過(guò)這個(gè)FactoryBean可以將Spring容器中Bean的方法包裝成Quartz任務(wù),這樣開發(fā)者就不必為Job創(chuàng)建對(duì)應(yīng)的類。
JobDetailBean
JobDetailBean擴(kuò)展于Quartz的JobDetail。使用該Bean聲明JobDetail時(shí),Bean的名字即是任務(wù)的名字,如果沒(méi)有指定所屬組,即使用默認(rèn)組。除了JobDetail中的屬性外,還定義了以下屬性:
● jobClass:類型為Class,實(shí)現(xiàn)Job接口的任務(wù)類;
● beanName:默認(rèn)為Bean的id名,通過(guò)該屬性顯式指定Bean名稱,對(duì)應(yīng)任務(wù)的名稱;
● jobDataAsMap:類型為Map,為任務(wù)所對(duì)應(yīng)的JobDataMap提供值。之所以需要提供這個(gè)屬性,是因?yàn)槌悄闶止ぷ?cè)一 個(gè)編輯器,你不能直接配置JobDataMap類型的值,所以Spring通過(guò)jobDataAsMap設(shè)置JobDataMap的值;
●applicationContextJobDataKey:你可以將Spring ApplicationContext的引用保存到JobDataMap中,以便在Job的代碼中訪問(wèn)ApplicationContext。為了達(dá)到這個(gè)目的,你需要指定一個(gè)鍵,用以在jobDataAsMap中保存ApplicationContext,如果不設(shè)置此鍵,JobDetailBean就不將ApplicationContext放入到JobDataMap中;
●jobListenerNames:類型為String[],指定注冊(cè)在Scheduler中的JobListeners名稱,以便讓這些監(jiān)聽器對(duì)本任務(wù)的事件進(jìn)行監(jiān)聽。
下面配置片斷使用JobDetailBean在Spring中配置一個(gè)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關(guān)聯(lián)的JobDataMap
String size =(String)dataMap.get("size"); ②
ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext"); ③
System.out.println("size:"+size);
dataMap.put("size",size+"0"); ④對(duì)JobDataMap所做的更改是否被會(huì)持久,取決于任務(wù)的類型
//do sth

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

public void doJob()

System.out.println("in MyService.dojob().");
}
}
doJob()方法即可以是static也可以是非static的,但不能擁有方法入?yún)ⅰMㄟ^(guò)MethodInvokingJobDetailFactoryBean產(chǎn)生的JobDetail不能被序列化,所以不能被持久化到數(shù)據(jù)庫(kù)中的,如果希望使用持久化任務(wù),則你只能創(chuàng)建正規(guī)的Quartz的Job實(shí)現(xiàn)類了。
創(chuàng)建Trigger
Quartz中另一個(gè)重要的組件就是Trigger,Spring按照相似的思路分別為SimpleTrigger和CronTrigger提供了更具Bean風(fēng)格的SimpleTriggerBean和CronTriggerBean擴(kuò)展類,通過(guò)這兩個(gè)擴(kuò)展類更容易在Spring中以Bean的方式配置Trigger。
SimpleTriggerBean
默認(rèn)情況下,通過(guò)SimpleTriggerBean配置的Trigger名字即為Bean的名字,并屬于默認(rèn)組Trigger組。SimpleTriggerBean在SimpleTrigger的基礎(chǔ)上,新增了以下屬性:
● jobDetail:對(duì)應(yīng)的JobDetail;
● beanName:默認(rèn)為Bean的id名,通過(guò)該屬性顯式指定Bean名稱,它對(duì)應(yīng)Trigger的名稱;
● jobDataAsMap:以Map類型為Trigger關(guān)聯(lián)的JobDataMap提供值;
● startDelay:延遲多少時(shí)間開始觸發(fā),單位為毫秒,默認(rèn)為0;
● triggerListenerNames:類型為String[],指定注冊(cè)在Scheduler中的TriggerListener名稱,以便讓這些監(jiān)聽器對(duì)本觸發(fā)器的事件進(jìn)行監(jiān)聽。
下面的實(shí)例使用SimpleTriggerBean定義了一個(gè)Trigger,該Trigger和jobDetail相關(guān)聯(lián),延遲10秒后啟動(dòng),時(shí)間間隔為20秒,重復(fù)執(zhí)行100次。此外,我們還為Trigger設(shè)置了JobDataMap數(shù)據(jù):
<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,任務(wù)執(zhí)行時(shí)必須通過(guò)以下方式獲取配置的值:

public void execute(JobExecutionContext jctx) throws JobExecutionException

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