周五檢查以前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:
如果您覺得我的文章對您有幫助,請關注我的微信公眾號,謝謝!