背景
起初這個(gè)問(wèn)題是同事在項(xiàng)目中發(fā)現(xiàn)的公给,搜索引擎了一下也就解決了這個(gè)問(wèn)題基协,但是我同事比較好學(xué)嗅蔬,解決這個(gè)Bug
的帖子很多,但是卻沒(méi)有提及為什么會(huì)出現(xiàn)此情況疾就,遂問(wèn)我能不能幫他看看原因澜术,那我當(dāng)然就義不容辭擼了一下源碼。
分析問(wèn)題
先看一眼報(bào)錯(cuò)吧:
2020-12-08 18:50:09.535 [main] ERROR org.springframework.boot.SpringApplication [line:837] - Application run failed
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'defaultSockJsTaskScheduler' is expected to be of type 'org.springframework.scheduling.TaskScheduler' but was actually of type 'org.springframework.beans.factory.support.NullBean'
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:399)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:227)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1175)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1142)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.resolveSchedulerBean(ScheduledAnnotationBeanPostProcessor.java:327)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:256)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:233)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:105)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:404)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:361)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:898)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:554)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
首先我們從報(bào)錯(cuò)入手猬腰,BeanNotOfRequiredTypeException
顯然是Bean
的類(lèi)型不對(duì)鸟废,然后我們?cè)倬唧w看名稱(chēng)叫defaultSockJsTaskScheduler
的Bean
類(lèi)型不對(duì),那搜索大法姑荷,先分別從@EnableWebSocket
和@EnableScheduling
點(diǎn)進(jìn)行盒延,把這兩個(gè)包的源碼下載了再說(shuō)缩擂,然后全局搜索defaultSockJsTaskScheduler
,這時(shí)候找定義叫defaultSockJsTaskScheduler
的添寺,很容易定位到org.springframework.web.socket.config.annotation.WebSocketConfigurationSupport#defaultSockJsTaskScheduler
胯盯,它的定義如下:
@Bean
@Nullable
public TaskScheduler defaultSockJsTaskScheduler() {
if (initHandlerRegistry().requiresTaskScheduler()) {
ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
threadPoolScheduler.setThreadNamePrefix("SockJS-");
threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
threadPoolScheduler.setRemoveOnCancelPolicy(true);
this.scheduler = threadPoolScheduler;
}
return this.scheduler;
}
這里的邏輯具體分析一下會(huì)知道,如果if
條件不滿足计露,這里將返回null
博脑,也就是會(huì)創(chuàng)建一個(gè)NullBean
在容器中作為TaskScheduler
。
我們找到了NullBean
產(chǎn)生的地方票罐,那么為什么單獨(dú)使用兩個(gè)包都不會(huì)有問(wèn)題呢叉趣,只能解釋?zhuān)?dāng)單獨(dú)使用spring-boot-starter-websocket
時(shí),即使創(chuàng)建了NullBean
但是并不會(huì)被使用该押,而增加spring-boot-starter-quartz
的過(guò)程肯定使用到了TaskScheduler
疗杉。而單獨(dú)使用spring-boot-starter-quartz
又沒(méi)有問(wèn)題,所以肯定還有地方實(shí)例化了TaskScheduler
蚕礼,那么進(jìn)入TaskScheduler
烟具,然后看看那些地方用到了它:
)
很容易猜到TaskSchedulingAutoConfiguration
,我們進(jìn)去看到了如下:
@Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
單獨(dú)有spring-boot-starter-websocket
時(shí)闻牡,會(huì)創(chuàng)建defaultSockJsTaskScheduler
净赴,但是沒(méi)地方用到,所以不會(huì)有問(wèn)題罩润,而引入spring-boot-starter-quartz
時(shí)玖翅,由于上面產(chǎn)生的NullBean
導(dǎo)致這個(gè)條件裝配失效,所以不會(huì)產(chǎn)生合適的TaskScheduler
實(shí)例割以。由于@EnableScheduling
的存在金度,導(dǎo)致org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#finishRegistration
會(huì)被執(zhí)行到,為啥不用細(xì)說(shuō)了吧(不明白可以截圖找我問(wèn))严沥,此時(shí)該方法中會(huì)執(zhí)行this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
代碼猜极,這里的resolveSchedulerBean
將會(huì)去容器中找TaskScheduler
的實(shí)例,通過(guò)名稱(chēng)找到了defaultSockJsTaskScheduler
消玄,但是他的類(lèi)型卻是個(gè)NullBean
此時(shí)就會(huì)觸發(fā)上述錯(cuò)誤跟伏。
這便是導(dǎo)致這個(gè)Bug
的原因了,有些人可能疑惑翩瓜,你上面那些類(lèi)咋知道的受扳,emmmm
,找這種問(wèn)題一般步驟就是猜想兔跌、驗(yàn)證勘高、整理。所以先猜,再Debug
驗(yàn)證华望。
解決問(wèn)題
那既然知道了為什么會(huì)出現(xiàn)這個(gè)錯(cuò)了蕊蝗,就容易修復(fù)了,就是因?yàn)榇嬖诹艘粋€(gè)NullBean
類(lèi)型的TaskScheduler
實(shí)例赖舟,在查找的時(shí)候出現(xiàn)了該錯(cuò)誤蓬戚,那么我們只需要自己創(chuàng)建一個(gè)正確類(lèi)型就好了:
@Bean
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
當(dāng)然,根據(jù)前面提到的if
條件不滿足才會(huì)出現(xiàn)NullBean
建蹄,那么自然還有種方案碌更,那就是讓if
條件滿足,即實(shí)現(xiàn)一個(gè)WebSocketConfigurer
也可以解決這個(gè)問(wèn)題(看一下if
附近的注釋?zhuān)瑢?shí)現(xiàn)應(yīng)該不難洞慎,不給案例了)痛单。
可以可以,問(wèn)題解決劲腿,愉快的告訴同事咋回事就完事了旭绒。
一點(diǎn)思考
然鵝,此時(shí)另一個(gè)念頭冒出來(lái)了焦人。這不應(yīng)該是Spring Framework
的Bug
嗎挥吵,雖然在幾個(gè)使用TaskScheduler
的地方都說(shuō)明可以為null
,但是可以為null
和創(chuàng)建了一個(gè)NullBean
類(lèi)型作為TaskScheduler
的實(shí)例花椭,并導(dǎo)致了真正在使用的時(shí)候產(chǎn)生了錯(cuò)誤忽匈,這妥妥的Spring Framework
的Bug
啊。那為啥明明是Bug
矿辽,網(wǎng)上也有不少關(guān)于這個(gè)錯(cuò)誤的處理方案丹允,那么官方卻沒(méi)有解決呢,看了一下版本袋倔,這個(gè)問(wèn)題存在好些個(gè)版本了雕蔽,難道沒(méi)人提過(guò)issue
,看了一眼好像還真是宾娜。那E!前塔!提OА!华弓!
說(shuō)干就干食零,啪提個(gè)issue,官方回復(fù)很快该抒,經(jīng)過(guò)討論后慌洪,據(jù)說(shuō)會(huì)在5.3.3
處理該問(wèn)題,比較曲折的是凑保,一開(kāi)始提在Spring Framework
下冈爹,后來(lái)被遷到了Spring Boot
下,然后討論完又認(rèn)為是Spring Framework
的問(wèn)題欧引,所以處理者又重新開(kāi)了個(gè)新的issue频伤。
結(jié)語(yǔ)
- 找問(wèn)題要多猜想驗(yàn)證
- 敢于質(zhì)疑,不論對(duì)于
Spring Framework
還是對(duì)于其他大佬芝此,不能一味相信憋肖,要有自己的判斷力
好了,就這樣吧婚苹,又水一篇文章岸更,唉~