你好呀,我是歪歪。
前兩天在看 SOFABoot 的時(shí)候,看到一個(gè)讓我眼前一亮的東西,來(lái)給大家盤(pán)一下。
SOFABoot,你可能不眼熟,但是沒(méi)關(guān)系,本文也不是給你講這個(gè)東西的,你就認(rèn)為它是 SpringBoot 的變種就行了。
(資料圖片僅供參考)
因?yàn)橛形浵伣鸱硶?shū),所以主要是一些金融類的公司在使用這個(gè)框架:
官方介紹是這樣的:
SOFABoot 是螞蟻金服開(kāi)源的基于 Spring Boot 的研發(fā)框架,它在 Spring Boot 的基礎(chǔ)上,提供了諸如 Readiness Check,類隔離,日志空間隔離等能力。在增強(qiáng)了 Spring Boot 的同時(shí),SOFABoot 提供了讓用戶可以在 Spring Boot 中非常方便地使用 SOFA 中間件的能力。
上面這些功能都很強(qiáng)大,但是我主要是分享一下它的這個(gè)小功能:https://help.aliyun.com/document_detail/133162.html
這個(gè)功能可以讓 Bean 的初始化方法在異步線程里面執(zhí)行,從而加快 Spring 上下文加載過(guò)程,提高應(yīng)用啟動(dòng)速度。
為什么看到功能的時(shí)候,我眼前一亮呢,因?yàn)槲液芫弥皩?xiě)過(guò)這篇文章《我是真沒(méi)想到,這個(gè)面試題居然從11年前就開(kāi)始討論了,而官方今年才表態(tài)。》
里面提到的面試題是這樣的:
Spring 在啟動(dòng)期間會(huì)做類掃描,以單例模式放入 ioc。但是 spring 只是一個(gè)個(gè)類進(jìn)行處理,如果為了加速,我們?nèi)∠?spring 自帶的類掃描功能,用寫(xiě)代碼的多線程方式并行進(jìn)行處理,這種方案可行嗎?為什么?
當(dāng)時(shí)通過(guò) issue 找到了官方對(duì)于這個(gè)問(wèn)題回復(fù)總結(jié)起來(lái)就是:應(yīng)該是先找到啟動(dòng)慢的根本原因,而不是把問(wèn)題甩鍋給 Spring。這部分對(duì)于 Spring 來(lái)說(shuō),能不動(dòng),就別動(dòng)。
僅從“啟動(dòng)加速-異步初始化方法”這個(gè)標(biāo)題上來(lái)看,Spring 官方不支持的東西 SOFABoot 支持了。所以這玩意讓我眼前一亮,我倒要看看你是怎么搞得。
先說(shuō)結(jié)論:SOFABoot 的方案能從一定程度上解決問(wèn)題,但是它依賴于我們編碼的時(shí)候指定哪些 Bean 是可以異步初始化的,這樣帶來(lái)的好處是不必考慮循環(huán)依賴、依賴注入等等各種復(fù)雜的情況了,壞處就是需要程序員自己去識(shí)別哪些類是可以異步初始化的。
我倒是覺(jué)得,程序員本來(lái)就應(yīng)該具備“識(shí)別自己的項(xiàng)目中哪些類是可以異步初始化”的能力。
但是,一旦要求程序員來(lái)主動(dòng)去識(shí)別了,就已經(jīng)“輸了”,已經(jīng)不夠驚艷了,在實(shí)現(xiàn)難度上就不是一個(gè)級(jí)別的事情了。人家 Spring 想的可是框架給你全部搞定,頂多給你留一個(gè)開(kāi)關(guān),你開(kāi)箱即用,啥都不用管。
但是總的來(lái)說(shuō),作為一次思路演變?yōu)樵创a的學(xué)習(xí)案例來(lái)說(shuō),還是很不錯(cuò)的。
我們主要是看實(shí)現(xiàn)方案和具體邏輯代碼,以 SOFABoot 為抓手,針對(duì)其“異步初始化方法”聚焦下鉆,把源碼當(dāng)做紐帶,協(xié)同 Spring,打出一套“我看到了->我會(huì)用了->我拿過(guò)來(lái)->我看懂了->是我的了->寫(xiě)進(jìn)簡(jiǎn)歷”的組合拳。
Demo先搞個(gè) Demo 出來(lái),演示一波效果,先讓你直觀的看到這是個(gè)啥玩意。
這個(gè) Demo 非常之簡(jiǎn)單,幾行代碼就搞定。
先搞兩個(gè) java 類,里面有一個(gè) init 方法:
圖片
然后把他們作為 Bean 交給 Spring 管理,Demo 就搭建好了:
圖片
直接啟動(dòng)項(xiàng)目,啟動(dòng)時(shí)間只需要 1.152s,非常絲滑:
圖片
然后,注意,我要稍微的變一下形。
在注入 Bean 的時(shí)候觸發(fā)一下初始化方法,模擬實(shí)際項(xiàng)目中在 Bean 的初始化階段,既在 Spring 項(xiàng)目啟動(dòng)過(guò)程中,做一些數(shù)據(jù)準(zhǔn)備、配置拉取等相關(guān)操作:
圖片
再次重啟一下項(xiàng)目,因?yàn)樾枰獔?zhí)行兩個(gè) Bean 的初始化動(dòng)作,各需要 5s 時(shí)間,而且是串行執(zhí)行,所以啟動(dòng)時(shí)間直接來(lái)到了 11.188s:
圖片
那么接下來(lái),就是見(jiàn)證奇跡的時(shí)刻了。
我加上 @SofaAsyncInit 這樣的一個(gè)注解:
圖片
你先別管這個(gè)注解是哪里來(lái)的,從這個(gè)注解的名稱你也知道它是干啥的:異步執(zhí)行初始化。
這個(gè)時(shí)候我再啟動(dòng)項(xiàng)目:
圖片
從日志中可以看到:
whyBean 和 maxBean 的 init 方法是由兩個(gè)不同的線程并行執(zhí)行的。啟動(dòng)時(shí)間縮短到了 6.049s。所以 @SofaAsyncInit 這個(gè)注解實(shí)現(xiàn)了“指定 Bean 的初始化方法實(shí)現(xiàn)異步化”。
你想想,如果你有 10 個(gè) Bean,每個(gè) Bean 都需要 1s 的時(shí)間做初始化,總計(jì) 10s。
但是這些 Bean 之間其實(shí)不需要串行初始化,那么用這個(gè)注解,并行只需要 1s,搞定。
到這里,你算是看到了這樣的東西存在,屬于“我看到了”。
接下來(lái),我們進(jìn)入到“我會(huì)用了”這個(gè)環(huán)節(jié)。
怎么來(lái)的。在解讀原理之前,我還得告訴你這個(gè)注解到底是怎么來(lái)的。
它屬于 SOFABoot 框架里面的注解,首先你得把你的 SpringBoot 修改為 SOFABoot。
這一步參照官方文檔中的“快速開(kāi)始”部分,非常的簡(jiǎn)單:https://www.sofastack.tech/projects/sofa-boot/quick-start/
第一步就是把項(xiàng)目中 pom.xml 中的:
org.springframework.boot spring-boot-starter-parent ${spring.boot.version}
替換為:
com.alipay.sofa sofaboot-dependencies ${sofa.boot.version}
這里的 ${sofa.boot.version} 指定具體的 SOFABoot 版本,我這里使用的是最新的 3.18.0 版本。
然后我們要使用 @SofaAsyncInit 注解,所以需要引入以下 maven:
com.alipay.sofa runtime-sofa-boot-starter
對(duì)于 pom.xml 文件的變化,就只有這么一點(diǎn):
圖片
最后,在工程的 application.properties 文件下添加 SOFABoot 工程一個(gè)必須的參數(shù)配置,spring.application.name,用于標(biāo)示當(dāng)前應(yīng)用的名稱
# Application Namespring.application.name=SOFABoot Demo
就搞定了,我就完成了一個(gè)從 SpringBoot 切換為 SOFABoot 這個(gè)大動(dòng)作。
當(dāng)然了,我這個(gè)是一個(gè) Demo 項(xiàng)目,結(jié)構(gòu)和 pom 依賴都非常簡(jiǎn)單,所以切換起來(lái)也非常容易。如果你的項(xiàng)目比較大的話,可能會(huì)遇到一些兼容性的問(wèn)題。
但是,注意我要說(shuō)但是了。
你是在學(xué)習(xí)摸索階段,Demo 一定要簡(jiǎn)單,越小越好,越純凈越好。所以這個(gè)切換的動(dòng)作對(duì)你搭建的一個(gè)全新的 Demo 項(xiàng)目來(lái)說(shuō)沒(méi)啥難度,不會(huì)遇到任何問(wèn)題。
這個(gè)時(shí)候,你就可以使用 @SofaAsyncInit 注解了:
圖片
到這里,恭喜你,會(huì)用了。
拿來(lái)吧你不知道你看到這里是什么感受。
反正對(duì)于我來(lái)說(shuō),如果僅僅是為了讓我能使用這個(gè)注解,達(dá)到異步初始化的目的,要讓我從熟悉的 SpringBoot 修改為聽(tīng)都沒(méi)聽(tīng)過(guò)的 SOFABoot,即使這個(gè)框架背后有阿里給它背書(shū),我肯定也是不會(huì)這么干的。
所以,對(duì)于這一類“人有我無(wú)”的東西,我都是采取“拿來(lái)吧你”策略。
你想,最開(kāi)始的我就說(shuō)了,SOFABoot 是 SpringBoot 的變種,它的底層還是 SpringBoot。
而 SOFABoot 又是開(kāi)源的,整個(gè)項(xiàng)目的源碼我都有了:https://github.com/sofastack/sofa-boot/blob/master/README_ZH.md
從其中剝離出一個(gè)基于 SpringBoot 做的小功能,融入到我自己的 SpringBoot 項(xiàng)目中,還玩意難道不是手到擒來(lái)的事情?
不過(guò)就是稍微高級(jí)一點(diǎn)的 cv 罷了。
首先,你得把 SOFABoot 的源碼下載下來(lái),或者在另外的一個(gè)項(xiàng)目中引用它,把自己的項(xiàng)目恢復(fù)為一個(gè) SpringBoot 項(xiàng)目。
圖片
我這邊是直接把 SOFABoot 源碼搞下來(lái)了,先把源碼里面的 @SofaAsyncInit 注解粘到項(xiàng)目里面來(lái),然后從 @SofaAsyncInit 注解入手,發(fā)現(xiàn)除了測(cè)試類只有一個(gè) AsyncInitBeanFactoryPostProcessor 類在對(duì)其進(jìn)行使用:
圖片
所以把這個(gè)類也搬運(yùn)過(guò)來(lái)。
搬運(yùn)過(guò)來(lái)之后你會(huì)發(fā)現(xiàn)有一些類找不到導(dǎo)致報(bào)錯(cuò):
圖片
針對(duì)這部分類,你可以采取無(wú)腦搬運(yùn)的方式,也可以稍加思考替換一些。
比如我就分為了兩種類型:
圖片
標(biāo)號(hào)為 ① 的部分,我是直接粘貼到自己的項(xiàng)目中,然后使用項(xiàng)目中的類。
標(biāo)號(hào)為 ② 的部分,比如 BeanLoadCostBeanFactory 和 SofaBootConstants,他們的目的是為了獲取一個(gè) moduleName 變量:
圖片
我也不知道這個(gè) moduleName 是啥,所以我采取的策略是自己指定一個(gè):
圖片
至于 ErrorCode 和 SofaLogger,日志相關(guān)的,就用自己項(xiàng)目里面的日志就行了。
就是這個(gè)意思:
這樣處理完成之后,AsyncInitBeanFactoryPostProcessor 類不報(bào)錯(cuò)了,接著看這個(gè)類在哪里使用到了。
就這樣順藤摸瓜,最后搬運(yùn)完成之后,就是這些類移過(guò)來(lái)了:
除了這些類之外,你還會(huì)把這個(gè) spring.factories 搬運(yùn)過(guò)來(lái),在項(xiàng)目啟動(dòng)時(shí)把這幾個(gè)相關(guān)的類加載進(jìn)去:
然后再次啟動(dòng)這個(gè)和 SOFABoot 沒(méi)有一點(diǎn)關(guān)系的項(xiàng)目:
你會(huì)發(fā)現(xiàn),你的項(xiàng)目也具備異步初始化 Bean 的功能了。
你要再進(jìn)一步,把它直接封裝為一個(gè) spring-boot-starter-asyncinitbean,發(fā)布到你們公司的私服里面。
其他團(tuán)隊(duì)也能開(kāi)箱即用的使用這個(gè)功能了。
別問(wèn),問(wèn)就是你自己獨(dú)立開(kāi)發(fā)出來(lái)的,掌握全部源碼,技術(shù)風(fēng)險(xiǎn)可控:
啃原理在開(kāi)始啃原理之前,我先多比比兩句。
我寫(xiě)文章的時(shí)候,為什么要把“拿來(lái)吧你”這一小節(jié)放在“啃原理”之前,我是有考慮的。
當(dāng)我們把“異步初始化”這個(gè)功能點(diǎn)剝離出來(lái)之后,你會(huì)發(fā)現(xiàn),要實(shí)現(xiàn)這個(gè)功能,一共也沒(méi)涉及到幾個(gè)類。
聚焦點(diǎn)從一整個(gè)項(xiàng)目變成了幾個(gè)類而已,至少?gòu)母泄偕喜粫?huì)覺(jué)得那么的難,對(duì)閱讀其源碼產(chǎn)生太大的抗拒心理。
而我之前很多關(guān)于源碼閱讀的文章,都強(qiáng)調(diào)過(guò)這一點(diǎn):帶著疑問(wèn)去調(diào)試源碼,要抓住主干,謹(jǐn)防走偏。
前面這一小節(jié),不過(guò)是把這一句話具化了而已。即使沒(méi)有把這些類剝離出來(lái),你直接基于 SOFABoot 來(lái)調(diào)試這個(gè)功能。在你搞清楚“異步初始化”這個(gè)功能的實(shí)現(xiàn)原理之前,理論上你的關(guān)注點(diǎn)和注意力不應(yīng)該被上面這些類之外的任何一個(gè)類給吸引走。
接下來(lái),我們就帶你啃一下原理。
關(guān)于原理部分,我們的突破口肯定是看 @SofaAsyncInit 這個(gè)注解的在哪個(gè)地方被解析的。
你仔細(xì)看這個(gè)注解里面有一個(gè) value 屬性,默認(rèn)為 true,上面的注解說(shuō):用來(lái)標(biāo)注是否應(yīng)該對(duì) init 方法進(jìn)行異步調(diào)用。
而使用到這個(gè) value 值的地方,就只有下面這一個(gè)地方:
com.alipay.sofa.runtime.spring.AsyncInitBeanFactoryPostProcessor#registerAsyncInitBean
判斷為 true 的時(shí)候,執(zhí)行了一個(gè) registerAsyncInitBean 方法。
從方法名稱也知道,它是把可以異步執(zhí)行的 init 方法的 Bean 收集起來(lái)。
所以看源碼可以看出,這里面是用 Map 來(lái)進(jìn)行的存儲(chǔ),提供了一個(gè) register 和 get 方法:
那么這個(gè) Map 里面到底放的是啥呢?
我也不知道,打個(gè)斷點(diǎn)瞅一眼,不就行了:
通過(guò)斷點(diǎn)調(diào)試,我們知道這個(gè)里面把項(xiàng)目中哪些 Bean 可以異步執(zhí)行 init 方法通過(guò) Map 存放了起來(lái)。
那么問(wèn)題就來(lái)了:它怎么知道哪些 Bean 可以異步執(zhí)行 init 呢?
很簡(jiǎn)單啊,因?yàn)槲以趯?duì)應(yīng)的 Bean 上打上了 @SofaAsyncInit 注解。所以可以通過(guò)掃描注解的方式找到這些 Bean。
所以你說(shuō) AsyncInitBeanFactoryPostProcessor 這個(gè)類是在干啥?
肯定核心邏輯就是在解析標(biāo)注了 @SofaAsyncInit 注解的地方嘛。
到這里,我們通過(guò)注解的 value 屬性,找到了 AsyncInitBeanHolder 這個(gè)關(guān)鍵類。
知道了這個(gè)類里面有一個(gè) Map,里面維護(hù)的是所有可以異步執(zhí)行 init 方法的 Bean 和其對(duì)應(yīng)的 init 方法。
好,你思考一下,接下來(lái)應(yīng)該干啥?
接下來(lái)肯定是看哪個(gè)地方在從這個(gè) Map 里面獲取數(shù)據(jù)出來(lái),獲取數(shù)據(jù)的時(shí)候,就說(shuō)明是要異步執(zhí)行這個(gè) Bean 的 init 方法的時(shí)候。
不然它把數(shù)據(jù)放到 Map 里面干啥?玩嗎?
調(diào)用 getAsyncInitMethodName 方法的地方,也在 AsyncProxyBeanPostProcessor 類里面:
com.alipay.sofa.runtime.spring.AsyncProxyBeanPostProcessor#postProcessBeforeInitialization
AsyncProxyBeanPostProcessor 類實(shí)現(xiàn)了 BeanPostProcessor 接口,并重新了其 postProcessBeforeInitialization 方法。
在這個(gè) postProcessBeforeInitialization 方法里面,執(zhí)行了從 Map 里面拿對(duì)象的動(dòng)作。
如果獲取到了則通過(guò) AOP 編程,編織進(jìn)一個(gè) AsyncInitializeBeanMethodInvoker 方法。
把 bean, beanName, methodName 都傳遞了進(jìn)去:
所以關(guān)鍵點(diǎn),就在 AsyncInitializeBeanMethodInvoker 里面,因?yàn)檫@個(gè)里面有真正判斷是否要進(jìn)行異步初始化的邏輯,主要解讀一下這個(gè)類。
首先,關(guān)注一下它的這三個(gè)參數(shù):
initCountDownLatch:是 CountDownLatch 對(duì)象,其中 count 初始化為 1isAsyncCalling:表示是否正在異步執(zhí)行 init 方法。isAsyncCalled:表示是否已經(jīng)異步執(zhí)行過(guò) init 方法。通過(guò)這三個(gè)字段,就可以感知到一個(gè) Bean 是否已經(jīng)或者正在異步執(zhí)行其 init 方法。
這個(gè)類的核心邏輯就是把可以異步執(zhí)行、但是還沒(méi)有執(zhí)行 init 方法的 bean ,把它的 init 方法扔到線程池里面去執(zhí)行:
看一下在上面的 invoke 方法中的 if 方法:
if (!isAsyncCalled && methodName.equals(asyncMethodName))
isAsyncCalled,首先判斷是否已經(jīng)異步執(zhí)行過(guò)這個(gè) bean 的 init 方法了。
然后看看 methodName.equals(asyncMethodName),要反射調(diào)用的方法是否是之前在 map 中維護(hù)的 init 方法。
如果都滿足,就扔到線程池里面去執(zhí)行,這樣就算是完成了異步 init。
如果不滿足呢?
首先,你想想不滿足的時(shí)候說(shuō)明什么情況?
是不是說(shuō)明一個(gè) Bean 的 init 方法在項(xiàng)目啟動(dòng)過(guò)程中不只被調(diào)用一次。
就像是這樣:
雖然,我不知道為什么一個(gè) Bean 要執(zhí)行兩次 init 方法,大概率是代碼寫(xiě)的有問(wèn)題。
但是我不說(shuō),我也不給你拋出異常,我反正就是給你兼容了。
所以,這段代碼就是在處理這個(gè)情況:
如果發(fā)現(xiàn)有多次調(diào)用,那么只要第一次異步初始化完成了,即 isAsyncCalling 為 false ,你可以繼續(xù)執(zhí)行反射調(diào)用初始化方法的動(dòng)作。
這個(gè) invoke 方法的邏輯就是這樣,主要是有一個(gè)線程池在里面。
那么這個(gè)線程池是哪里來(lái)的呢?
com.alipay.sofa.runtime.spring.async.AsyncTaskExecutor
在第一次 submit 任務(wù)的時(shí)候,框架會(huì)幫我們初始化一個(gè)線程池出來(lái)。
然后通過(guò)這個(gè)線程池幫我們完成異步初始化的目標(biāo)。
所以你想想,整個(gè)過(guò)程是非常清晰的。首先找出來(lái)哪些 Bean 上標(biāo)注了 @SofaAsyncInit 注解,找個(gè) Map 維護(hù)起來(lái),接著搞個(gè) AOP 切面,看看哪些 Bean 能在 Map 里面找到,在線程池里面通過(guò)動(dòng)態(tài)代理,調(diào)用其 init 方法。
就完了。
對(duì)不對(duì)?
好,那么問(wèn)題就來(lái)了?
為什么我不直接在 init 方法里面搞個(gè)線程池呢,就像是這樣。
先注入一個(gè)自定義線程池,同時(shí)注釋掉 @SofaAsyncInit 注解:
在指定 Bean 的 init 方法中使用該線程池:
這也不也是能達(dá)到“異步初始化”的目的嗎?
你說(shuō)對(duì)不對(duì)?
不對(duì)啊,對(duì)個(gè)錘子對(duì)。
你看啟動(dòng)日志:
服務(wù)已經(jīng)啟動(dòng)完成了,但是 4s 之后,Bean 的 init 方法才執(zhí)行完畢。
在這期間,如果有請(qǐng)求要使用對(duì)應(yīng)的 Bean 怎么辦?
拿著一個(gè)還未執(zhí)行完成 init 方法的 Bean 框框一頓用,這畫(huà)面想想就很美。
所以怎么辦?
我也不知道,看一下 SOFABoot 里面是怎么解決這個(gè)問(wèn)題的。
在我們前面提到的線程池里面,有這樣的一個(gè)方法:
com.example.asyncthreadpool.spring.AsyncTaskExecutor#ensureAsyncTasksFinish
在這個(gè)方法里面,調(diào)用了 future 的 get 方法進(jìn)行阻塞等待。當(dāng)所有的 future 執(zhí)行完成之后,會(huì)關(guān)閉線程池。
這個(gè) FUTURES 是什么玩意,怎么來(lái)的?
它就是執(zhí)行 submitTask 方法時(shí),維護(hù)進(jìn)行去的,里面裝的就是一個(gè)個(gè)異步執(zhí)行的 init 方法:
所以它通過(guò)這個(gè)方法可以確保能感知到所有的通過(guò)這個(gè)線程池執(zhí)行的 init 方法都執(zhí)行完畢。
現(xiàn)在,方法有了,你先思考一下,我們什么時(shí)候觸發(fā)這個(gè)方法的調(diào)用呢?
是不是應(yīng)該在 Spring 容器告訴你:小老弟,我這邊所有的 Bean 都搞定了,你這邊啥情況了?
這個(gè)時(shí)候你就需要調(diào)用一下這個(gè)方法。
而 Spring 容器加載完成之后,會(huì)發(fā)布這樣的一個(gè)事件。也就是它:
所以,SOFABoot 的做法就是監(jiān)聽(tīng)這個(gè)事件:
com.example.asyncthreadpool.spring.AsyncTaskExecutionListener
這樣,即可確保在異步線程中執(zhí)行的 init 方法的 Bean 執(zhí)行完成之后,容器才算啟動(dòng)成功,對(duì)外提供服務(wù)。
到這里,原理部分我算是講完了。
但是寫(xiě)到這里的時(shí)候,我突然冒出了一個(gè)寫(xiě)之前沒(méi)有過(guò)的想法:在整個(gè)實(shí)現(xiàn)的過(guò)程中,最關(guān)鍵的有兩個(gè)東西:
一個(gè) Map:里面維護(hù)的是所有可以異步執(zhí)行 init 方法的 Bean 和其對(duì)應(yīng)的 init 方法。一個(gè)線程池:異步執(zhí)行 init 方法。而這個(gè) Map 是怎么來(lái)的?
不是通過(guò)掃描 @SofaAsyncInit 注解得到的嗎?
那么掃描出來(lái)的 @SofaAsyncInit 怎么來(lái)的?
不就是我寫(xiě)代碼的時(shí)候主動(dòng)標(biāo)注上去的嗎?
所以,我們是不是可以完全不用 Map ,直接使用異步線程池:
剩去中間環(huán)節(jié),直接一步到位,只需要留下兩個(gè)類即可:
我這里把這個(gè)兩個(gè)類貼出來(lái)。
AsyncTaskExecutionListener:
public class AsyncTaskExecutionListener implements PriorityOrdered, ApplicationListener, ApplicationContextAware { private ApplicationContext applicationContext; @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (applicationContext.equals(event.getApplicationContext())) { AsyncTaskExecutor.ensureAsyncTasksFinish(); } } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}
AsyncTaskExecutor:
@Slf4jpublic class AsyncTaskExecutor { protected static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); protected static final AtomicReference THREAD_POOL_REF = new AtomicReference(); protected static final List FUTURES = new ArrayList<>(); public static Future submitTask(Runnable runnable) { if (THREAD_POOL_REF.get() == null) { ThreadPoolExecutor threadPoolExecutor = createThreadPoolExecutor(); boolean success = THREAD_POOL_REF.compareAndSet(null, threadPoolExecutor); if (!success) { threadPoolExecutor.shutdown(); } } Future future = THREAD_POOL_REF.get().submit(runnable); FUTURES.add(future); return future; } private static ThreadPoolExecutor createThreadPoolExecutor() { int threadPoolCoreSize = CPU_COUNT + 1; int threadPoolMaxSize = CPU_COUNT + 1; log.info(String.format( "create why-async-init-bean thread pool, corePoolSize: %d, maxPoolSize: %d.", threadPoolCoreSize, threadPoolMaxSize)); return new ThreadPoolExecutor(threadPoolCoreSize, threadPoolMaxSize, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy()); } public static void ensureAsyncTasksFinish() { for (Future future : FUTURES) { try { future.get(); } catch (Throwable e) { throw new RuntimeException(e); } } FUTURES.clear(); if (THREAD_POOL_REF.get() != null) { THREAD_POOL_REF.get().shutdown(); THREAD_POOL_REF.set(null); } }}
你只需要把這兩個(gè)類,一共 68 行代碼,粘到你的項(xiàng)目中,然后把 AsyncTaskExecutionListener 以 @Bean 的方式注入:
@Beanpublic AsyncTaskExecutionListener asyncTaskExecutionListener() { return new AsyncTaskExecutionListener();}
恭喜你,你項(xiàng)目中的 Bean 也可以異步執(zhí)行 init 方法了,使用方法就像這樣式兒的:
但是,如果你要對(duì)比這兩種寫(xiě)的法的話:
圖片
肯定是選注解嘛,優(yōu)雅的一比。
所以,我現(xiàn)在問(wèn)你一個(gè)問(wèn)題:清理聊聊異步初始化 Bean 的思路。
然后在追問(wèn)你一個(gè)問(wèn)題:如果通過(guò)自定義注解的方式實(shí)現(xiàn)?需要用到 Spring 的那些擴(kuò)展點(diǎn)?
還思考個(gè)毛啊,不就是這個(gè)過(guò)程嗎?
圖片
回想一下前面的內(nèi)容,是不是品出點(diǎn)味道了,是不是有點(diǎn)感覺(jué)了,是不是覺(jué)得自己又行了?
其實(shí)說(shuō)真的,這個(gè)方案,當(dāng)需要人來(lái)主動(dòng)標(biāo)識(shí)哪些 Bean 是可以異步初始化的時(shí)候,就已經(jīng)“輸了”,已經(jīng)不夠驚艷了。
但是,你想想本文只是想教你“異步初始化”這個(gè)點(diǎn)嗎?
不是的,只是以“異步初始化”為抓手,試圖教你一種源碼解讀的方法,找到撕開(kāi) Spring 框架的又一個(gè)口子,這才是重要的。
最后,前兩天阿里開(kāi)發(fā)者公眾號(hào)也發(fā)布了一篇叫《Bean異步初始化,讓你的應(yīng)用啟動(dòng)飛起來(lái)》的文章,想要達(dá)成的目的一樣,但是最終的落地方案可以說(shuō)差距很大。這篇文章沒(méi)有具體的源碼,但是也可以對(duì)比著看一下,取長(zhǎng)補(bǔ)短,融會(huì)貫通。
行了,我就帶你走到這了,我只是給你指?jìng)€(gè)路,剩下的路就要你自己走了。
天黑路滑,燈火昏暗,抓住主干,及時(shí)回頭。
好了,本文的技術(shù)部分就到這里啦。
關(guān)鍵詞: