# 解决 Quartz Job 中无法注入 Spring Bean
# 问题
在 Spring 集成 Quartz 的时候有没有遇到过这样一个问题,就是在 Quartz 的 Job 中依赖注入的时候报空指针异常。如果在 Spring 中无法使用 @Autowired 进行注入一个 Bean 的话,这无疑是一个噩耗,那么遇到这个问题,该如何解决呢?
# 原因
出现这个问题是因为定时任务的 Job 对象实例化的过程是通过 Quartz 内部自己完成的,但是我们通过 Spring 进行注入的 Bean 却是由 Spring 容器管理的,Quartz 内部无法感知到 Spring 容器管理的 Bean,所以没有办法在创建 Job 的时候就给装配进去。
# 源码分析
传统的 Spring 项目,我们可以看到 Schedule 的创建是通过 SchedulerFactoryBean 进行创建的,我们看一下 SchedulerFactoryBean 源码,该类实现了 InitializingBean 接口,会在 Bean 属性初始化之后调用 afterPropertiesSet() 方法
@Override | |
public void afterPropertiesSet() throws Exception { | |
// 省略部分代码... | |
// Initialize the Scheduler instance... | |
this.scheduler = prepareScheduler(prepareSchedulerFactory()); | |
// 省略部分代码... | |
} |
接着查看 prepareScheduler() 方法,可以发现如果 jobFactory 不存在的话,默认会使用 AdaptableJobFactory 实现对 Job 对象的创建。
private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws SchedulerException { | |
// 省略部分代码... | |
// Get Scheduler instance from SchedulerFactory. | |
try { | |
Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName); | |
populateSchedulerContext(scheduler); | |
if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) { | |
// Use AdaptableJobFactory as default for a local Scheduler, unless when | |
// explicitly given a null value through the "jobFactory" bean property. | |
this.jobFactory = new AdaptableJobFactory(); | |
} | |
if (this.jobFactory != null) { | |
if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) { | |
((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext); | |
} | |
if (this.jobFactory instanceof SchedulerContextAware) { | |
((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext()); | |
} | |
scheduler.setJobFactory(this.jobFactory); | |
} | |
return scheduler; | |
} | |
// 省略部分代码... | |
} |
既然找到了源码,那么处理起来就方便了,我们如果可以自定义 JobFactory 的话,在创建完 Job 实例之后,再将 Job 注入到 Spring 容器中即可解决该问题。
# 解决
# 自定义 JobFactory
首先自定义一个 JobFactory,通过 AutowireCapableBeanFactory 将创建好的 Job 对象交给 Spring 管理
@Configuration | |
public class CustomJobFactory extends AdaptableJobFactory { | |
@Autowired | |
private AutowireCapableBeanFactory autowireCapableBeanFactory; | |
/** | |
* Create the job instance, populating it with property values taken | |
* from the scheduler context, job data map and trigger data map. | |
* | |
* @param bundle | |
*/ | |
@Override | |
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { | |
Object jobInstance = super.createJobInstance(bundle); | |
autowireCapableBeanFactory.autowireBean(jobInstance); | |
return jobInstance; | |
} | |
} |
再创建一个配置类,将自定义的 JobFactory 设置到 Schedule 中
@Configuration | |
public class QuartzConfig { | |
@Autowired | |
private CustomJobFactory customJobFactory; | |
@SneakyThrows | |
@Bean | |
public Scheduler scheduler(){ | |
SchedulerFactory schedulerFactory = new StdSchedulerFactory(); | |
Scheduler scheduler = schedulerFactory.getScheduler(); | |
// 自定义 JobFactory 使得在 Quartz Job 中可以使用 @Autowired | |
scheduler.setJobFactory(customJobFactory); | |
scheduler.start(); | |
return scheduler; | |
} | |
} |
这样你就可以愉快的在定时任务中使用 @Autowired 注入 Spring 管理的 Bean 了。
@Slf4j | |
public class JobDemo2 implements Job { | |
@Autowired | |
private DemoService demoService; | |
@Override | |
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { | |
demoService.echo("JobDemo2"); | |
} | |
} |
注:此方法原理上可行,但是我在实现上依赖还是注入不了
# 通过 SpringUtil 实现
创建一个 Spring 工具类
/** | |
* <p> | |
* Spring 工具类 | |
* </p> | |
* | |
* @author yangkai.shen | |
* @date Created in 2020-06-05 11:13 | |
*/ | |
@Slf4j | |
@Component | |
public class SpringUtil implements ApplicationContextAware { | |
private static ApplicationContext context; | |
@Override | |
public void setApplicationContext(ApplicationContext context) throws BeansException { | |
SpringUtil.context = context; | |
} | |
/** | |
* 获取 Spring Bean | |
* | |
* @param clazz 类 | |
* @param <T> 泛型 | |
* @return 对象 | |
*/ | |
public static <T> T getBean(Class<T> clazz) { | |
if (clazz == null) { | |
return null; | |
} | |
return context.getBean(clazz); | |
} | |
/** | |
* 获取 Spring Bean | |
* | |
* @param bean 名称 | |
* @param <T> 泛型 | |
* @return 对象 | |
*/ | |
@SuppressWarnings("unchecked") | |
public static <T> T getBean(String bean) { | |
if (bean == null) { | |
return null; | |
} | |
return (T) context.getBean(bean); | |
} | |
/** | |
* 获取 Spring Bean | |
* | |
* @param beanName 名称 | |
* @param clazz 类 | |
* @param <T> 泛型 | |
* @return 对象 | |
*/ | |
public static <T> T getBean(String beanName, Class<T> clazz) { | |
if (null == beanName || "".equals(beanName.trim())) { | |
return null; | |
} | |
if (clazz == null) { | |
return null; | |
} | |
return (T) context.getBean(beanName, clazz); | |
} | |
/** | |
* 获取上下文 | |
* | |
* @return 上下文 | |
*/ | |
public static ApplicationContext getContext() { | |
if (context == null) { | |
throw new RuntimeException("There has no Spring ApplicationContext!"); | |
} | |
return context; | |
} | |
/** | |
* 发布事件 | |
* | |
* @param event 事件 | |
*/ | |
public static void publishEvent(ApplicationEvent event) { | |
if (context == null) { | |
return; | |
} | |
try { | |
context.publishEvent(event); | |
} catch (Exception ex) { | |
log.error(ex.getMessage()); | |
} | |
} | |
} |
然后在定时任务中通过该工具类获取 Spring Bean,也可以实现同样的效果。
@Slf4j | |
public class JobDemo1 implements Job { | |
@Override | |
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { | |
DemoService demoService = SpringUtil.getBean(DemoService.class); | |
demoService.echo("JobDemo1"); | |
} | |
} |
# Spring Boot 2.x 中的实现
了解 Spring Boot 自动装配机制的小伙伴应该都知道,当我们引入了 spring-boot-starter-quartz 这样一个 starter 依赖之后,最终其实是通过 Spring Boot 的 SPI 机制,自动加载了一个 QuartzAutoConfiguration 配置类,该配置类是对 Quartz 中的一些对象及配置进行一系列的初始化操作。
在这个配置类中,定义了一个 SchedulerFactoryBean ,这个类主要是实现在 Spring 中进行对任务的调度。
需要注意的是,在 Spring Boot 中,该类默认是通过 SpringBeanJobFactory 实现对 Job 对象的创建。源码如下:
@Bean | |
@ConditionalOnMissingBean | |
public SchedulerFactoryBean quartzScheduler(QuartzProperties properties, | |
ObjectProvider<SchedulerFactoryBeanCustomizer> customizers, ObjectProvider<JobDetail> jobDetails, | |
Map<String, Calendar> calendars, ObjectProvider<Trigger> triggers, ApplicationContext applicationContext) { | |
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); | |
SpringBeanJobFactory jobFactory = new SpringBeanJobFactory(); | |
jobFactory.setApplicationContext(applicationContext); | |
schedulerFactoryBean.setJobFactory(jobFactory); | |
// 省略其余代码.... | |
return schedulerFactoryBean; | |
} |
那么这个 SpringBeanJobFactory 有什么特殊的呢?该类继承了 AdaptableJobFactory 同时也实现了 ApplicationContextAware 接口
@Override | |
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { | |
// 关键代码 | |
Object job = (this.applicationContext != null ? | |
this.applicationContext.getAutowireCapableBeanFactory().createBean( | |
bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) : | |
super.createJobInstance(bundle)); | |
// 省略其余代码.... | |
return job; | |
} |
我们可以看到该类在重写 createJobInstance() 方法中,创建 Job 对象的时候,会先判断是否在 Spring 上下文中,如果是在 Spring 环境中,也是通过 AutowireCapableBeanFactory 将 Job 对象放在 Spring 容器中,如果没有,则会调用父类 AdaptableJobFactory 进行反射创建 Job 对象。
从这儿可以看出,Spring Boot 2.x 版本通过 spring-boot-starter-quartz 集成之后,默认就是可以在 Job 对象中使用 @Autowired 的,并且实现的思路和我们是一致的,通过 AutowireCapableBeanFactory 将 Job 对象放入 Spring 容器中才是正确做法。
注:其实我用的就是 2.x 版本,在一开始什么别的操作都没有的时候,也会出现注入不成功的问题,只能说很奇怪。
