品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:SpringBoot和Spring到底有沒有本質的不同?
品Spring:SpringBoot輕鬆取勝bean定義註冊的“第一階段”
品Spring:SpringBoot發起bean定義註冊的“二次攻堅戰”
品Spring:註解之王@Configuration和它的一眾“小弟們”
品Spring:對@PostConstruct和@PreDestroy註解的處理方法
品Spring:對@Autowired和@Value註解的處理方法
品Spring:真沒想到,三十步才能完成一個bean實例的創建
>>> 熱門文章集錦 <<<
爸爸又給Spring MVC生了個弟弟叫Spring WebFlux
【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)
【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)
【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)
【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)
作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!