1. 程式人生 > >Quartz的簡化(只要一張表,動態啟停任務)

Quartz的簡化(只要一張表,動態啟停任務)

專案中有模組依賴到了Quartz來做定時任務。那天和大師研究了一下午,講一個使用這個工具的一些收穫。

首先,用的不是原先的Quartz,而是與spring整合的。需要用到Spring-Conte-Support-4.2.3.Release.jar,Quartz-2.2.2.jar。使用的方式如下

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

使用xml檔案進行配置,都是“四段式”的配置方法。

1是自己寫的來做具體邏輯處理的類

2是JobDetail,MethodInvokingJobDetailFactoryBean是spring對Quartz的JobDetail的包裝,在它裡面,定義了一個來自org.quartz包的JobDetail。

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;
......
spring中大量用到了FactoryBean,這個介面的說明:

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.

A FactoryBean is defined in a bean style, but the object exposed for bean references ( is always the object that it creates.

大意是說一個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都會消失,執行緒終結,這是最徹底的方式。

我的表設計,可以參考一下。

總體來說,目前基本滿足了要求。既不要一大堆表,又可以資料庫配置,不重啟應用。這也告訴我一件事,瞭解一下別人是怎麼寫程式碼的是挺有意思的。都是牛人。

如果大家有更好的方法,敬請賜教。