这个场景其实很普遍啊,我们在写 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管理类也可以通过这种方法来获取。