<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這個(gè)Spring循環(huán)依賴的坑,90%以上的人都不知道

          共 5186字,需瀏覽 11分鐘

           ·

          2019-10-20 23:21

          作者:Mythsman

          原文:https://blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/

          轉(zhuǎn)載自:猿天地

          1

          -

          前言



          這兩天工作遇到了一個(gè)挺有意思的Spring循環(huán)依賴的問(wèn)題,但是這個(gè)和以往遇到的循環(huán)依賴問(wèn)題都不太一樣,隱藏的相當(dāng)隱蔽,網(wǎng)絡(luò)上也很少看到有其他人遇到類似的問(wèn)題。這里權(quán)且稱他非典型Spring循環(huán)依賴問(wèn)題但是我相信我肯定不是第一個(gè)踩這個(gè)坑的,也一定不是最后一個(gè),可能只是因?yàn)椴冗^(guò)的人比較少、鮮有記錄罷了。因此這里權(quán)且記錄一下這個(gè)坑,方便后人查看。

          正如魯迅(我)說(shuō)過(guò),“這個(gè)世上本沒(méi)有坑,踩的人多了,也便成了坑”。



          -

          2

          -

          典型場(chǎng)景



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

          其實(shí)這句話前半句當(dāng)然沒(méi)有錯(cuò),出現(xiàn)循環(huán)依賴的確是設(shè)計(jì)上的問(wèn)題,理論上應(yīng)當(dāng)將循環(huán)依賴進(jìn)行分層,抽取公共部分,然后由各個(gè)功能類再去依賴公共部分。

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

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


          -

          3

          -

          什么是依賴



          其實(shí),不分場(chǎng)景地、籠統(tǒng)地說(shuō)A依賴B其實(shí)是不夠準(zhǔn)確、至少是不夠細(xì)致的。我們可以簡(jiǎn)單定義一下什么是依賴

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

          1. A強(qiáng)依賴B創(chuàng)建A的實(shí)例這件事情本身需要B來(lái)參加。對(duì)照在現(xiàn)實(shí)生活就像媽媽生你一樣。

          2. A弱依賴B創(chuàng)建A的實(shí)例這件事情不需要B來(lái)參加,但是A實(shí)現(xiàn)功能是需要調(diào)用B的方法。對(duì)照在現(xiàn)實(shí)生活就像男耕女織一樣。

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

          1. 強(qiáng)依賴之間的循環(huán)依賴。

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

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


          -

          4

          -

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



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

          對(duì)于弱依賴而言,A和B的存在并沒(méi)有前提關(guān)系,A和B只是互相合作。因此正常情況下是不會(huì)出現(xiàn)違反因果律的問(wèn)題的。

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

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

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


          -

          5

          -

          為什么要依賴注入



          網(wǎng)上經(jīng)常看到很多手?jǐn)]IOC容器的入門科普文,大部分人只是將IOC容器實(shí)現(xiàn)成一個(gè)“存儲(chǔ)Bean的map”,將DI實(shí)現(xiàn)成“通過(guò)注解+反射將bean賦給類中的field”。實(shí)際上很多人都忽視了DI的依賴調(diào)解的功能。幫助我們進(jìn)行依賴調(diào)解本身就是我們使用IOC+DI的一個(gè)重要原因。

          在沒(méi)有依賴注入的年代里,很多人都會(huì)將類之間的依賴通過(guò)構(gòu)造函數(shù)傳遞(實(shí)際上是構(gòu)成了強(qiáng)依賴)。當(dāng)項(xiàng)目越來(lái)越龐大時(shí),非常容易出現(xiàn)無(wú)法調(diào)解的循環(huán)依賴。這時(shí)候開(kāi)發(fā)人員就被迫必須進(jìn)行重新抽象,非常麻煩。而事實(shí)上,我們之所以將原本的弱依賴弄成了強(qiáng)依賴,完全是因?yàn)槲覀儗?strong style="font-style:inherit;font-size:inherit;font-family:inherit;">類的構(gòu)造、類的配置類的初始化邏輯三個(gè)功能耦合在構(gòu)造函數(shù)之中。

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

          那么Spring是怎么進(jìn)行解耦的呢?


          -

          6

          -

          Spring的依賴注入模型


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

          1. 類的構(gòu)造,調(diào)用構(gòu)造函數(shù)、解析強(qiáng)依賴(一般是無(wú)參構(gòu)造),并創(chuàng)建類實(shí)例。

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

          3. 類的初始化邏輯,調(diào)用生命周期中的初始化方法(例如@PostConstruct注解或InitializingBeanafterPropertiesSet方法),執(zhí)行實(shí)際的初始化業(yè)務(wù)邏輯。

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

          想到這一層,忽然解決了我堵在心頭已久的問(wèn)題。在剛開(kāi)始學(xué)Spring的時(shí)候,我一直想不通:

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

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

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

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

          1. 為了進(jìn)行依賴調(diào)解,Spring在調(diào)用構(gòu)造函數(shù)時(shí)是沒(méi)有將依賴注入進(jìn)來(lái)的。也就是說(shuō)構(gòu)造函數(shù)中是無(wú)法使用通過(guò)DI注入進(jìn)來(lái)的bean(或許可以,但是Spring并不保證這一點(diǎn))。

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


          -

          7

          -

          非典型問(wèn)題



          結(jié)論?

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

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

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

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

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

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

          問(wèn)題

          將實(shí)際遇到的問(wèn)題簡(jiǎn)化后大概是下面的樣子(下面的類在同一個(gè)包中):


          @SpringBootApplication@Import({ServiceA.class, ConfigurationA.class, BeanB.class})public class TestApplication {    public static void main(String[] args) {        SpringApplication.run(TestApplication.class, args);    }}


          public class ServiceA {    @Autowired    private BeanA beanA;
          @Autowired private BeanB beanB; }


          public class ConfigurationA {    @Autowired    public BeanB beanB;
          @Bean public BeanA beanA() { return new BeanA(); }}


          public class BeanA {}


          public class BeanB {    @Autowired    public BeanA beanA;}


          首先聲明一點(diǎn),我沒(méi)有用@Component@Configuration之類的注解,而是采用@Import手動(dòng)掃描Bean是為了方便指定Bean的初始化順序。Spring會(huì)按照我@Import的順序依次加載Bean。同時(shí),在加載每個(gè)Bean的時(shí)候,如果這個(gè)Bean有需要注入的依賴,則會(huì)試圖加載他依賴的Bean。

          簡(jiǎn)單梳理一下,整個(gè)依賴鏈大概是這樣:


          c71bb8a507fae27522c955cb279af807.webp


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

          但是實(shí)際上,這段代碼會(huì)報(bào)下面的錯(cuò):

          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無(wú)法調(diào)解的循環(huán)依賴了。

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

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

          解釋

          相信你已經(jīng)發(fā)現(xiàn)問(wèn)題了,沒(méi)錯(cuò),問(wèn)題的癥結(jié)就在于ConfigurationA這個(gè)配置類。

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

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

          這樣一來(lái),BeanA對(duì)ConfigurationA就不再是弱依賴,而是實(shí)打?qū)嵉?strong style="font-style:inherit;font-size:inherit;font-family:inherit;">強(qiáng)依賴了(也就是說(shuō)ConfigurationA的初始化不僅影響了BeanA的依賴填充,也影響了BeanA的實(shí)例構(gòu)造)。

          有了這樣的認(rèn)識(shí),我們?cè)賮?lái)分別分析兩種初始化的路徑。

          先加載BeanA

          1. 當(dāng)Spring在試圖加載ServiceA時(shí),先構(gòu)造了ServiceA,然后發(fā)現(xiàn)他依賴BeanA,于是就試圖去加載BeanA;

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

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

          4. Spring構(gòu)造了BeanB的實(shí)例,然后發(fā)現(xiàn)他依賴BeanA,于是就試圖去加載BeanA。

          5. Spring發(fā)現(xiàn)BeanA還沒(méi)有實(shí)例化,此時(shí)Spring發(fā)現(xiàn)自己回到了步驟2。GG。

          先加載BeanB

          1. 當(dāng)Spring在試圖加載ServiceA時(shí),先構(gòu)造了ServiceA,然后發(fā)現(xiàn)他依賴BeanB,于是就試圖去加載BeanB;

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

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

          4. Spring構(gòu)造了ConfigurationA的實(shí)例,然后發(fā)現(xiàn)他依賴BeanB,并且BeanB的實(shí)例已經(jīng)有了,于是將這個(gè)依賴填充進(jìn)ConfigurationA中。

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

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

          7. Spring回到了為ServiceA填充依賴的過(guò)程,發(fā)現(xiàn)還依賴BeanA,于是將BeanA填充給了ServiceA。

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

          結(jié)論

          總結(jié)一下這個(gè)問(wèn)題,結(jié)論就是:

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

          代碼壞味道

          寫到這,我已經(jīng)覺(jué)得有點(diǎn)惡心了。誰(shuí)在寫代碼的時(shí)候沒(méi)事做還要這么分析依賴,太容易出鍋了吧!那到底有沒(méi)有什么方法能避免分析這種惡心的問(wèn)題呢?

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

          沒(méi)錯(cuò),對(duì)配置類進(jìn)行依賴注入,幾乎等價(jià)于對(duì)配置類中的所有Bean增加了一個(gè)強(qiáng)依賴,極大的提高了出現(xiàn)無(wú)法調(diào)解的循環(huán)依賴的風(fēng)險(xiǎn)。我們應(yīng)當(dāng)將依賴盡可能的縮小,所有依賴只能由真正需要的Bean直接依賴才行。

          參考資料

          Circular Dependencies in Spring

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

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


          推薦閱讀:


          0866f9729c3b6a190ef190893c569d77.webp喜歡我可以給我設(shè)為星標(biāo)哦0866f9729c3b6a190ef190893c569d77.webp

          73e9195bfc4a0ba9a7501a7322378922.webp

          好文章,我?在看?

          95072e06b24fdf3c45aab9b93b25d5f2.webp
          瀏覽 93
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91人妻视频 | 午夜福利日本 | 51露脸丨熟女 | 成人网站视频大香蕉 | 日韩A级|