這個 Spring 循環(huán)依賴的坑利诺,今天讓我遇見了

1. 前言

這兩天工作遇到了一個挺有意思的Spring循環(huán)依賴的問題,但是這個和以往遇到的循環(huán)依賴問題都不太一樣剩燥,隱藏的相當隱蔽慢逾,網(wǎng)絡(luò)上也很少看到有其他人遇到類似的問題立倍。這里權(quán)且稱他非典型Spring循環(huán)依賴問題。但是我相信我肯定不是第一個踩這個坑的侣滩,也一定不是最后一個口注,可能只是因為踩過的人比較少、鮮有記錄罷了君珠。因此這里權(quán)且記錄一下這個坑寝志,方便后人查看。

正如魯迅(我)說過策添,“這個世上本沒有坑材部,踩的人多了,也便成了坑”唯竹。

2. 典型場景

經(jīng)常聽很多人在Review別人代碼的時候有如下的評論:“你在設(shè)計的時候這些類之間怎么能有循環(huán)依賴呢乐导?你這樣會報錯的!”摩窃。

其實這句話前半句當然沒有錯兽叮,出現(xiàn)循環(huán)依賴的確是設(shè)計上的問題,理論上應(yīng)當將循環(huán)依賴進行分層猾愿,抽取公共部分,然后由各個功能類再去依賴公共部分账阻。

但是在復(fù)雜代碼中蒂秘,各個manager類互相調(diào)用太多,總會一不小心出現(xiàn)一些類之間的循環(huán)依賴的問題淘太∫錾可有時候我們又發(fā)現(xiàn)在用Spring進行依賴注入時,雖然Bean之間有循環(huán)依賴蒲牧,但是代碼本身卻大概率能很正常的work撇贺,似乎也沒有任何bug。

很多敏感的同學(xué)心里肯定有些犯嘀咕冰抢,循環(huán)依賴這種觸犯因果律的事情怎么能發(fā)生呢松嘶?沒錯,這一切其實都并不是那么理所當然挎扰。

3. 什么是依賴

其實翠订,不分場景地、籠統(tǒng)地說A依賴B其實是不夠準確遵倦、至少是不夠細致的尽超。我們可以簡單定義一下什么是依賴

所謂A依賴B梧躺,可以理解為A中某些功能的實現(xiàn)是需要調(diào)用B中的其他功能配合實現(xiàn)的似谁。這里也可以拆分為兩層含義:

A強依賴B。創(chuàng)建A的實例這件事情本身需要B來參加。對照在現(xiàn)實生活就像媽媽生你一樣巩踏。

A弱依賴B斜筐。創(chuàng)建A的實例這件事情不需要B來參加,但是A實現(xiàn)功能是需要調(diào)用B的方法蛀缝。對照在現(xiàn)實生活就像男耕女織一樣顷链。

那么,所謂循環(huán)依賴屈梁,其實也有兩層含義:

強依賴之間的循環(huán)依賴嗤练。

弱依賴之間的循環(huán)依賴。

講到這一層在讶,我想大家應(yīng)該知道我想說什么了煞抬。

4. 什么是依賴調(diào)解

對于強依賴而言,A和B不能互相作為存在的前提构哺,否則宇宙就爆炸了革答。因此這類依賴目前是無法調(diào)解的。

對于弱依賴而言曙强,A和B的存在并沒有前提關(guān)系残拐,A和B只是互相合作。因此正常情況下是不會出現(xiàn)違反因果律的問題的碟嘴。

那什么是循環(huán)依賴的調(diào)解呢溪食?我的理解是:

將 原本是弱依賴關(guān)系的兩者誤當做是強依賴關(guān)系的做法 重新改回弱依賴關(guān)系的過程。

基于上面的分析娜扇,我們基本上也就知道Spring是怎么進行循環(huán)依賴調(diào)解的了(僅指弱依賴错沃,強依賴的循環(huán)依賴只有上帝能自動調(diào)解)。

5. 為什么要依賴注入

網(wǎng)上經(jīng)橙钙埃看到很多手擼IOC容器的入門科普文枢析,大部分人只是將IOC容器實現(xiàn)成一個“存儲Bean的map”,將DI實現(xiàn)成“通過注解+反射將bean賦給類中的field”刃麸。實際上很多人都忽視了DI的依賴調(diào)解的功能醒叁。而幫助我們進行依賴調(diào)解本身就是我們使用IOC+DI的一個重要原因。

在沒有依賴注入的年代里嫌蚤,很多人都會將類之間的依賴通過構(gòu)造函數(shù)傳遞(實際上是構(gòu)成了強依賴)辐益。當項目越來越龐大時,非常容易出現(xiàn)無法調(diào)解的循環(huán)依賴脱吱。這時候開發(fā)人員就被迫必須進行重新抽象智政,非常麻煩。而事實上箱蝠,我們之所以將原本的弱依賴弄成了強依賴续捂,完全是因為我們將類的構(gòu)造垦垂、類的配置類的初始化邏輯三個功能耦合在構(gòu)造函數(shù)之中牙瓢。

而DI就是幫我們將構(gòu)造函數(shù)的功能進行了解耦劫拗。

那么Spring是怎么進行解耦的呢?

6. Spring的依賴注入模型

這一部分網(wǎng)上有很多相關(guān)內(nèi)容矾克,我的理解大概是上面提到的三步:

1·類的構(gòu)造页慷,調(diào)用構(gòu)造函數(shù)、解析強依賴(一般是無參構(gòu)造)胁附,并創(chuàng)建類實例掂榔。

2·類的配置弄兜,根據(jù)Field/GetterSetter中的依賴注入相關(guān)注解、解析弱依賴赎败,并填充所有需要注入的類秤涩。

3·類的初始化邏輯乔夯,調(diào)用生命周期中的初始化方法(例如@PostConstruct注解或InitializingBean的afterPropertiesSet方法)库说,執(zhí)行實際的初始化業(yè)務(wù)邏輯藏斩。

這樣,構(gòu)造函數(shù)的功能就由原來的三個弱化為了一個菇存,只負責(zé)類的構(gòu)造夸研。并將類的配置交由DI,將類的初始化邏輯交給生命周期撰筷。

想到這一層陈惰,忽然解決了我堵在心頭已久的問題。在剛開始學(xué)Spring的時候毕籽,我一直想不通:

為什么Spring除了構(gòu)造函數(shù)之外還要在Bean生命周期里有一個額外的初始化方法?

這個初始化方法和構(gòu)造函數(shù)到底有什么區(qū)別井辆?

為什么Spring建議將初始化的邏輯寫在生命周期里的初始化方法里关筒?

現(xiàn)在,把依賴調(diào)解結(jié)合起來看杯缺,解釋就十分清楚了:

1·為了進行依賴調(diào)解蒸播,Spring在調(diào)用構(gòu)造函數(shù)時是沒有將依賴注入進來的。也就是說構(gòu)造函數(shù)中是無法使用通過DI注入進來的bean(或許可以萍肆,但是Spring并不保證這一點)袍榆。

2·如果不在構(gòu)造函數(shù)中使用依賴注入的bean而僅僅使用構(gòu)造函數(shù)中的參數(shù),雖然沒有問題塘揣,但是這就導(dǎo)致了這個bean強依賴于他的入?yún)ean包雀。當后續(xù)出現(xiàn)循環(huán)依賴時無法進行調(diào)解。

7. ?非典型問題

結(jié)論亲铡?

根據(jù)上面的分析我們應(yīng)該得到了以下共識:

通過構(gòu)造函數(shù)傳遞依賴的做法是有可能造成無法自動調(diào)解的循環(huán)依賴的才写。

純粹通過Field/GetterSetter進行依賴注入造成的循環(huán)依賴是完全可以被自動調(diào)解的葡兑。

因此這樣我就得到了一個我認為正確的結(jié)論。這個結(jié)論屢試不爽赞草,直到我發(fā)現(xiàn)了這次遇到的場景:

在Spring中對Bean進行依賴注入時讹堤,在純粹只考慮循環(huán)依賴的情況下,只要不使用構(gòu)造函數(shù)注入就永遠不會產(chǎn)生無法調(diào)解的循環(huán)依賴厨疙。

當然洲守,我沒有任何“不建議使用構(gòu)造器注入”的意思。相反沾凄,我認為能夠“優(yōu)雅地梗醇、不引入循環(huán)依賴地使用構(gòu)造器注入”是一個要求更高的、更優(yōu)雅的做法搭独。貫徹這一做法需要有更高的抽象能力婴削,并且會自然而然的使得各個功能解耦合。

問題

將實際遇到的問題簡化后大概是下面的樣子(下面的類在同一個包中):

@SpringBootApplication

@Import({ServiceA.class, ConfigurationA.class, BeanB.class})

publicclassTestApplication{

publicstaticvoidmain(String[] args){

SpringApplication.run(TestApplication.class, args);

}

}

publicclassServiceA{

@Autowired

privateBeanA beanA;

@Autowired

privateBeanB beanB;

}

publicclassConfigurationA{

@Autowired

publicBeanB beanB;

@Bean

publicBeanAbeanA(){

returnnewBeanA();

}

}

publicclassBeanA{

}

publicclassBeanB{

@Autowired

publicBeanA beanA;

}

首先聲明一點牙肝,我沒有用@Component唉俗、@Configuration之類的注解,而是采用@Import手動掃描Bean是為了方便指定Bean的初始化順序配椭。Spring會按照我@Import的順序依次加載Bean虫溜。同時,在加載每個Bean的時候股缸,如果這個Bean有需要注入的依賴衡楞,則會試圖加載他依賴的Bean。

簡單梳理一下敦姻,整個依賴鏈大概是這樣:

我們可以發(fā)現(xiàn)瘾境,BeanA,BeanB,ConfigurationA之間有一個循環(huán)依賴,不過莫慌镰惦,所有的依賴都是通過非構(gòu)造函數(shù)注入的方式實現(xiàn)的迷守,理論上似乎可以自動調(diào)解的。

但是實際上旺入,這段代碼會報下面的錯:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?

這顯然是出現(xiàn)了Spring無法調(diào)解的循環(huán)依賴了兑凿。

這已經(jīng)有點奇怪了。但是茵瘾,如果你嘗試將ServiceA類中聲明的BeanA,BeanB調(diào)換一下位置礼华,你就會發(fā)現(xiàn)這段代碼突然就跑的通了!^置亍圣絮!

顯然,調(diào)換這兩個Bean的依賴的順序本質(zhì)是調(diào)整了Spring加載Bean的順序(眾所周知聘殖,Spring創(chuàng)建Bean是單線程的)晨雳。

解釋

相信你已經(jīng)發(fā)現(xiàn)問題了行瑞,沒錯,問題的癥結(jié)就在于ConfigurationA這個配置類餐禁。

配置類和普通的Bean有一個區(qū)別血久,就在于除了同樣作為Bean被管理之外,配置類也可以在內(nèi)部聲明其他的Bean帮非。

這樣就存在一個問題氧吐,配置類中聲明的其他Bean的構(gòu)造過程其實是屬于配置類的業(yè)務(wù)邏輯的一部分的。也就是說我們只有先將配置類的依賴全部滿足之后才可以創(chuàng)建他自己聲明的其他的Bean末盔。(如果不加這個限制筑舅,那么在創(chuàng)建自己聲明的其他Bean的時候,如果用到了自己的依賴陨舱,則有空指針的風(fēng)險翠拣。)

這樣一來,BeanA對ConfigurationA就不再是弱依賴游盲,而是實打?qū)嵉?b>強依賴了(也就是說ConfigurationA的初始化不僅影響了BeanA的依賴填充误墓,也影響了BeanA的實例構(gòu)造)。

有了這樣的認識益缎,我們再來分別分析兩種初始化的路徑谜慌。

先加載BeanA

1·當Spring在試圖加載ServiceA時,先構(gòu)造了ServiceA莺奔,然后發(fā)現(xiàn)他依賴BeanA欣范,于是就試圖去加載BeanA;

2·Spring想構(gòu)造BeanA令哟,但是發(fā)現(xiàn)BeanA在ConfigurationA內(nèi)部恼琼,于是又試圖加載ConfigurationA(此時BeanA仍未構(gòu)造);

? ?Spring構(gòu)造了ConfigurationA的實例屏富,然后發(fā)現(xiàn)他依賴BeanB驳癌,于是就試圖去加載BeanB。

3·Spring構(gòu)造了BeanB的實例役听,然后發(fā)現(xiàn)他依賴BeanA,于是就試圖去加載BeanA表窘。

4·Spring發(fā)現(xiàn)BeanA還沒有實例化典予,此時Spring發(fā)現(xiàn)自己回到了步驟2。乐严。瘤袖。GG。昂验。捂敌。

先加載BeanB

1·當Spring在試圖加載ServiceA時艾扮,先構(gòu)造了ServiceA,然后發(fā)現(xiàn)他依賴BeanB占婉,于是就試圖去加載BeanB泡嘴;

2·Spring構(gòu)造了BeanB的實例,然后發(fā)現(xiàn)他依賴BeanA逆济,于是就試圖去加載BeanA酌予。

3·Spring發(fā)現(xiàn)BeanA在ConfigurationA內(nèi)部,于是試圖加載ConfigurationA(此時BeanA仍未構(gòu)造)奖慌;

4·Spring構(gòu)造了ConfigurationA的實例抛虫,然后發(fā)現(xiàn)他依賴BeanB,并且BeanB的實例已經(jīng)有了简僧,于是將這個依賴填充進ConfigurationA中建椰。

5·Spring發(fā)現(xiàn)ConfigurationA已經(jīng)完成了構(gòu)造、填充了依賴岛马,于是想起來構(gòu)造了BeanA棉姐。

6·Spring發(fā)現(xiàn)BeanA已經(jīng)有了實例,于是將他給了BeanB蛛枚,BeanB填充的依賴完成谅海。

7·Spring回到了為ServiceA填充依賴的過程,發(fā)現(xiàn)還依賴BeanA蹦浦,于是將BeanA填充給了ServiceA扭吁。

8·Spring成功完成了初始化操作。

結(jié)論

總結(jié)一下這個問題盲镶,結(jié)論就是:

除了構(gòu)造注入會導(dǎo)致強依賴以外侥袜,一個Bean也會強依賴于暴露他的配置類。

代碼壞味道

寫到這溉贿,我已經(jīng)覺得有點惡心了枫吧。誰在寫代碼的時候沒事做還要這么分析依賴,太容易出鍋了吧宇色!那到底有沒有什么方法能避免分析這種惡心的問題呢九杂?

方法其實是有的,那就是遵守下面的代碼規(guī)范————不要對有@Configuration注解的配置類進行Field級的依賴注入宣蠕。

沒錯例隆,對配置類進行依賴注入,幾乎等價于對配置類中的所有Bean增加了一個強依賴抢蚀,極大的提高了出現(xiàn)無法調(diào)解的循環(huán)依賴的風(fēng)險镀层。我們應(yīng)當將依賴盡可能的縮小,所有依賴只能由真正需要的Bean直接依賴才行皿曲。

參考資料

Circular Dependencies in Spring

Spring-bean的循環(huán)依賴以及解決方式

Factory method injection should be used in "@Configuration" classes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唱逢,一起剝皮案震驚了整個濱河市吴侦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坞古,老刑警劉巖备韧,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绸贡,居然都是意外死亡盯蝴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門听怕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捧挺,“玉大人,你說我怎么就攤上這事尿瞭∶隼樱” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵声搁,是天一觀的道長黑竞。 經(jīng)常有香客問我,道長疏旨,這世上最難降的妖魔是什么很魂? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮檐涝,結(jié)果婚禮上遏匆,老公的妹妹穿的比我還像新娘。我一直安慰自己谁榜,他們只是感情好幅聘,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著窃植,像睡著了一般帝蒿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巷怜,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天葛超,我揣著相機與錄音,去河邊找鬼延塑。 笑死巩掺,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的页畦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼研儒,長吁一口氣:“原來是場噩夢啊……” “哼豫缨!你這毒婦竟也來了独令?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤好芭,失蹤者是張志新(化名)和其女友劉穎燃箭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舍败,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡招狸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了邻薯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裙戏。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖厕诡,靈堂內(nèi)的尸體忽然破棺而出累榜,到底是詐尸還是另有隱情,我是刑警寧澤灵嫌,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布壹罚,位于F島的核電站,受9級特大地震影響寿羞,放射性物質(zhì)發(fā)生泄漏猖凛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一绪穆、第九天 我趴在偏房一處隱蔽的房頂上張望辨泳。 院中可真熱鬧,春花似錦霞幅、人聲如沸漠吻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽途乃。三九已至,卻和暖如春扔傅,著一層夾襖步出監(jiān)牢的瞬間耍共,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工猎塞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留试读,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓荠耽,卻偏偏與公主長得像钩骇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容