這個場景其實很普遍啊,我們在寫 quartz 的 job 或者說 task 的時候,肯定想要把 spring 的 bean 拿來用,這樣就像正常調用的模式那樣,但是我們知道 quartz 是一個容器,spring 又是另外的一個容器,s所以,我們要在2者這件架起橋梁,讓spring的bean 能注冊到 quartz 當中去。
下面我說的是 springboot 當中整合 quartz 的時候怎么 打通2者的 :
1. 首先,我們要一定要引入 support 包 :
<!-- 引入 quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.3</version>
</dependency>
<!-- 引入 context-support ,否則quartz 容器無法得到spring的bean -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.12.RELEASE</version>
<scope>compile</scope>
</dependency>
要實現Job注入bean必須使用spring-context-support
2. 建一個MyJobFactory
然后 把其設置為SchedulerFactoryBean 的 JobFactory。其目的是因為我在具體的Job 中 需要Spring 注入一些Service。
所以我們要自定義一個jobfactory, 讓其在具體job 類實例化時 使用Spring 的API 來進行依賴注入。
比如我們 一個最簡單的方法是 繼承 AdaptableJobFactory 類,完成對其方法的重寫:
@Component public class MyJobFactory extends AdaptableJobFactory { @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 調用父類的方法 Object jobInstance = super.createJobInstance(bundle); // 進行注入 capableBeanFactory.autowireBean(jobInstance); return jobInstance; } }
然后我們通過 xml 或者 javaConfig 的方式立馬 指定 quartz的相關 factoryBean,把我們的 這個自己先的 MyJobFactory 丟進去 。
@Configuration public class QuartzConfig { @Autowired private MyJobFactory myJobFactory; @Bean public SchedulerFactoryBean schedulerFactoryBean() { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); schedulerFactoryBean.setJobFactory(myJobFactory); System.out.println("myJobFactory:"+myJobFactory); return schedulerFactoryBean; } @Bean public Scheduler scheduler() { return schedulerFactoryBean().getScheduler(); } }
下面是我在項目當中實際寫的,其實其模式跟這差不多,只不過是 我對 MyJobFactory 的嵌入時機不一樣 :
1. 首先,我們自己寫一個
package root.report.quartz.config; import org.quartz.Job; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.spi.JobFactory; import org.quartz.spi.TriggerFiredBundle; import org.springframework.scheduling.quartz.DelegatingJob; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Method; /** * @Auther: pccw * @Date: 2018/11/27 16:50 * @Description: */ public class AdaptableJobFactory implements JobFactory { @Override public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException { return newJob(bundle); } // 我們自己再寫個 job public Job newJob(TriggerFiredBundle bundle) throws SchedulerException { try { Object jobObject = createJobInstance(bundle); return adaptJob(jobObject); } catch (Exception ex) { throw new SchedulerException("Job instantiation failed", ex); } } protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Method getJobDetail = bundle.getClass().getMethod("getJobDetail"); Object jobDetail = ReflectionUtils.invokeMethod(getJobDetail, bundle); Method getJobClass = jobDetail.getClass().getMethod("getJobClass"); Class jobClass = (Class) ReflectionUtils.invokeMethod(getJobClass, jobDetail); return jobClass.newInstance(); } protected Job adaptJob(Object jobObject) throws Exception { if (jobObject instanceof Job) { return (Job) jobObject; } else if (jobObject instanceof Runnable) { // 需要引入 context-support 的jar 包才行 return new DelegatingJob((Runnable) jobObject); } else { throw new IllegalArgumentException("Unable to execute job class [" + jobObject.getClass().getName() + "]: only [org.quartz.Job] and [java.lang.Runnable] supported."); } } }
2. 然后,我們寫自己的 MyJobFactory
package root.report.quartz.config; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.stereotype.Component; /** * @Auther: pccw * @Date: 2018/11/27 18:05 * @Description: * 把 job factory 升級成配置類 */ @Component public class MyJobFactory extends AdaptableJobFactory{ @Autowired private AutowireCapableBeanFactory capableBeanFactory; protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { //調用父類的方法 Object jobInstance = super.createJobInstance(bundle); capableBeanFactory.autowireBean(jobInstance); return jobInstance; } }
3. 最后,我在業務當中自然是希望馬上去初始化定時任務並在項目啟動的時候去執行他,所以這個啟動項目-》 啟動定時任務 -》 這個時機就是我們要注入 jobFactory 的時機
package root.report.quartz.config; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.session.SqlSession; import org.apache.log4j.Logger; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import root.report.db.DbFactory; import root.report.quartz.Service.IPccwJobService; import root.report.quartz.Util.BaseJob; import root.report.quartz.entity.PccwJob; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Auther: pccw * @Date: 2018/11/27 18:06 * @Description: * 這個類用於啟動springboot 時,加載作業,run方法自動執行。 * 另外可以使用 ApplicationRunner */ @Component public class InitStartSchedule implements CommandLineRunner { private static Logger logger = Logger.getLogger(InitStartSchedule.class); @Autowired private IPccwJobService pccwJobService; @Autowired private MyJobFactory myJobFactory; @Override public void run(String... args) throws Exception { /** * 用於程序啟動時加載定時任務,並執行已啟動的定時任務(只會執行一次,在程序啟動完執行) */ //查詢job狀態為啟用的 HashMap<String,String> map = new HashMap<String,String>(); map.put("jobStatus", "1"); // 1. 查詢數據庫是否有要執行的任務 SqlSession sqlSession = DbFactory.Open(DbFactory.FORM); List<PccwJob> jobList= this.pccwJobService.querySysJobList(sqlSession,map); if( null == jobList || jobList.size() ==0){ logger.info("系統啟動,沒有需要執行的任務... ..."); } // 2. 對 scheduler 注入 myJobFactory ,讓我們的job能調度 service 方法 // 通過SchedulerFactory獲取一個調度器實例 SchedulerFactory sf = new StdSchedulerFactory(); Scheduler scheduler = sf.getScheduler(); // 如果不設置JobFactory,Service注入到Job會報空指針 scheduler.setJobFactory(myJobFactory); // ********************* 一定要注入我們的bean,才能調用到spring的bean。 // 啟動調度器 scheduler.start(); for (PccwJob sysJob:jobList) { String jobClassName=sysJob.getJobName(); String jobGroupName=sysJob.getJobGroup(); //構建job信息 JobDetail jobDetail = JobBuilder.newJob(getClass(sysJob.getJobClassPath()).getClass()).withIdentity(jobClassName, jobGroupName).build(); if (StringUtils.isNotEmpty(sysJob.getJobDataMap())) { JSONObject jb = JSONObject.parseObject(sysJob.getJobDataMap()); Map<String, Object> dataMap = (Map<String, Object>)jb.get("data"); for (Map.Entry<String, Object> m:dataMap.entrySet()) { jobDetail.getJobDataMap().put(m.getKey(),m.getValue()); } } //表達式調度構建器(即任務執行的時間) CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(sysJob.getJobCron()); //按新的cronExpression表達式構建一個新的trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName) .withSchedule(scheduleBuilder).startNow().build(); // 任務不存在的時候才添加 if( !scheduler.checkExists(jobDetail.getKey()) ){ try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { logger.info("\n創建定時任務失敗"+e); throw new Exception("創建定時任務失敗"); } } } } public static BaseJob getClass(String classname) throws Exception { Class<?> c= Class.forName(classname); return (BaseJob)c.newInstance(); } }
不過有個概念我一直說反了,其實是我們把 job 托管給spring 管理,我們可以看這篇博客,也是介紹怎么結合 bean的:
https://blog.csdn.net/pml18710973036/article/details/70768259。
package cn.zto.job; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.quartz.SpringBeanJobFactory; public class JobBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobInstance = super.createJobInstance(bundle); //把Job交給Spring來管理,這樣Job就能使用由Spring產生的Bean了 applicationContext.getAutowireCapableBeanFactory().autowireBean(jobInstance); return jobInstance; } }
SchedulerFactoryBean的一個重要功能是允許你將Quartz配置文件中的信息轉移到Spring配置文件中,帶來的好處是,配置信息的集中化管理,同時我們不必熟悉多種框架的配置文件結構。回憶一個Spring集成JPA、Hibernate框架,就知道這是Spring在集成第三方框架經常采用的招數之一。SchedulerFactoryBean通過以下屬性代替框架的自身配置文件:
●dataSource:當需要使用數據庫來持久化任務調度數據時,你可以在Quartz中配置數據源,也可以直接在Spring中通過dataSource指定一個Spring管理的數據源。如果指定了該屬性,即使quartz.properties中已經定義了數據源,也會被此dataSource覆蓋;
●transactionManager:可以通過該屬性設置一個Spring事務管理器。在設置dataSource時,Spring強烈推薦你使用一個事務管理器,否則數據表鎖定可能不能正常工作;
●nonTransactionalDataSource:在全局事務的情況下,如果你不希望Scheduler執行化數據操作參與到全局事務中,則可以通過該屬性指定數據源。在Spring本地事務的情況下,使用dataSource屬性就足夠了;
●quartzProperties:類型為Properties,允許你在Spring中定義Quartz的屬性。其值將覆蓋quartz.properties配置文件中的設置,這些屬性必須是Quartz能夠識別的合法屬性,在配置時,你可以需要查看Quartz的相關文檔。
配置好數據源dataSource后,需要在Quartz的QRTZ_LOCKS表中插入以下數據:
INSERT INTO QRTZ_LOCKS values('TRIGGER_ACCESS');
INSERT INTO QRTZ_LOCKS values('JOB_ACCESS');
否則會報
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scheduler' defined in file [...\webapps\WEB-INF\classes\config\applicationContext-quartz.xml]: Invocation of init method failed; nested exception is org.quartz.SchedulerConfigException: Failure occured during job recovery. [See nested exception: org.quartz.impl.jdbcjobstore.LockException: Failure obtaining db row lock: No row exists in table QRTZ_LOCKS for lock named: TRIGGER_ACCESS [See nested exception: java.sql.SQLException: No row exists in table QRTZ_LOCKS for lock named: TRIGGER_ACCESS]]異常
既然有job 托管給 spring的機制,肯定也有 從quartz 運行環境中去讀取 spring bean的做法,我們只需要 從 Job的執行容器當中去獲取當前的web容器即可 :
public class ExampleJob extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { WebApplicationContext a = ContextLoader.getCurrentWebApplicationContext(); System.out.println(a.getBean(APIController.class)); System.out.println(a.containsBean("APIController")); } }
因為我們的任務的execute方法是有這個 Job的運行容器 :

通過ContextLoader類的**getCurrentWebApplicationContext()**方法獲取spring的WebApplicationContext,然后再通過相應獲取Bean的方法獲取Bean。上述方法是個通用方法,不只可以用在Quartz中,其他非spring管理類也可以通過這種方法來獲取。
