1. 程式人生 > >Spring3整合Quartz2實現定時任務及動態任務調整(新增刪除暫停恢復)--推薦

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):

  1. 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配置檔案

通過研究,發現要實現我們的功能,只需要以下配置:

  1. <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即可實現有狀態:

  1. /**
  2. * 定時任務執行工廠類
  3. * <p/>
  4. * User: liyd
  5. * Date: 14-1-3
  6. * Time: 上午10:11
  7. */
  8. @DisallowConcurrentExecution
  9. public class QuartzJobFactory implements Job {...}
三、建立任務

既然要動態的建立任務,我們的任務資訊當然要儲存在某個地方了,這裡我們新建一個儲存任務資訊對應的實體類:

複製程式碼
/**
 * 計劃任務資訊
 * 
 * User: liyd
 * Date: 14-1-3
 * Time: 上午10:24
 */
public class ScheduleJob {
 
    /**