2011年7月8日 星期五

Spring + Quartz Scheduler with Service Transaction

Spring 裡面集成了對Quartz 的支持, 並且對對Quartz進行了封裝, 在使用上是比較方便的.
上次手邊一個Project source code 翻開spring 的ApplicationContext.xml 一看, 可以看到Quartz基本的設定,如SchedulerFactoryBean,JobDetail,Trigger....等,很正常,當我幫忙找一個Bug時就發現了一些問題,

當他們想要設定一個Job, 裡面有個TaskExecutor , 每次會做10個Thread , 然後每個Thread做的事都是1條Transaction , 所以自己寫了一個MyQuartzScheduler.class , 去幹一些事,每個Job裡面會有一個TaskExecutor , TaskExecutor 呼叫Service method 然後開一個Transaction, 這時問題就來了, Transaction 並沒有預期的開啟,Spring 連create Transaction 都沒啟動, 就去看了一下..

MyQuartzScheduler.java
public class MyQuartzScheduler {

 private static final Log logger = MyUtils.getLog();
  
// private SchedulerFactoryBean schedulerFactory; 
 private TaskExecutor taskExecutor;  
 private Scheduler scheduler; 
 
 private int job_seq = 0;
 private int trigger_seq = 0;
 
 
 /**
  * constructor
  * @param inTaskExecutor
  */
 public MyQuartzScheduler(TaskExecutor inTaskExecutor ) {
  //  this.taskExecutor = inTaskExecutor;
  try {
   SchedulerFactory sf = new StdSchedulerFactory();
   this.scheduler = sf.getScheduler();
  } catch (Exception e) {
   logger.error("MyQuartzScheduler exception", e);
  }
 }
     . 
     .
     .
     .
     .
     .
     .
     .
     .
}

上面的Code在測試之後發現, 雖然MyQuartzScheduler在ApplicationContext.xml 有定義bean的相關設定,但是最重要的Quartz Scheduler不是尤Spring所給的, SchedulerFactory sf = new StdSchedulerFactory(), 然後Transaction 設定在Service Layer, Service 傳進來的Spring也無法在Scheduler啟動時辦別,最主要的是這個的目的只是為了手動去設定,啟動,停止Quartz Scheduler 的部份。

關於Quartz Scheduler跟Spring 的相關設定,Quartz Job with Transaction 是可行的,
官方的Documentation有寫到

Transactions

  • Quartz can participate in JTA transactions, via the use of JobStoreCMT (a subclass of JDBCJobStore).
  • Quartz can manage JTA transactions (begin and commit them) around the execution of a Job, so that the work performed by the Job automatically happens within a JTA transaction.
當然這邊的設定就不列出來了,我的實作也是用另一種方法,從Spring fourm 上找到的Solution , 對於簡單的問題比較好解決。

如果想了解Spring + Quartz 的設定, 跟基本的Example 可以參考如下:
Spring + Quartz scheduler example
25.6 Using the OpenSymphony Quartz Scheduler

首先是TaskExceutor 在executor service 時沒有啟動Transaction , 主因是原本設計上的錯誤, 原本在ServiceA.method() 呼叫時,裡面做了一些事,取得TaskExecutor , 但是又把this 自己傳進去,然後再呼叫method, 這邊沒去追Spring 的Source code , 但是應該設計上是不能如此,從Spring Fourm 找到的解如下:

other code....
.
.
taskExecutor.execute(new Runnable() {
     @Override
     public void run() {
      try {
       newCaseService.deleteCaseReferenceData(deleteCase);
      } catch (Exception e) {
       logger.info("Exception for taskExecutor "   e);
      }
 
     }
    });



現在來解解Spring + Quartz 的問題,
我們有一個Interface TestManullySchedulerService 跟implements TestManullySchedulerServiceImpl 如下

TestManullySchedulerService.java
package com.gfactor.emaildiscovery.service;

public interface TestManullySchedulerService {
 public void testTransactionFunction();
}


TestManullySchedulerServiceImpl .java
package com.gfactor.emaildiscovery.service.impl;

import org.apache.commons.logging.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateSystemException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional

@Transactional(readOnly = false, propagation = Propagation.REQUIRED,rollbackFor={Exception.class,HibernateSystemException.class})
public class TestManullySchedulerServiceImpl implements
  TestManullySchedulerService {
 
 private static final Log logger = MyUtils.getLog();

 @Autowired
 private UserRoleFunDAO userRoleFunDAO;
 
 
 @Transactional
 public void testTransactionFunction() {
  DbUser[] dbuser = userRoleFunDAO.getAllUsers();
  logger.info("TestManullySchedulerServiceImpl check user object result list = "  dbuser.length);
 }


 public UserRoleFunDAO getUserRoleFunDAO() {
  return userRoleFunDAO;
 }


 public void setUserRoleFunDAO(UserRoleFunDAO userRoleFunDAO) {
  this.userRoleFunDAO = userRoleFunDAO;
 }
}

單純對針testTransactionFunction() 定義相關的Transaction , 再來我們要定義自己的QuartzJobBean,
package com.gfactor.emaildiscovery.schedule;

import org.apache.commons.logging.Log;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

import com.gfactor.emaildiscovery.service.TestManullySchedulerService;
import com.gfactor.emaildiscovery.utils.MyUtils;
import com.gfactor.emaildiscovery.utils.TestRumJobMethod;

public class TestManuallyJob extends QuartzJobBean{
 private static final Log logger = MyUtils.getLog();

 @Autowired
 private TestRumJobMethod testRumJobMethod;
 
 @Autowired
 private TestManullySchedulerService testManullySchedulerService;
 
// private 
 public void setTestRumJobMethod(TestRumJobMethod testRumJobMethod) {
  this.testRumJobMethod = testRumJobMethod;
 }

 public TestManullySchedulerService getTestManullySchedulerService() {
  return testManullySchedulerService;
 }

 public void setTestManullySchedulerService(
   TestManullySchedulerService testManullySchedulerService) {
  this.testManullySchedulerService = testManullySchedulerService;
 }

 public TestRumJobMethod getTestRumJobMethod() {
  return testRumJobMethod;
 }

 
 
 @Override
 protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException {  
  logger.info("**** TestManuallyJob start ... ");  
  printTestManuallyJobObjectValues();
  testManullySchedulerService.testTransactionFunction();
  testRumJobMethod.runMeLogs();
 }
 
 public void printTestManuallyJobObjectValues(){
  logger.info("* testRumJobMethod = "   testRumJobMethod);
  logger.info("* testManullySchedulerService = "   testManullySchedulerService);
 }
}


然後隨便寫隻util來測試,
TestManuallySchedulerUtil.java
package com.gfactor.emaildiscovery.schedule;

import org.apache.commons.logging.Log;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.quartz.SimpleTriggerBean;

import com.gfactor.emaildiscovery.utils.MyUtils;

public class TestManuallySchedulerUtil {
 private static final Log logger = MyUtils.getLog();
  
 @Autowired
 @Qualifier("manuallySchedulerFactoryBean")  
 private Scheduler scheduler;
 
 @Autowired
 private SimpleTrigger testSimpleTrigger;

 @Autowired
 @Qualifier("testManuallyJob")
 private JobDetail testManuallyJob;


 public JobDetail getTestManuallyJob() {
  return testManuallyJob;
 }
 public void setTestManuallyJob(JobDetail testManuallyJob) {
  this.testManuallyJob = testManuallyJob;
 }
 public SimpleTrigger getTestSimpleTrigger() {
  return testSimpleTrigger;
 }
 public void setTestSimpleTrigger(SimpleTrigger testSimpleTrigger) {
  this.testSimpleTrigger = testSimpleTrigger;
 }
 public Scheduler getScheduler() {
  return scheduler;
 }
 public void setScheduler(Scheduler scheduler) {
  this.scheduler = scheduler;
 }




 public void printInitObjectValues(){
  logger.info(" ***** TestManuallySchedulerUtil.printInitObjectValues ");  
  logger.info("scheduler,Get() = "   getScheduler()); 
  logger.info("testSimpleTrigger,Get() = "   getTestSimpleTrigger());
  logger.info("getTestSimpleTrigger getJobName = " getTestSimpleTrigger().getJobName());
  logger.info("getTestManuallyJob testManuallyJob = "  getTestManuallyJob());
 }
 
 
 
 public void startScheduler(){
  logger.info("To start Scheduler , source job from Spring DI bean....");
  
  try {
   logger.info("tray block start....");
//   scheduler.scheduleJob(testSimpleTrigger);
   
   scheduler.scheduleJob(testManuallyJob, testSimpleTrigger);
   scheduler.start();
   logger.info("schedler start.....");
  } catch (SchedulerException e) {
   logger.error("Exception for TestManuallySchedulerUtil , startScheduler() : "   e);   
  }
  
  
 }
 
 
 public void stopScheduler(){
  try {
   logger.info("shutdown scheduler....");
   scheduler.shutdown();
  } catch (SchedulerException e) {
   logger.error("Exception for TestManuallySchedulerUtil , stopScheduler() : "   e);   
  }
 }
// public void setScheduler(Scheduler scheduler) {
//  this.scheduler = scheduler;
// }
}
這邊會需要Scheduler, Trigger(SimpleTrigger) 跟JobDetail, 但是要注意的是scheduler跟testManuallyJob這邊指定了@Qualifier, 主要是因為相同的class instance可能會有2個以上,Spring會不知道要DI那一個進來,所以必須指定相關的bean name.
再來是org.springframework.scheduling.quartz.SchedulerFactoryBean 在做DI的時候回傳的實例是一個Scheduler , org.springframework.scheduling.quartz.JobDetailBean 則是一個JobDetail, org.springframework.scheduling.quartz.SimpleTriggerBean則是一個SimpleTrigger。

下面是Spring applicationContext.xml 的設定
<bean id="manuallySchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="autoStartup" value="false" />
 </bean>
 
 <bean id="testRumJobMethod" class="com.gfactor.emaildiscovery.utils.TestRumJobMethod" />
 
 <bean id="testManullySchedulerService" class="com.gfactor.emaildiscovery.service.impl.TestManullySchedulerServiceImpl">
  <property name="userRoleFunDAO" ref="userRoleFunDAO" />
 </bean>
 
 <bean id="testManuallyJob" class="org.springframework.scheduling.quartz.JobDetailBean">
  <property name="jobClass" value="com.gfactor.emaildiscovery.schedule.TestManuallyJob" />
  <property name="jobDataAsMap">
   <map>
    <entry key="testRumJobMethod" value-ref="testRumJobMethod" />
    <entry key="testManullySchedulerService" value-ref="testManullySchedulerService" />
   </map>
  </property>
  

 </bean>
 
 <bean id="testSimpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> 
  <property name="jobDetail" ref="testManuallyJob" />
  <property name="repeatInterval" value="15000" />
  <property name="startDelay" value="5000" />
 
 </bean>
 
 <bean id="testManuallySchedulerUtil" class="com.gfactor.emaildiscovery.schedule.TestManuallySchedulerUtil">
  <!-- 
  <property name="triggers">
   <list>
    <ref bean="testSimpleTrigger" />
   </list>
  </property>
   -->
  <property name="scheduler" ref="manuallySchedulerFactoryBean"/>
 </bean>

這樣就能透過Spring 取得相關的Scheduler,Trigger,JobDetail, 然後去實作相關的QuartzJobBean 來達成需要的功能,如果需要定義Trigger的屬性,可以實作CronTrigger, 再定義自己的屬性,這邊主要是透過自行取得的Scheduler 來達成手動啟動,關閉每一個Scheduler 。


Reference:

Quartz job fires twice when manually starting job :
How do I startup quartz scheduler manually with SchedulerFactoryBean? :
Quartz任务监控管理 (1)
Quartz 在 Spring 中如何动态配置时间