在開發時我們會常常遇到定時任務可以由客戶進行管理在什么時候去執行或者甚至不再執行該定時任務。而Spring中所提供的定時任務組件卻只能夠通過修改trigger的配置才能夠控制定時的時間以及是否啟用定時任務,為此我搜索了網上的一些解決方法,發現還是不能夠很好的解決這個問題。所以干脆仔仔細細的研究了一把Quartz和Spring中相關的源碼,我們發現當我們在Spring通過如下聲明定時任務時:
- <bean id="yourJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
- <property name="targetObject" ref="yourJobBean"/>
- <property name="targetMethod" value="yourJobMethod"/>
- <property name="concurrent" value="false"/>
- </bean>
- <bean id="yourCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean" >
- <property name="jobDetail" ref="yourobDetail"/>
- <property name="cronExpression">
- <value>0 0 2 * * ?</value>
- </property>
- </bean>
- <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
- <property name="triggers">
- <list>
- <ref local="yourCronTrigger"/>
- </list>
- </property>
- bean>
所生成的Quartz的JobDetail並不是你定義的Bean,因為JobDetail並不能夠直接存放一個實例,具體可以參考JobDetail的構造函數:
- JobDetail(String name, String group, Class jobClass)
- Create a JobDetail with the given name, group and class, and the default settings of all the other properties.
Spring將其轉換為MethodInvokingJob或者StatefulMethodInvokingJob類型,其實這兩個類都是從QuartzJobBean類繼承而來,那么我們來看看QuartzJobBean類的代碼:
- public abstract class QuartzJobBean implements Job {
- /**
- * 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 {
- BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
- MutablePropertyValues pvs = new MutablePropertyValues();
- pvs.addPropertyValues(context.getScheduler().getContext());
- pvs.addPropertyValues(context.getMergedJobDataMap());
- 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;
- }
看到這個你或許已經明白了Spring對Quartz的封裝原理了。是的,Spring通過這種方式最后就可以在Job真正執行的時候可以反調用到我們所注入的類和方法,具體的代碼在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 ? (Class) MethodInvokingJob.class : StatefulMethodInvokingJob.class);
- // Build JobDetail instance.
- this.jobDetail = new JobDetail(name, this.group, jobClass);
- this.jobDetail.getJobDataMap().put("methodInvoker", this);
- this.jobDetail.setVolatility(true);
- this.jobDetail.setDurability(true);
- // Register job listener names.
- if (this.jobListenerNames != null) {
- for (int i = 0; i < this.jobListenerNames.length; i++) {
- this.jobDetail.addJobListener(this.jobListenerNames[i]);
- }
- }
- postProcessJobDetail(this.jobDetail);
- }
其實主要是這句:
- this.jobDetail.getJobDataMap().put("methodInvoker", this);
這樣在調用的時候那就可以調用到我們所注入的類和方法了。
那好我們理解了Spring的原理后就可以動手修改成我們自己的了,所以在設計的時候我們考慮到以下兩個方面:
- 精簡Spring的配置,添加一個定時任務至少有10幾行配置代碼,太麻煩了,最好只聲明一個Bean就可以了;
- 客戶不但能夠控制執行的時間,還可以能夠啟用/禁用某個定時任務。
在上面的思路下,我們決定保留Spring的這個配置:
- <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
- </bean>
只不過不需要注入trigger這個屬性了,我們僅僅利用Spring來幫我們構造一個Scheduler。
為了簡化后續的處理,我們決定定義一個自己Job的接口,方便進行控制及精簡代碼。
- public interface MyJob {
- /**
- * 執行具體的任務處理
- * @throws JobException
- */
- public void execute() throws JobException;
- }
接下來我們還是需要一個類似QuartzJobBean類,因此參考Spring的更改如下:
- public class QuartzJobBean implements Job {
- public void execute(JobExecutionContext context) throws JobExecutionException {
- String targetBeanId = (String)context.getMergedJobDataMap().get("targetObjectId");
- if(StringUtils.isNullString(targetBeanId))
- return;
- Object targetBean = SpringUtils.getBean(targetBeanId);
- if(null == targetBean)
- return;
- // 判斷是否是實現了MyJob接口
- if(!(targetBean instanceof MyJob))
- return;
- // 執行相應的任務
- ((MyJob)targetBean).execute();
- }
- }
這個類處理邏輯就是通過獲取我在創建JobDetail任務是設定的目標任務Bean的,即targetObjectId的值,然后通過SpringUtils獲取該Bean,最后轉換成MyJob接口執行。
如何創建JobDetail呢,因為我們前面已經說明不需要在Spring配置那么麻煩而且客戶還可以進行配置,因此我們將任務的定時信息存放在數據庫中,相應的Domain定義如下:
- public class SchedulingJob extends Entry {
- public static final int JS_ENABLED = 0; // 任務啟用狀態
- public static final int JS_DISABLED = 1; // 任務禁用狀態
- public static final int JS_DELETE = 2; // 任務已刪除狀態
- private String jobId; // 任務的Id,一般為所定義Bean的ID
- private String jobName; // 任務的描述
- private String jobGroup; // 任務所屬組的名稱
- private int jobStatus; // 任務的狀態,0:啟用;1:禁用;2:已刪除
- private String cronExpression; // 定時任務運行時間表達式
- private String memos; // 任務描述
- // 省略getter和setter... ...
- public String getTriggerName(){
- return this.getJobId() + "Trigger";
- }
- }
這時候我們就可以對定時任務進行控制了,具體控制代碼如下:
- /**
- * 啟用指定的定時任務
- * @param context
- * @param <a href="mailto:schedulingJob@throws">schedulingJob</a>
- */
- protected void enabled(Context context, SchedulingJob schedulingJob) {
- try {
- CronTrigger trigger = (CronTrigger)this.scheduler.getTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup());
- if (null == trigger) {
- // Trigger不存在,那么創建一個
- JobDetail jobDetail = new JobDetail(schedulingJob.getJobId(), schedulingJob.getJobGroup(), QuartzJobBean.class);
- jobDetail.getJobDataMap().put("targetObjectId", schedulingJob.getJobId());
- trigger = new CronTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup(), schedulingJob.getCronExpression());
- this.scheduler.scheduleJob(jobDetail, trigger);
- }else{
- // Trigger已存在,那么更新相應的定時設置
- trigger.setCronExpression(schedulingJob.getCronExpression());
- this.scheduler.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
- }
- } catch (SchedulerException e) {
- e.printStackTrace();
- // TODO
- } catch (ParseException e) {
- e.printStackTrace();
- // TODO
- }
- }
- /**
- * 禁用指定的定時任務
- * @param context
- * @param <a href="mailto:schedulingJob@throws">schedulingJob</a>
- */
- protected void disabled(Context context, SchedulingJob schedulingJob) {
- try {
- Trigger trigger = this.scheduler.getTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup());
- if (null != trigger) {
- this.scheduler.deleteJob(schedulingJob.getJobId(), schedulingJob.getJobGroup());
- }
- } catch (SchedulerException e) {
- e.printStackTrace();
- // TODO
- }
- }
再加上前台處理頁面(這個俺就不寫了,估計大家都會) 這樣我們就可以在控制定時任務的定時時間及啟用和禁用了。
最后我們的配置代碼如下:
- <bean id="taskScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"/>
- <bean id="tempDirClearTask" class="com.my.module.attachment.util.TempDirClearTask"/>
P.S.
這個定時任務組件是有局限性的,比如所設置的SchedulingJob.jobID必須是和Spring中定時任務Bean的id一致,否則這個定時任務不會被運行。還有這個現在僅支持CronTrigger。而且也不支持分組,不過你可以通過擴展。
再一個要記得寫一個加載類在系統初始化時從數據庫的配置中加載所有啟用的定時任務哦,不然所有定時任務都不能夠運行