Quartz的簡化(只要一張表,動態啟停任務)
專案中有模組依賴到了Quartz來做定時任務。那天和大師研究了一下午,講一個使用這個工具的一些收穫。
首先,用的不是原先的Quartz,而是與spring整合的。需要用到Spring-Conte-Support-4.2.3.Release.jar,Quartz-2.2.2.jar。使用的方式如下
使用xml檔案進行配置,都是“四段式”的配置方法。<pre name="code" class="html"><pre name="code" class="html"><pre name="code" class="html"><?xml version="1.0" encoding="utf-8"?> <beans> <!-- 定時清理 MessageRelation和hadsend Map 1 --> <bean id="clearRelationJob" class="com.yicong.kisp.job.ClearRelationAndHadsendJob"/> <!-- JobDetajil,基於MethodInvokingJobDetailFactoryBean呼叫普通Spring Bean 2 --> <bean id="clearRelationJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="clearRelationJob"/> <property name="targetMethod" value="doClear"/> <!-- 同一任務在前一次執行未完成而Trigger時間又到時是否併發開始新的執行, 預設為true. --> <property name="concurrent" value="false"/> </bean> <!-- Cron式Trigger定義 3 --> <bean id="clearRelationJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="clearRelationJobDetail"/> <property name="misfireInstruction" value="2"/> <!-- 全年、周2,4,6、00:01:01 --> <property name="cronExpression" value="1 1 0 ? 1-12 2,4,6 *"/> <!-- 延遲10秒啟動 --> <property name="startDelay" value="10000"/> </bean> <!-- 排程器 4 --> <bean id="schedulerFactoryBean" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="clearRelationJobTrigger"/> </list> </property> </bean> </beans>
1是自己寫的來做具體邏輯處理的類
2是JobDetail,MethodInvokingJobDetailFactoryBean是spring對Quartz的JobDetail的包裝,在它裡面,定義了一個來自org.quartz包的JobDetail。
spring中大量用到了FactoryBean,這個介面的說明:public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker implements FactoryBean<JobDetail>, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean { private String name; private boolean concurrent = true; private String targetBeanName; private String beanName; private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private BeanFactory beanFactory; private JobDetail jobDetail; ......
Interface to be implemented by objects used within a which are themselves factories. If a bean implements this interface, it is used as a factory for an object to expose, not directly as a bean instance that will be exposed itself.
NB: A bean that implements this interface cannot be used as a normal bean.
大意是說一個bean實現了它,通常用來作為一個向外暴露物件的工廠,而非直接得到它本身,向外暴露的物件由泛型指定,通過覆蓋的getObject()返回真正的物件。
This interface is heavily used within the framework itself, for example for the AOP or the. It can be used for application components as well; however, this is not common outside of infrastructure code.
本介面被框架本身大量使用,如果AOP‘動態代理方面。然而在基礎程式碼外少見。
2中的targetObject、targetMethod指定了類與方法,還有一些屬性配置未寫出來。
當2中的JobDetail準備完了,就可以被3引用了。3用來配置一個基於Cron的觸發器。也是用到了FactoryBean。Trigger的作用在於根據Cron中的設定,定時觸發job,所以還有concurrent、misfireInstructiont等可以配置。
4中把Trigger註冊到Scheduler中,Scheduler配置執行緒池的大小。一個Job執行時消耗一個執行緒。
就這麼多。就開始就這麼用,感覺也還可以,但是改了什麼屬性就要重啟應用,顯得不靈活方便。雖然Quartz也有基於資料庫表的,但是配套的表有十多張,有些用不上也不能刪。想著業務表才十多張,Quartz也要這麼多,很不舒服。
最方便的做法就是:
1、把必須要的JobDetail和Trigger的屬性移到一張表中,在應用啟動後自動讀取表資料,初始化job並執行。
2、在管理頁面中可以控制Job的執行、停止狀態。
That's all。
那就要進行相應的改造了。由於已經用了spring,不考慮用原先的quartz,那麼能不能人這四段程式碼中抽取出來,因為以xml的形式,一個job的執行,只需要這四段程式碼。找到了原始碼包,先找開MethodInvokingJobDetailFactoryBean,關鍵程式碼如下:
@Override
@SuppressWarnings("unchecked")
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.
JobDetailImpl jdi = new JobDetailImpl();
jdi.setName(name);
jdi.setGroup(this.group);
jdi.setJobClass((Class) jobClass);
jdi.setDurability(true);
jdi.getJobDataMap().put("methodInvoker", this);
this.jobDetail = jdi;
postProcessJobDetail(this.jobDetail);
}
afterPropertiesSet()實現的是InitializingBean中的方法。根據註釋,Interface to be implemented by beans that need to react once all their properties have been set by a BeanFactory。實現了這個介面的類,當屬性都設定完成,將執行一次。在這裡一個JobDetail物件封裝完成,postProcessJobDetail沒有預設實現,空方法。
CronTriggerFactoryBean也是相同道理:
@Override
public void afterPropertiesSet() throws ParseException {
......
CronTriggerImpl cti = new CronTriggerImpl();
cti.setName(this.name);
cti.setGroup(this.group);
cti.setJobKey(this.jobDetail.getKey());
cti.setJobDataMap(this.jobDataMap);
cti.setStartTime(this.startTime);
cti.setCronExpression(this.cronExpression);
cti.setTimeZone(this.timeZone);
cti.setCalendarName(this.calendarName);
cti.setPriority(this.priority);
cti.setMisfireInstruction(this.misfireInstruction);
cti.setDescription(this.description);
this.cronTrigger = cti;
}
最後是SchedulerFactoryBean,程式碼略。
弄清了思路,基本上是這樣子的:
@SuppressWarnings({ "rawtypes", "unchecked" })
public void startJob(Integer jobId, String jobName, String method,
String clazz, String cron, String startDelay, String triggerName,
Properties p) throws Exception
{
if (MessageContainer.quartzMap.containsKey(jobName)) {
System.out.println("已經有名為" + jobName + "的Job了");
}else {
/*
* JobDetail
*/
MethodInvokingJobDetailFactoryBean methodJD = new MethodInvokingJobDetailFactoryBean();
methodJD.setName(jobName);
/*
* //根據類名獲取Class物件 Class c=Class.forName(clazz); //引數型別陣列 Class[]
* parameterTypes={Integer.class}; //根據引數型別獲取相應的建構函式
* java.lang.reflect.Constructor
* constructor=c.getConstructor(parameterTypes); //引數陣列 Object[]
* parameters={jobId}; //根據獲取的建構函式和引數,建立例項 Object
* o=constructor.newInstance(parameters);
*/
// 資料庫中類路徑是com.xxx.Test形式的字串,通過反射獲取一個例項。Test中可能使用了@autowire注入了其他物件,
// 所以必須要從spring中get出來,不然o裡面注入的都是空的
Class c = Class.forName(clazz);
Object o = BeanHoldFactory.getApplicationContext().getBean(c);
// 這裡通過setJobId方法向Test物件中傳入了一個值
Method mth = c.getMethod("setJobId", Integer.class);
mth.invoke(o, jobId);
methodJD.setTargetObject(o);
methodJD.setTargetMethod(method);
methodJD.afterPropertiesSet();
JobDetail jd = methodJD.getObject();
/*
* Trigger
*/
CronTriggerFactoryBean crTiger = new CronTriggerFactoryBean();
crTiger.setCronExpression(cron);
crTiger.setName(triggerName);
crTiger.setStartDelay(Integer.valueOf(startDelay));
crTiger.setJobDetail(jd);
//crTiger.setMisfireInstruction(Integer.valueOf(misfire));
crTiger.afterPropertiesSet();
Trigger trigger = crTiger.getObject();
/*
* scheduler
*/
Properties p = new Properties();
p.setProperty("org.quartz.threadPool.threadCount", "1");
p.setProperty("org.quartz.scheduler.skipUpdateCheck", "true");
SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
// setTriggers(... triggers)可一次性傳入多個trigger
scheduler.setTriggers(trigger);
scheduler.setQuartzProperties(p);
scheduler.afterPropertiesSet();
MessageContainer.quartzMap.put(jobName, scheduler);
scheduler.start();
}
}
可以看出JobDetail,Trigger和Scheduler都是new出來的,一一對應,這是因為在scheduler只有setTrigger,如果在頁面新加一個job,相同的程式碼執行一遍,每set一次,原有的trigger就會丟失,而且執行緒數不對。用一個HashMap來儲存scheduler。這樣每次add時,可以通過contains(jobName)來判斷是不已經存在。
你可以使用ApplicationListener在spring容器完成時,先把表的資料讀到記憶體中,然後再看有個job,就迴圈執行多少次。Scheduler也提供了destroy()來銷燬整個排程器,這樣註冊在上面的所有trigger都會消失,執行緒終結,這是最徹底的方式。
我的表設計,可以參考一下。
總體來說,目前基本滿足了要求。既不要一大堆表,又可以資料庫配置,不重啟應用。這也告訴我一件事,瞭解一下別人是怎麼寫程式碼的是挺有意思的。都是牛人。
如果大家有更好的方法,敬請賜教。