Spring3整合Quartz2實現定時任務及動態任務調整(新增刪除暫停恢復)--推薦
1、常規整合
最近工作中需要用到定時任務的功能,雖然Spring3也自帶了一個輕量級的定時任務實現,但感覺不夠靈活,功能也不夠強大。在考慮之後,決定整合更為專業的Quartz來實現定時任務功能。
首先,當然是新增依賴的jar檔案,我的專案是maven管理的,以下的我專案的依賴:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId><version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.7.4</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons.lang.version}</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>${commons.dbcp.version}</version> </dependency> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc14</artifactId> <version>${ojdbc.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.version}</version> </dependency> </dependencies>
或許你應該看出來了,我的專案是spring整合了mybatis,目前spring的最新版本已經到了4.x系列,但是最新版的mybatis-spring的整合外掛所依賴推薦的依然是spring 3.1.3.RELEASE,所以這裡沒有用spring的最新版而是用了推薦的3.1.3.RELEASE,畢竟最新版本的功能一般情況下也用不到。至於quartz,則是用了目前的最新版2.2.1之所以在這裡特別對版本作一下說明,是因為spring和quartz的整合對版本是有要求的。
spring3.1以下的版本必須使用quartz1.x系列,3.1以上的版本才支援quartz 2.x,不然會出錯。
至於原因,則是spring對於quartz的支援實現,org.springframework.scheduling.quartz.CronTriggerBean繼承了org.quartz.CronTrigger,在quartz1.x系列中org.quartz.CronTrigger是個類,而在quartz2.x系列中org.quartz.CronTrigger變成了介面,從而造成無法用spring的方式配置quartz的觸發器(trigger)。
在Spring中使用Quartz有兩種方式實現:第一種是任務類繼承QuartzJobBean,第二種則是在配置檔案裡定義任務類和要執行的方法,類和方法可以是普通類。很顯然,第二種方式遠比第一種方式來的靈活。
這裡採用的就是第二種方式。
spring配置檔案:
<!-- 使用MethodInvokingJobDetailFactoryBean,任務類可以不實現Job介面,通過targetMethod指定呼叫方法--> <bean id="taskJob" class="com.tyyd.dw.task.DataConversionTask"/> <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="group" value="job_work"/> <property name="name" value="job_work_name"/> <!--false表示等上一個任務執行完後再開啟新的任務--> <property name="concurrent" value="false"/> <property name="targetObject"> <ref bean="taskJob"/> </property> <property name="targetMethod"> <value>run</value> </property> </bean> <!-- 排程觸發器 --> <bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="name" value="work_default_name"/> <property name="group" value="work_default"/> <property name="jobDetail"> <ref bean="jobDetail" /> </property> <property name="cronExpression"> <value>0/5 * * * * ?</value> </property> </bean> <!-- 排程工廠 --> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="myTrigger"/> </list> </property> </bean> Task類則是一個普通的Java類,沒有繼承任何類和實現任何介面(當然可以用註解方式來宣告bean): //@Component public class DataConversionTask{ /** 日誌物件 */ private static final Logger LOG = LoggerFactory.getLogger(DataConversionTask.class); public void run() { if (LOG.isInfoEnabled()) { LOG.info("資料轉換任務執行緒開始執行"); } } }
至此,簡單的整合大功告成,run方法將每隔5秒執行一次,因為配置了concurrent等於false,所以假如run方法的執行時間超過5秒,在執行完之前即使時間已經超過了5秒下一個定時計劃執行任務仍不會被開啟,如果是true,則不管是否執行完,時間到了都將開啟。
這裡,順便貼一下cronExpression表示式備忘:
欄位 允許值 允許的特殊字元
秒 0-59 , – * /
分 0-59 , – * /
小時 0-23 , – * /
日期 1-31 , – * ? / L W C
月份 1-12 或者 JAN-DEC , – * /
星期 1-7 或者 SUN-SAT , – * ? / L C #
年(可選) 留空, 1970-2099 , – * /
表示式意義
"0 0 12 * * ?" 每天中午12點觸發
"0 15 10 ? * *" 每天上午10:15觸發
"0 15 10 * * ?" 每天上午10:15觸發
"0 15 10 * * ? *" 每天上午10:15觸發
"0 15 10 * * ? 2005" 2005年的每天上午10:15觸發
"0 * 14 * * ?" 在每天下午2點到下午2:59期間的每1分鐘觸發
"0 0/5 14 * * ?" 在每天下午2點到下午2:55期間的每5分鐘觸發
"0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
"0 0-5 14 * * ?" 在每天下午2點到下午2:05期間的每1分鐘觸發
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2: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 2002-2005" 2002年至2005年的每月的最後一個星期五上午10:15觸發
"0 15 10 ? * 6#3" 每月的第三個星期五上午10:15觸發
每天早上6點
0 6 * * *
每兩個小時
0 */2 * * *
晚上11點到早上8點之間每兩個小時,早上八點
0 23-7/2,8 * * *
每個月的4號和每個禮拜的禮拜一到禮拜三的早上11點
0 11 4 * 1-3
1月1日早上4點
0 4 1 1 *
前面,我們已經對Spring 3和Quartz 2用配置檔案的方式進行了整合,如果需求比較簡單的話應該已經可以滿足了。但是很多時候,我們常常會遇到需要動態的新增或修改任務,而spring中所提供的定時任務元件卻只能夠通過修改xml中trigger的配置才能控制定時任務的時間以及任務的啟用或停止,這在帶給我們方便的同時也失去了動態配置任務的靈活性。我搜索了一些網上的解決方法,都沒有很好的解決這個問題,而且大多數提到的解決方案都停留在Quartz 1.x系列版本上,所用到的程式碼和API已經不能適用於新版本的Spring和Quartz。沒辦法只能靠自己了,花了點時間好好研究了一下Spring和Quartz中相關的程式碼。
首先,我們來回顧一下spring中使用quartz的配置程式碼:
<!-- 使用MethodInvokingJobDetailFactoryBean,任務類可以不實現Job介面,通過targetMethod指定呼叫方法--> <bean id="taskJob" class="com.tyyd.dw.task.DataConversionTask"/> <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="group" value="job_work"/> <property name="name" value="job_work_name"/> <!--false表示等上一個任務執行完後再開啟新的任務--> <property name="concurrent" value="false"/> <property name="targetObject"> <ref bean="taskJob"/> </property> <property name="targetMethod"> <value>execute</value> </property> </bean> <!-- 排程觸發器 --> <bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="name" value="work_default_name"/> <property name="group" value="work_default"/> <property name="jobDetail"> <ref bean="jobDetail" /> </property> <property name="cronExpression"> <value>0/5 * * * * ?</value> </property> </bean> <!-- 排程工廠 --> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="myTrigger"/> </list> </property> </bean>
所有的配置都在xml中完成,包括cronExpression表示式,十分的方便。但是如果我的任務資訊是儲存在資料庫的,想要動態的初始化,而且任務較多的時候不是得有一大堆的xml配置?或者說我要修改一下trigger的表示式,使原來5秒執行一次的任務變成10秒執行一次,這時問題就來了,試過在配置檔案中不傳入cronExpression等引數,但是啟動時就報錯了,難道我每次都修改xml檔案然後重啟應用嗎,這顯然不合適的。最理想的是在與spring整合的同時又能實現動態任務的新增、刪除及修改配置。
我們來看一下spring實現quartz的方式,先看一下上面配置檔案中定義的jobDetail。其實上面生成的jobDetail並不是我們定義的Bean,因為在Quartz 2.x版本中JobDetail已經是一個介面(當然以前的版本也並非直接生成JobDetail):
- public interface JobDetail extends Serializable, Cloneable {...}
Spring是通過將其轉換為MethodInvokingJob或StatefulMethodInvokingJob型別來實現的,這兩個都是靜態的內部類,MethodInvokingJob類繼承於QuartzJobBean,而StatefulMethodInvokingJob則直接繼承於MethodInvokingJob。 這兩個類的實現區別在於有狀態和無狀態,對應於quartz的Job和StatefulJob,具體可以檢視quartz文件,這裡不再贅述。先來看一下它們實現的QuartzJobBean的主要程式碼:
/** * This implementation applies the passed-in job data map as bean property * values, and delegates to <code>executeInternal</code> afterwards. * @see #executeInternal */ public final void execute(JobExecutionContext context) throws JobExecutionException { try { // Reflectively adapting to differences between Quartz 1.x and Quartz 2.0... Scheduler scheduler = (Scheduler) ReflectionUtils.invokeMethod(getSchedulerMethod, context); Map mergedJobDataMap = (Map) ReflectionUtils.invokeMethod(getMergedJobDataMapMethod, context); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.addPropertyValues(scheduler.getContext()); pvs.addPropertyValues(mergedJobDataMap); bw.setPropertyValues(pvs, true); } catch (SchedulerException ex) { throw new JobExecutionException(ex); } executeInternal(context); } /** * Execute the actual job. The job data map will already have been * applied as bean property values by execute. The contract is * exactly the same as for the standard Quartz execute method. * @see #execute */ protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException; //還有MethodInvokingJobDetailFactoryBean中的程式碼: public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { prepare(); // Use specific name if given, else fall back to bean name. String name = (this.name != null ? this.name : this.beanName); // Consider the concurrent flag to choose between stateful and stateless job. Class jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class); // Build JobDetail instance. if (jobDetailImplClass != null) { // Using Quartz 2.0 JobDetailImpl class... this.jobDetail = (JobDetail) BeanUtils.instantiate(jobDetailImplClass); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this.jobDetail); bw.setPropertyValue("name", name); bw.setPropertyValue("group", this.group); bw.setPropertyValue("jobClass", jobClass); bw.setPropertyValue("durability", true); ((JobDataMap) bw.getPropertyValue("jobDataMap")).put("methodInvoker", this); } else { // Using Quartz 1.x JobDetail class... this.jobDetail = new JobDetail(name, this.group, jobClass); this.jobDetail.setVolatility(true); this.jobDetail.setDurability(true); this.jobDetail.getJobDataMap().put("methodInvoker", this); } // Register job listener names. if (this.jobListenerNames != null) { for (String jobListenerName : this.jobListenerNames) { if (jobDetailImplClass != null) { throw new IllegalStateException("Non-global JobListeners not supported on Quartz 2 - " + "manually register a Matcher against the Quartz ListenerManager instead"); } this.jobDetail.addJobListener(jobListenerName); } } postProcessJobDetail(this.jobDetail); }
上面主要看我們目前用的Quartz 2.0版本的實現部分,到這裡或許你已經明白Spring對Quartz的封裝原理了。Spring就是通過這種方式在最後Job真正執行時反呼叫到我們所注入的類和方法。
現在,理解了Spring的實現原理後,我們就可以來設計我們自己的了。在設計時我想到以下幾點:
1、減少spring的配置檔案,為了實現一個定時任務,spring的配置程式碼太多了。
2、使用者可以通過頁面等方式新增、啟用、禁用某個任務。
3、使用者可以修改某個已經在執行任務的執行時間表達式,CronExpression。
4、為方便維護,簡化任務的執行呼叫處理,任務的執行入口即Job實現類最好只有一個,該Job執行類相當於工廠類,在實際呼叫時把任務的相關資訊通過引數方式傳入,由該工廠類根據任務資訊來具體執行需要的操作。
在上面的思路下來進行我們的開發吧。
一、spring配置檔案
通過研究,發現要實現我們的功能,只需要以下配置:
- <bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" />
二、任務執行入口,即Job實現類,在這裡我把它看作工廠類:
/** * 定時任務執行工廠類 * * User: liyd * Date: 14-1-3 * Time: 上午10:11 */ public class QuartzJobFactory implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("任務成功執行"); ScheduleJob scheduleJob = (ScheduleJob)context.getMergedJobDataMap().get("scheduleJob"); System.out.println("任務名稱 = [" + scheduleJob.getJobName() + "]"); } }
這裡我們實現的是無狀態的Job,如果要實現有狀態的Job在以前是實現StatefulJob介面,在我使用的quartz 2.2.1中,StatefulJob介面已經不推薦使用了,換成了註解的方式,只需要給你實現的Job類加上註解@DisallowConcurrentExecution即可實現有狀態:
- /**
- * 定時任務執行工廠類
- * <p/>
- * User: liyd
- * Date: 14-1-3
- * Time: 上午10:11
- */
- @DisallowConcurrentExecution
- public class QuartzJobFactory implements Job {...}
三、建立任務
既然要動態的建立任務,我們的任務資訊當然要儲存在某個地方了,這裡我們新建一個儲存任務資訊對應的實體類:
/** * 計劃任務資訊 * * User: liyd * Date: 14-1-3 * Time: 上午10:24 */ public class ScheduleJob { /**