Spring/Spring boot正确集成Quartz及解决@Autowired失效问题
周五检查以前Spring boot
集成Quartz
项目的时候,发现配置错误,因此通过阅读源码的方式,探索Spring
正确集成Quartz
的方式.
问题发现
检查去年的项目代码,发现关于QuartzJobBean
的实现存在不合理的地方.
(1) 项目依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
</dependencies>
(2) 问题代码:
@Component
public class UnprocessedTaskJob extends QuartzJobBean {
private TaskMapper taskMapper;
@Autowired
public UnprocessedTaskJob(TaskMapper taskMapper){
this.taskMapper = taskMapper;
}
}
private JobDetail generateUnprocessedJobDetail(Task task) {
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(UnprocessedTaskJob.TASK_ID, task.getId());
return JobBuilder.newJob(UnprocessedTaskJob.class)
.withIdentity(UnprocessedTaskJob.UNPROCESSED_TASK_KEY_PREFIX + task.getId(), UnprocessedTaskJob.UNPROCESSED_TASK_JOB_GROUP)
.usingJobData(jobDataMap)
.storeDurably()
.build();
}
(3) 提炼问题:
以上代码存在错误的原因是,UnprocessedTaskJob
添加@Component
注解,表示其是Spring IOC
容器中的单例
类.
然而Quartz
在创建Job
是通过相应的Quartz Job Bean
的class
反射创建相应的Job
.也就是说,每次创建新的Job
时,都会生成相应的Job
实例.从而,这与UnprocessedTaskJob
是单例
相冲突.
查看代码提交记录,原因是当时认为不添加@Component
注解,则无法通过@Autowired
引入由Spring IOC
托管的taskMapper
实例,即无法实现依赖注入
.
然而令人感到奇怪的是,当我在开发环境去除了UnprocessedTaskJob
的@Component
注解之后,运行程序后发现TaskMapper
实例依然可以注入到Job
中,程序正常运行...
Spring托管Quartz
代码分析
网上搜索Spring
托管Quartz
的文章,大多数都是Spring MVC
项目,集中于如何解决在Job
实现类中通过@Autowired
实现Spring
的依赖注入
.
网上大多实现均依赖SpringBeanJobFactory
去实现Spring
与Quartz
的集成.
/**
* Subclass of {@link AdaptableJobFactory} that also supports Spring-style
* dependency injection on bean properties. This is essentially the direct
* equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz
* {@link org.quartz.spi.JobFactory}.
*
* <p>Applies scheduler context, job data map and trigger data map entries
* as bean property values. If no matching bean property is found, the entry
* is by default simply ignored. This is analogous to QuartzJobBean's behavior.
*
* <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
*
* @author Juergen Hoeller
* @since 2.0
* @see SchedulerFactoryBean#setJobFactory
* @see QuartzJobBean
*/
public class SpringBeanJobFactory extends AdaptableJobFactory
implements ApplicationContextAware, SchedulerContextAware {
}
/**
* {@link JobFactory} implementation that supports {@link java.lang.Runnable}
* objects as well as standard Quartz {@link org.quartz.Job} instances.
*
* <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
*
* @author Juergen Hoeller
* @since 2.0
* @see DelegatingJob
* @see #adaptJob(Object)
*/
public class AdaptableJobFactory implements JobFactory {
}
通过上述代码以及注释可以发现:
(1) AdaptableJobFactory
实现了JobFactory
接口,可以藉此创建标准的Quartz
实例(仅限于Quartz
2.1.4及以上版本);
(2) SpringBeanJobFactory
继承于AdaptableJobFactory
,从而实现对Quartz
封装实例的属性依赖注入.
(3) SpringBeanJobFactory
实现了ApplicationContextAware
以及SchedulerContextAware
接口(Quartz
任务调度上下文),因此可以在创建Job Bean
的时候注入ApplicationContex
以及SchedulerContext
.
Tips:
以上代码基于Spring
5.1.8版本.
在Spring 4.1.0
版本, SpringBeanJobFactory
的实现如以下代码所示:
public class SpringBeanJobFactory extends AdaptableJobFactory
implements SchedulerContextAware{
// 具体代码省略
}
因此,在早期的Spring
项目中,需要封装SpringBeanJobFactory
并实现ApplicationContextAware
接口(惊不惊喜?).
Spring老版本解决方案
基于老版本Spring
给出解决Spring
集成Quartz
解决方案.
解决方案由第三十九章:基于SpringBoot & Quartz完成定时任务分布式单节点持久化提供(大神的系列文章质量很棒).
@Configuration
public class QuartzConfiguration
{
/**
* 继承org.springframework.scheduling.quartz.SpringBeanJobFactory
* 实现任务实例化方式
*/
public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
/**
* 将job实例交给spring ioc托管
* 我们在job实例实现类内可以直接使用spring注入的调用被spring ioc管理的实例
* @param bundle
* @return
* @throws Exception
*/
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
/**
* 将job实例交付给spring ioc
*/
beanFactory.autowireBean(job);
return job;
}
}
/**
* 配置任务工厂实例
* @param applicationContext spring上下文实例
* @return
*/
@Bean
public JobFactory jobFactory(ApplicationContext applicationContext)
{
/**
* 采用自定义任务工厂 整合spring实例来完成构建任务
* see {@link AutowiringSpringBeanJobFactory}
*/
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
/**
* 配置任务调度器
* 使用项目数据源作为quartz数据源
* @param jobFactory 自定义配置任务工厂(其实就是AutowiringSpringBeanJobFactory)
* @param dataSource 数据源实例
* @return
* @throws Exception
*/
@Bean(destroyMethod = "destroy",autowire = Autowire.NO)
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception
{
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//将spring管理job自定义工厂交由调度器维护
schedulerFactoryBean.setJobFactory(jobFactory);
//设置覆盖已存在的任务
schedulerFactoryBean.setOverwriteExistingJobs(true);
//项目启动完成后,等待2秒后开始执行调度器初始化
schedulerFactoryBean.setStartupDelay(2);
//设置调度器自动运行
schedulerFactoryBean.setAutoStartup(true);
//设置数据源,使用与项目统一数据源
schedulerFactoryBean.setDataSource(dataSource);
//设置上下文spring bean name
schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
//设置配置文件位置
schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
return schedulerFactoryBean;
}
}
通过以上代码,就实现了由SpringBeanJobFactory
的createJobInstance
创建Job
实例,并将生成的Job
实例交付由AutowireCapableBeanFactory
来托管.
schedulerFactoryBean
则设置诸如JobFactory
(实际上是AutowiringSpringBeanJobFactory
,内部封装了applicationContext
)以及DataSource
(数据源,如果不设置,则Quartz
默认使用RamJobStore
).
RamJobStore
优点是运行速度快,缺点则是调度任务无法持久化保存.
因此,我们可以在定时任务内部使用Spring IOC
的@Autowired
等注解进行依赖注入
.
Spring新版本解决方案
(1) 解释
如果你使用Spring boot
,并且版本好大于2.0
,则推荐使用spring-boot-starter-quartz
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
Auto-configuration support is now include for the Quartz Scheduler. We’ve also added a new spring-boot-starter-quartz starter POM.
You can use in-memory JobStores, or a full JDBC-based store. All JobDetail, Calendar and Trigger beans from your Spring application context will be automatically registered with the Scheduler.
For more details read the new “Quartz Scheduler” section of the reference documentation.
以上是spring-boot-starter-quartz
的介绍,基于介绍可知,如果你没有关闭Quartz
的自动配置,则SpringBoot
会帮助你完成Scheduler
的自动化配置,诸如JobDetail
/Calendar
/Trigger
等Bean
会被自动注册至Shceduler
中.你可以在QuartzJobBean
中自由的使用@Autowired
等依赖注入
注解.
其实,不引入spring-boot-starter-quartz
,而仅仅导入org.quartz-scheduler
,Quartz
的自动化配置依然会起效(这就是第一节问题分析中,去除@Bean
注解,程序依然正常运行原因,悲剧中万幸).
(2) 代码分析
/**
* {@link EnableAutoConfiguration Auto-configuration} for Quartz Scheduler.
*
* @author Vedran Pavic
* @author Stephane Nicoll
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass({ Scheduler.class, SchedulerFactoryBean.class, PlatformTransactionManager.class })
@EnableConfigurationProperties(QuartzProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
public class QuartzAutoConfiguration{
// 此处省略部分代码
@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean quartzScheduler() {
// 因为新版本SchedulerFactoryBean已经实现ApplicationContextAware接口
// 因此相对于老版本Spring解决方案中的AutowiringSpringBeanJobFactory进行封装
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
SpringBeanJobFactory jobFactory = new SpringBeanJobFactory();
// SpringBeanJobFactory中注入applicationContext,为依赖注入创造条件
jobFactory.setApplicationContext(this.applicationContext);
// schedulerFactoryBean中注入setJobFactory(注意此处没有配置DataSource,DataSource详见`JdbcStoreTypeConfiguration`)
// 以上这几个步骤,与老版本的Spring解决方案类似
schedulerFactoryBean.setJobFactory(jobFactory);
// 后续都是Quartz的配置属性设置,不再叙述
if (this.properties.getSchedulerName() != null) {
schedulerFactoryBean.setSchedulerName(this.properties.getSchedulerName());
}
schedulerFactoryBean.setAutoStartup(this.properties.isAutoStartup());schedulerFactoryBean.setStartupDelay((int) this.properties.getStartupDelay().getSeconds());
schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(this.properties.isWaitForJobsToCompleteOnShutdown());
schedulerFactoryBean.setOverwriteExistingJobs(this.properties.isOverwriteExistingJobs());
if (!this.properties.getProperties().isEmpty()) {
schedulerFactoryBean.setQuartzProperties(asProperties(this.properties.getProperties()));
}
if (this.jobDetails != null && this.jobDetails.length > 0) {
schedulerFactoryBean.setJobDetails(this.jobDetails);
}
if (this.calendars != null && !this.calendars.isEmpty()) {
schedulerFactoryBean.setCalendars(this.calendars);
}
if (this.triggers != null && this.triggers.length > 0) {
schedulerFactoryBean.setTriggers(this.triggers);
}
customize(schedulerFactoryBean);
return schedulerFactoryBean;
}
@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
protected static class JdbcStoreTypeConfiguration {
// 为Quartz的持久化配置DataSource,具体代码可以翻阅Spring源码得到
}
}
下面对SpringBeanJobFactory
进行分析,它是生成Job
实例,以及进行依赖注入
操作的关键类.
/**
* Subclass of {@link AdaptableJobFactory} that also supports Spring-style
* dependency injection on bean properties. This is essentially the direct
* equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz
* {@link org.quartz.spi.JobFactory}.
*
* <p>Applies scheduler context, job data map and trigger data map entries
* as bean property values. If no matching bean property is found, the entry
* is by default simply ignored. This is analogous to QuartzJobBean's behavior.
*
* <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
*
* @author Juergen Hoeller
* @since 2.0
* @see SchedulerFactoryBean#setJobFactory
* @see QuartzJobBean
*/
public class SpringBeanJobFactory extends AdaptableJobFactory
implements ApplicationContextAware, SchedulerContextAware {
@Nullable
private String[] ignoredUnknownProperties;
@Nullable
private ApplicationContext applicationContext;
@Nullable
private SchedulerContext schedulerContext;
/**
* Specify the unknown properties (not found in the bean) that should be ignored.
* <p>Default is {@code null}, indicating that all unknown properties
* should be ignored. Specify an empty array to throw an exception in case
* of any unknown properties, or a list of property names that should be
* ignored if there is no corresponding property found on the particular
* job class (all other unknown properties will still trigger an exception).
*/
public void setIgnoredUnknownProperties(String... ignoredUnknownProperties) {
this.ignoredUnknownProperties = ignoredUnknownProperties;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void setSchedulerContext(SchedulerContext schedulerContext) {
this.schedulerContext = schedulerContext;
}
/**
* Create the job instance, populating it with property values taken
* from the scheduler context, job data map and trigger data map.
*/
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 创建Job实例
// (1) 包含applicationContext,则通过AutowireCapableBeanFactory()创建相应Job实例,实现依赖注入
// (2) 如果applicationContext为空,则使用AdaptableJobFactory创建相应的Bean(无法实现依赖注入)
Object job = (this.applicationContext != null ?
this.applicationContext.getAutowireCapableBeanFactory().createBean(
bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) :
super.createJobInstance(bundle));
if (isEligibleForPropertyPopulation(job)) {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
MutablePropertyValues pvs = new MutablePropertyValues();
if (this.schedulerContext != null) {
pvs.addPropertyValues(this.schedulerContext);
}
pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
if (this.ignoredUnknownProperties != null) {
for (String propName : this.ignoredUnknownProperties) {
if (pvs.contains(propName) && !bw.isWritableProperty(propName)) {
pvs.removePropertyValue(propName);
}
}
bw.setPropertyValues(pvs);
}
else {
bw.setPropertyValues(pvs, true);
}
}
return job;
}
// 省略部分代码
}
/**
* {@link JobFactory} implementation that supports {@link java.lang.Runnable}
* objects as well as standard Quartz {@link org.quartz.Job} instances.
*
* <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
*
* @author Juergen Hoeller
* @since 2.0
* @see DelegatingJob
* @see #adaptJob(Object)
*/
public class AdaptableJobFactory implements JobFactory {
/**
* Create an instance of the specified job class.
* <p>Can be overridden to post-process the job instance.
* @param bundle the TriggerFiredBundle from which the JobDetail
* and other info relating to the trigger firing can be obtained
* @return the job instance
* @throws Exception if job instantiation failed
*/
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 获取`QuartzJobBean`的实现`class`,通过反射工具创建相应的类实例(自然无法注入Spring托管的Bean实例)
Class<?> jobClass = bundle.getJobDetail().getJobClass();
return ReflectionUtils.accessibleConstructor(jobClass).newInstance();
}
}
此处需要解释下AutowireCapableBeanFactory
的作用.
项目中,有部分实现并未与Spring
深度集成,因此其实例并未被Spring
容器管理.
然而,出于需要,这些并未被Spring
管理的Bean
需要引入Spring
容器中的Bean
.
此时,就需要通过实现AutowireCapableBeanFactory
,从而让Spring
实现依赖注入等功能.
希望能够通过上述解释以及代码分析,让你知晓如何在老版本以及新版本Spring
中正确集成Quartz
.
此外,Spring boot
的自动化配置能够解决绝大多数配置问题,但是在时间充裕的情况下,建议通过阅读源码等方式了解配置细节
,从而做到更加的胸有成竹.
PS:
如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!