问题背景
项目上使用的springboot版本是2.1.1.RELEASE
,现在因为要接入elasticsearch7.x版本,参考官方文档要求,需要将springboot版本升级到2.5.14
。
本以为是改一下版本号的事,但是升级之后发现服务启动报错了😱。
问题原因
Caused by: org.quartz.SchedulerConfigException: DataSource name not set.
从错误信息中可以看出,报错与quartz
有关,我们先来顺着异常栈看一下,可以看到是JobStoreSupport.initialize()
方法中抛出的错误:
public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException { if (dsName == null) { throw new SchedulerConfigException("DataSource name not set."); } ... }
向上溯源可以发现,其初始调用方是这里:
那么这个js
对象是什么呢?它是一个JobStore
实例,而JobStore
其实是一个接口,它有多个实现类:
我们先来打断点看看,这里实际使用到的是哪个类的实例,先将springboot版本改回到2.1.1.RELEASE
看看,可以看到使用的是LocalDataSourceJobStore
,而当我们使用springboot2.5.14
版本时,这里使用的是JobStoreTX
是实例化对象。
那么,是什么原因导致两个版本使用了不同的JobStore
实现类呢?先来看看js
对象是如何初始化的:首先获取org.quartz.jobStore.class
属性值,然后通过反射实例化js
对象。
显然,在不同版本下,这里获取到的org.quartz.jobStore.class
属性值不一致导致了创建了不同的JobStore
实现类。
接下来我们看一下项目中与quartz
相关的配置,如下代码所示,是一个SchedulerFactoryBean
的初始化操作 ,其中设置org.quartz.jobStore.class
属性值为org.quartz.impl.jdbcjobstore.JobStoreTX
,但是从上面分析中我们知道,在真正创建JobStore
实现类时,这个属性值已经发生了变化,由此说明这个值在后期被更改过。
@Configuration public class ScheduleConfig { @Bean public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setDataSource(dataSource); Properties prop = new Properties(); prop.put("org.quartz.scheduler.instanceName", "MyScheduler"); prop.put("org.quartz.scheduler.instanceId", "AUTO"); prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); prop.put("org.quartz.threadPool.threadCount", "20"); prop.put("org.quartz.threadPool.threadPriority", "5"); // 这里设置org.quartz.jobStore.class属性值为org.quartz.impl.jdbcjobstore.JobStoreTX prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); prop.put("org.quartz.jobStore.isClustered", "true"); prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); prop.put("org.quartz.jobStore.misfireThreshold", "12000"); prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); factory.setQuartzProperties(prop); factory.setSchedulerName("MyScheduler"); factory.setStartupDelay(1); factory.setApplicationContextSchedulerContextKey("applicationContext"); factory.setOverwriteExistingJobs(true); factory.setAutoStartup(true); return factory; } }
而我们从异常栈中可以发现,SchedulerFactoryBean
在完成初始化操作之后,执行了afterPropertiesSet()
方法,先来看一下这个方法中做了哪些事情:
public void afterPropertiesSet() throws Exception { if (this.dataSource == null && this.nonTransactionalDataSource != null) { this.dataSource = this.nonTransactionalDataSource; } if (this.applicationContext != null && this.resourceLoader == null) { this.resourceLoader = this.applicationContext; } // Initialize the Scheduler instance... // 先来看看this.prepareSchedulerFactory()方法中做了什么 this.scheduler = this.prepareScheduler(this.prepareSchedulerFactory()); try { this.registerListeners(); this.registerJobsAndTriggers(); } catch (Exception var4) { try { this.scheduler.shutdown(true); } catch (Exception var3) { this.logger.debug("Scheduler shutdown exception after registration failure", var3); } throw var4; } } /** * Create a SchedulerFactory if necessary and apply locally defined Quartz properties to it. * @return the initialized SchedulerFactory */ private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException { SchedulerFactory schedulerFactory = this.schedulerFactory; if (schedulerFactory == null) { // Create local SchedulerFactory instance (typically a StdSchedulerFactory) schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass); if (schedulerFactory instanceof StdSchedulerFactory) { // 重点来了,这里有一个SchedulerFactory初始化方法 initSchedulerFactory((StdSchedulerFactory) schedulerFactory); } else if (this.configLocation != null || this.quartzProperties != null || this.taskExecutor != null || this.dataSource != null) { throw new IllegalArgumentException( "StdSchedulerFactory required for applying Quartz properties: " + schedulerFactory); } // Otherwise, no local settings to be applied via StdSchedulerFactory.initialize(Properties) } // Otherwise, assume that externally provided factory has been initialized with appropriate settings return schedulerFactory; }
接下来我们进入initSchedulerFactory()
方法内部看看具体都有哪些逻辑,这是一个SchedulerFactory
初始化方法,它会应用Quartz
的一些本地配置属性用于SchedulerFactory
初始化:
/** * Initialize the given SchedulerFactory, applying locally defined Quartz properties to it. * @param schedulerFactory the SchedulerFactory to initialize */ private void initSchedulerFactory(StdSchedulerFactory schedulerFactory) throws SchedulerException, IOException { Properties mergedProps = new Properties(); if (this.resourceLoader != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, ResourceLoaderClassLoadHelper.class.getName()); } if (this.taskExecutor != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, LocalTaskExecutorThreadPool.class.getName()); } else { // Set necessary default properties here, as Quartz will not apply // its default configuration when explicitly given properties. mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName()); mergedProps.setProperty(PROP_THREAD_COUNT, Integer.toString(DEFAULT_THREAD_COUNT)); } if (this.configLocation != null) { if (logger.isDebugEnabled()) { logger.debug("Loading Quartz config from [" + this.configLocation + "]"); } PropertiesLoaderUtils.fillProperties(mergedProps, this.configLocation); } CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps); if (this.dataSource != null) { // 重点来了,如果未设置"org.quartz.jobStore.class"属性,就将其设置为"org.springframework.scheduling.quartz.LocalDataSourceJobStore" mergedProps.putIfAbsent(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); } // Determine scheduler name across local settings and Quartz properties... if (this.schedulerName != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.schedulerName); } else { String nameProp = mergedProps.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME); if (nameProp != null) { this.schedulerName = nameProp; } else if (this.beanName != null) { mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.beanName); this.schedulerName = this.beanName; } } schedulerFactory.initialize(mergedProps); }
也就是说,当我们配置了org.quartz.jobStore.class
属性时,在springboot2.5.14
版本中会以我们代码中配置的为准,也就是org.quartz.impl.jdbcjobstore.JobStoreTX
。
再来看一下springboot2.1.1.RELEASE
版本中此处的逻辑,它会直接把org.quartz.jobStore.class
属性值设置为org.springframework.scheduling.quartz.LocalDataSourceJobStore
,不关心之前有没有设置过该值。
解决方案
明白了问题的症结所在,解决起来就相当容易了,两种方式:
- 去掉
ScheduleConfig
配置类中SchedulerFactoryBean
对象的org.quartz.jobStore.class
属性配置,交由SchedulerFactoryBean#initSchedulerFactor
去设置。 - 直接将
ScheduleConfig
配置类中SchedulerFactoryBean
对象的org.quartz.jobStore.class
属性值设置为LocalDataSourceJobStore.class.getName()
。
再次启动服务,大功告成✌️。