品Spring:關於@Scheduled定時任務的思考與探索,結果尷尬了

  • 2019 年 10 月 11 日
  • 筆記


非Spring風格的代碼與Spring的結合



現在的開發都是基於Spring的,所有的依賴都有Spring管理,這沒有問題。

但是要突然寫一些非Spring風格的代碼時,可能會很不習慣,如果還要和Spring風格的代碼結合起來的話,就會稍顯麻煩。

因為非Spring風格的代碼不由Spring管理,所以Spring不會給我們注入依賴,相反,我們要自己去Spring中拿取依賴。

所以首先目標就是要獲取Spring容器,即ApplicationContext,方法通常如下圖01:

 

定義一個類實現ApplicationContextAware,類中定義一個靜態的ApplicationContext字段,Spring會把容器注入到這個靜態字段。

由於類的靜態字段在JVM中一直存在,這樣ApplicationContextUtils這個類就可以在非Spring風格的代碼里使用Spring管理的bean了。


若用於定時任務是否有潛在的問題


Spring自帶的定時任務,非常好用,而且我很早就用過,具體時間已經記不清了。

我依稀記得以前好像覺得容器還沒有啟動完成時,定時任務就有可能被觸發。就姑且認為是這樣吧,當然也可能不是。

如果我的定時任務運行的代碼是非Spring風格的,我自然需要自己去new實例,如下圖02:

 

如果這個非Spring風格的代碼恰好又要使用Spring管理的bean,那就是剛剛上面提到的方式,如下圖03:

 

可以看到SimpleService是Spring管理的bean,SimpleTask卻不是,所以只能在構造方法里使用容器的getBean方式獲取。

這種方式通常是沒有問題的,我們也都是這樣用的。但是要把它放到定時任務里呢?

會不會出現定時任務觸發的較早,此時ApplicationContextUtils類里的靜態字段ApplicationContext還沒有被注入呢?

如果真這樣的話,那可就空指針了。那到底會不會這樣呢,一起來探索發現下吧。


探索與發現,沒有頻道


我比較認同這個觀點:

當一個人什麼都不知道的時候,他覺得自己什麼都懂,老想出來指點江山。

當一個人隨着學習知道的越多,他發現自己懂的越少,反而不敢隨便亂說。

其實就是,知道的越多,問題就越多,隨之而來的困惑也就越多。

我寫完16篇《品Spring》文章,知道了bean定義註冊的順序、bean實例化的順序、bean後處理器應用的順序都和本文描述的問題有關。

所以我也不敢冒然亂說,只能逐步測試逼近答案,這就是典型的知道的“太多了”的煩惱,哈哈。

其實本文的問題就是一個先後順序的問題,如果定時任務先觸發就會產生空指針,如果靜態字段先注入,就不會有空指針。

而我選擇了相信有空指針,完全是吃瓜群眾幸災樂禍的心理,嘻嘻。

所以我就想辦法安排空指針的出現。甚至“處心積慮”,直至使出渾身解數。

一、讓定時任務的bean定義註冊早於ApplicationContextUtils

拋開依賴不說,實例化的順序就是bean定義註冊的順序。

bean定義註冊的順序怎麼確定呢?單就掃描jar包而言,就是包名和類名的字母順序。

因此,我的安排如下圖04:

 

最終bean定義的順序符合預期,如下圖05:

 

這說明bean實例化的順序是,先實例化定時任務,再實例化ApplicationContextUtils。保證了定時任務在前。

遺憾的是,我安排的空指針沒有出現,一切是正常的,定時任務中可以獲取到靜態字段的值。

二、讓定時任務以最快的速度觸發

因為這兩個bean定義是挨着的,所以實例化也是挨着的。會不會是實例化執行的太快了?

由於實例化的速度無法控制,所以就加快定時任務的觸發速度,試試看。

改成1秒就觸發,如下圖06:

 

哎,遺憾的是還是一切正常,我太想看到報錯了,哈哈,繼續使“陰招”。

三、讓ApplicationContextUtils的實例化過程卡住

定時任務肯定先實例化好,然後才會去實例化ApplicationContextUtils。

這次想辦法讓後者卡住,這樣定時任務該先執行了吧。小樣,我還治不了你啦。

因為定時任務是在單獨的線程池中執行,所以讓主線程睡一會即可,如下圖07:

 

主線程確實卡住了,遺憾的是還是一切正常。


深入虎穴,不為虎子


ApplicationContext的注入和定時任務的處理都是由bean後處理器完成的。

所以把容器中的後處理器都輸出來看看,如下圖08:

 

可以看到共有12個,顯示的順序就是它們被應用的順序。

也就是說對於每一個bean實例的創建,都會應用這12個,且按如圖順序應用。

只不過每個bean後處理器只處理自己關注的bean,對於不關注的不起作用而已。

而且這12個的順序只對單個bean有意義,對於不同的bean,沒有意義。

因為在測試時,我發現每次必須等容器啟動好後,定時任務才開始執行。

所以只能去看處理定時任務的bean後處理器源碼了,即ScheduledAnnotationBeanPostProcessor這個類。

於是就從上往下看源碼,當看到這個方法後,我似乎明白了,如下圖09:

 

這是一個事件的回調方法,參數是ContextRefreshed事件對象,說明在容器啟動完成後會調用這個方法。

再看看方法體,就一句代碼,finishRegistration,完成註冊,說明在容器沒有啟動好之前,這個註冊是不會完成的。

其實已經表達的很清楚了,只有在容器啟動完成後,定時任務才會完成註冊,才會開始被調度。

然後再看看完成註冊方法,它的最後一句代碼如下圖10:

 

這個方法名很親切吧,就是和初始化相關的。

然後再進到這個方法里看看,如下圖11:

 

也只有一句代碼,就是調度任務。哦,現在才開始調度。之前的只是註冊任務,並沒有調度。


慘遭打臉?其實並沒有


我仔細看了幾遍源碼,發現寫的很有特點,既支持容器啟動好後觸發定時任務,也支持容器啟動過程中的及時觸發。

只不過現在默認是前者而已。所以我懷疑以前可能就是及時觸發,後來可能覺得不太合適,就進行了改造,成了現在這樣子。

這既是探索與發現精神,也是好奇精神,就是它促使了我們向前發展,去了解更多的未知領域。

>>> 品Spring系列文章 <<<

 

品Spring:帝國的基石

品Spring:bean定義上梁山

品Spring:實現bean定義時採用的“先進生產力”

品Spring:註解終於“成功上位”

品Spring:能工巧匠們對註解的“加持”

品Spring:SpringBoot和Spring到底有沒有本質的不同?

品Spring:負責bean定義註冊的兩個“排頭兵”

品Spring:SpringBoot輕鬆取勝bean定義註冊的“第一階段”

品Spring:SpringBoot發起bean定義註冊的“二次攻堅戰”

品Spring:註解之王@Configuration和它的一眾“小弟們”

品Spring:bean工廠後處理器的調用規則

品Spring:詳細解說bean後處理器

品Spring:對@PostConstruct和@PreDestroy註解的處理方法

品Spring:對@Resource註解的處理方法

品Spring:對@Autowired和@Value註解的處理方法

品Spring:真沒想到,三十步才能完成一個bean實例的創建

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)

【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!