如何解决 Quartz Job 中无法注入 Spring Bean

1. 问题

不知道小伙伴们在 Spring 集成 Quartz 的时候有没有遇到过这样一个问题,就是在 Quartz 的 Job 中 @Autowired 一个 Spring Bean 的时候会报空指针异常。如果在 Spring 中无法使用 @Autowired 进行注入一个 Bean 的话,这无疑是一个噩耗,那么遇到这个问题,该如何解决呢?

2. 原因

出现这个问题是因为定时任务的 Job 对象实例化的过程是通过 Quartz 内部自己完成的,但是我们通过 Spring 进行注入的 Bean 却是由 Spring 容器管理的,Quartz 内部无法感知到 Spring 容器管理的 Bean,所以没有办法在创建 Job 的时候就给装配进去。

3. 源码分析

传统的 Spring 项目,我们可以看到 Schedule 的创建是通过 SchedulerFactoryBean 进行创建的,我们看一下 SchedulerFactoryBean 源码,该类实现了 InitializingBean 接口,会在 Bean 属性初始化之后调用 afterPropertiesSet() 方法。

1
2
3
4
5
6
7
8
@Override
public void afterPropertiesSet() throws Exception {
// 省略部分代码...

// Initialize the Scheduler instance...
this.scheduler = prepareScheduler(prepareSchedulerFactory());
// 省略部分代码...
}

接着查看 prepareScheduler() 方法,可以发现如果 jobFactory 不存在的话,默认会使用 AdaptableJobFactory 实现对 Job 对象的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 容器中即可解决该问题。

4. 解决

首先自定义一个 JobFactory,通过 AutowireCapableBeanFactory 将创建好的 Job 对象交给 Spring 管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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 了。

1
2
3
4
5
6
7
8
9
10
@Slf4j
public class JobDemo2 implements Job {
@Autowired
private DemoService demoService;

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
demoService.echo("JobDemo2");
}
}

5. 知识拓展

5.1. 通过 SpringUtil 实现相同效果

当然了,如果不想通过这种方法实现的话,还有另外一种方式。

创建一个 Spring 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* <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,也可以实现同样的效果。

1
2
3
4
5
6
7
8
9
@Slf4j
public class JobDemo1 implements Job {

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
DemoService demoService = SpringUtil.getBean(DemoService.class);
demoService.echo("JobDemo1");
}
}

5.2. Spring Boot 2.x 中的实现

了解 Spring Boot 自动装配机制的小伙伴应该都知道,当我们引入了 spring-boot-starter-quartz 这样一个 starter 依赖之后,最终其实是通过 Spring Boot 的 SPI 机制,自动加载了一个 QuartzAutoConfiguration 配置类,该配置类是对 Quartz 中的一些对象及配置进行一系列的初始化操作。

在这个配置类中,定义了一个 SchedulerFactoryBean,这个类主要是实现在 Spring 中进行对任务的调度。

需要注意的是,在 Spring Boot 中,该类默认是通过 SpringBeanJobFactory 实现对 Job 对象的创建。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@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 接口

1
2
3
4
5
6
7
8
9
10
11
@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 容器中才是正确做法。

6. 代码

  • Spring 集成 Quartz:https://github.com/xkcoding/practice_demo/tree/master/quartz-demo-spring
  • Spring Boot 集成 Quartz:https://github.com/xkcoding/practice_demo/tree/master/quartz-demo-spring-boot
-------------本文结束  感谢您的阅读-------------
xkcoding wechat
欢迎来我的公众号「xkcoding小凯扣丁」逛逛
o(╯□╰)o 赞助一杯咖啡 ~~