日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

这个 Spring 循环依赖的坑,90% 以上的人都不知道

發布時間:2024/3/12 javascript 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 这个 Spring 循环依赖的坑,90% 以上的人都不知道 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

點擊上方“后端技術精選”,選擇“置頂公眾號”

技術文章第一時間送達!

作者:Mythsman

blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/

1.前言

這兩天工作遇到了一個挺有意思的Spring循環依賴的問題,但是這個和以往遇到的循環依賴問題都不太一樣,隱藏的相當隱蔽,網絡上也很少看到有其他人遇到類似的問題。這里權且稱他非典型Spring循環依賴問題

但是我相信我肯定不是第一個踩這個坑的,也一定不是最后一個,可能只是因為踩過的人比較少、鮮有記錄罷了。因此這里權且記錄一下這個坑,方便后人查看。

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

2. 典型場景

經常聽很多人在Review別人代碼的時候有如下的評論:“你在設計的時候這些類之間怎么能有循環依賴呢?你這樣會報錯的!”。

其實這句話前半句當然沒有錯,出現循環依賴的確是設計上的問題,理論上應當將循環依賴進行分層,抽取公共部分,然后由各個功能類再去依賴公共部分。

但是在復雜代碼中,各個manager類互相調用太多,總會一不小心出現一些類之間的循環依賴的問題。可有時候我們又發現在用Spring進行依賴注入時,雖然Bean之間有循環依賴,但是代碼本身卻大概率能很正常的work,似乎也沒有任何bug。

很多敏感的同學心里肯定有些犯嘀咕,循環依賴這種觸犯因果律的事情怎么能發生呢?沒錯,這一切其實都并不是那么理所當然。

3. 什么是依賴

其實,不分場景地、籠統地說A依賴B其實是不夠準確、至少是不夠細致的。我們可以簡單定義一下什么是依賴

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

  • A強依賴B。創建A的實例這件事情本身需要B來參加。對照在現實生活就像媽媽生你一樣。

  • A弱依賴B。創建A的實例這件事情不需要B來參加,但是A實現功能是需要調用B的方法。對照在現實生活就像男耕女織一樣。

  • 那么,所謂循環依賴,其實也有兩層含義:

  • 強依賴之間的循環依賴。

  • 弱依賴之間的循環依賴。

  • 講到這一層,我想大家應該知道我想說什么了。

    4. 什么是依賴調解

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

    對于弱依賴而言,A和B的存在并沒有前提關系,A和B只是互相合作。因此正常情況下是不會出現違反因果律的問題的。

    那什么是循環依賴的調解呢?我的理解是:

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

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

    5. 為什么要依賴注入

    網上經常看到很多手擼IOC容器的入門科普文,大部分人只是將IOC容器實現成一個“存儲Bean的map”,將DI實現成“通過注解+反射將bean賦給類中的field”。實際上很多人都忽視了DI的依賴調解的功能。而幫助我們進行依賴調解本身就是我們使用IOC+DI的一個重要原因。

    在沒有依賴注入的年代里,很多人都會將類之間的依賴通過構造函數傳遞(實際上是構成了強依賴)。當項目越來越龐大時,非常容易出現無法調解的循環依賴。這時候開發人員就被迫必須進行重新抽象,非常麻煩。而事實上,我們之所以將原本的弱依賴弄成了強依賴,完全是因為我們將類的構造類的配置類的初始化邏輯三個功能耦合在構造函數之中。

    而DI就是幫我們將構造函數的功能進行了解耦。

    那么Spring是怎么進行解耦的呢?(springboot內容:SpringBoot內容聚合)

    6. Spring的依賴注入模型

    這一部分網上有很多相關內容,我的理解大概是上面提到的三步:

  • 類的構造,調用構造函數、解析強依賴(一般是無參構造),并創建類實例。

  • 類的配置,根據Field/GetterSetter中的依賴注入相關注解、解析弱依賴,并填充所有需要注入的類。

  • 類的初始化邏輯,調用生命周期中的初始化方法(例如@PostConstruct注解或InitializingBean的afterPropertiesSet方法),執行實際的初始化業務邏輯。

  • 這樣,構造函數的功能就由原來的三個弱化為了一個,只負責類的構造。并將類的配置交由DI,將類的初始化邏輯交給生命周期。

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

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

    • 這個初始化方法和構造函數到底有什么區別?

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

    現在,把依賴調解結合起來看,解釋就十分清楚了:

  • 為了進行依賴調解,Spring在調用構造函數時是沒有將依賴注入進來的。也就是說構造函數中是無法使用通過DI注入進來的bean(或許可以,但是Spring并不保證這一點)。

  • 如果不在構造函數中使用依賴注入的bean而僅僅使用構造函數中的參數,雖然沒有問題,但是這就導致了這個bean強依賴于他的入參bean。當后續出現循環依賴時無法進行調解。

  • 7. ?非典型問題

    結論?

    根據上面的分析我們應該得到了以下共識:

    • 通過構造函數傳遞依賴的做法是有可能造成無法自動調解的循環依賴的。

    • 純粹通過Field/GetterSetter進行依賴注入造成的循環依賴是完全可以被自動調解的。

    因此這樣我就得到了一個我認為正確的結論。這個結論屢試不爽,直到我發現了這次遇到的場景:

    在Spring中對Bean進行依賴注入時,在純粹只考慮循環依賴的情況下,只要不使用構造函數注入就永遠不會產生無法調解的循環依賴。

    當然,我沒有任何“不建議使用構造器注入”的意思。相反,我認為能夠“優雅地、不引入循環依賴地使用構造器注入”是一個要求更高的、更優雅的做法。貫徹這一做法需要有更高的抽象能力,并且會自然而然的使得各個功能解耦合。

    問題

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

    @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 {@Autowiredprivate BeanA beanA;@Autowiredprivate BeanB beanB; } public class ConfigurationA {@Autowiredpublic BeanB beanB;@Beanpublic BeanA beanA() {return new BeanA();} } public class BeanA { } public class BeanB {@Autowiredpublic BeanA beanA; }

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

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

    我們可以發現,BeanA,BeanB,ConfigurationA之間有一個循環依賴,不過莫慌,所有的依賴都是通過非構造函數注入的方式實現的,理論上似乎可以自動調解的。

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

    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?

    這顯然是出現了Spring無法調解的循環依賴了。

    這已經有點奇怪了。但是,如果你嘗試將ServiceA類中聲明的BeanA,BeanB調換一下位置,你就會發現這段代碼突然就跑的通了!!!

    顯然,調換這兩個Bean的依賴的順序本質是調整了Spring加載Bean的順序(眾所周知,Spring創建Bean是單線程的)。參考:11張流程圖幫你搞定 Spring Bean 生命周期

    解釋

    相信你已經發現問題了,沒錯,問題的癥結就在于ConfigurationA這個配置類。

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

    這樣就存在一個問題,配置類中聲明的其他Bean的構造過程其實是屬于配置類的業務邏輯的一部分的。也就是說我們只有先將配置類的依賴全部滿足之后才可以創建他自己聲明的其他的Bean。(如果不加這個限制,那么在創建自己聲明的其他Bean的時候,如果用到了自己的依賴,則有空指針的風險。)

    這樣一來,BeanA對ConfigurationA就不再是弱依賴,而是實打實的強依賴了(也就是說ConfigurationA的初始化不僅影響了BeanA的依賴填充,也影響了BeanA的實例構造)。

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

    先加載BeanA

  • 當Spring在試圖加載ServiceA時,先構造了ServiceA,然后發現他依賴BeanA,于是就試圖去加載BeanA;

  • Spring想構造BeanA,但是發現BeanA在ConfigurationA內部,于是又試圖加載ConfigurationA(此時BeanA仍未構造);

  • Spring構造了ConfigurationA的實例,然后發現他依賴BeanB,于是就試圖去加載BeanB。

  • Spring構造了BeanB的實例,然后發現他依賴BeanA,于是就試圖去加載BeanA。

  • Spring發現BeanA還沒有實例化,此時Spring發現自己回到了步驟2。。。GG。。。

  • 先加載BeanB

  • 當Spring在試圖加載ServiceA時,先構造了ServiceA,然后發現他依賴BeanB,于是就試圖去加載BeanB;

  • Spring構造了BeanB的實例,然后發現他依賴BeanA,于是就試圖去加載BeanA。

  • Spring發現BeanA在ConfigurationA內部,于是試圖加載ConfigurationA(此時BeanA仍未構造);

  • Spring構造了ConfigurationA的實例,然后發現他依賴BeanB,并且BeanB的實例已經有了,于是將這個依賴填充進ConfigurationA中。

  • Spring發現ConfigurationA已經完成了構造、填充了依賴,于是想起來構造了BeanA。

  • Spring發現BeanA已經有了實例,于是將他給了BeanB,BeanB填充的依賴完成。

  • Spring回到了為ServiceA填充依賴的過程,發現還依賴BeanA,于是將BeanA填充給了ServiceA。

  • Spring成功完成了初始化操作。

  • 結論

    總結一下這個問題,結論就是:

    除了構造注入會導致強依賴以外,一個Bean也會強依賴于暴露他的配置類。

    代碼壞味道

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

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

    沒錯,對配置類進行依賴注入,幾乎等價于對配置類中的所有Bean增加了一個強依賴,極大的提高了出現無法調解的循環依賴的風險。我們應當將依賴盡可能的縮小,所有依賴只能由真正需要的Bean直接依賴才行。

    參考資料

    • Circular Dependencies in Spring

    • Spring-bean的循環依賴以及解決方式

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

    END

    Java面試題專欄

    【40期】說一下線程池內部工作原理

    【39期】Mybatis面試18問,你想知道的都在這里了!

    【38期】一份tcp、http面試指南,常考點都給你了

    【37期】請你詳細說說類加載流程,類加載機制及自定義類加載器

    【36期】說說 如何停止一個正在運行的線程?

    【35期】談談你對Java線程之間通信方式的理解

    【34期】談談為什么要拆分數據庫?有哪些方法?

    【33期】分別談談聯合索引生效和失效的條件

    【32期】你知道Redis的字符串是怎么實現的嗎?

    【31期】了解什么是 redis 的雪崩、穿透和擊穿?redis 崩潰之后會怎么樣?應對措施是什么


    歡迎長按下圖關注公眾號后端技術精選

    總結

    以上是生活随笔為你收集整理的这个 Spring 循环依赖的坑,90% 以上的人都不知道的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。