首页
/
每日頭條
/
生活
/
spring循環依賴怎麼使用
spring循環依賴怎麼使用
更新时间:2024-10-05 09:16:48
1.由同事抛的一個問題開始

最近項目組的一個同事遇到了一個問題,問我的意見,一下子引起的我的興趣,因為這個問題我也是第一次遇到。平時自認為對spring循環依賴問題還是比較了解的,直到遇到這個和後面的幾個問題後,重新刷新了我的認識。

我們先看看當時出問題的代碼片段:

@Service publicclass TestService1 { @Autowired private TestService2 testService2; @Async public void test1() { } }

@Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

這兩段代碼中定義了兩個Service類:TestService1和TestService2,在TestService1中注入了TestService2的實例,同時在TestService2中注入了TestService1的實例,這裡構成了循環依賴。

隻不過,這不是普通的循環依賴,因為TestService1的test1方法上加了一個@Async注解。

大家猜猜程序啟動後運行結果會怎樣?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

報錯了。。。原因是出現了循環依賴。

「不科學呀,spring不是号稱能解決循環依賴問題嗎,怎麼還會出現?」

如果把上面的代碼稍微調整一下:

@Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } }

把TestService1的test1方法上的@Async注解去掉,TestService1和TestService2都需要注入對方的實例,同樣構成了循環依賴。

但是重新啟動項目,發現它能夠正常運行。這又是為什麼?

帶着這兩個問題,讓我們一起開始spring循環依賴的探秘之旅。

2.什麼是循環依賴?

循環依賴:說白是一個或多個對象實例之間存在直接或間接的依賴關系,這種依賴關系構成了構成一個環形調用。

第一種情況:自己依賴自己的直接依賴

spring循環依賴怎麼使用(我是如何解決循環依賴的)1

第二種情況:兩個對象之間的直接依賴

spring循環依賴怎麼使用(我是如何解決循環依賴的)2

第三種情況:多個對象之間的間接依賴

spring循環依賴怎麼使用(我是如何解決循環依賴的)3

前面兩種情況的直接循環依賴比較直觀,非常好識别,但是第三種間接循環依賴的情況有時候因為業務代碼調用層級很深,不容易識别出來。

3.循環依賴的N種場景

spring中出現循環依賴主要有以下場景:

spring循環依賴怎麼使用(我是如何解決循環依賴的)4

單例的setter注入

這種注入方式應該是spring用得最多的,代碼如下:

@Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } }

@Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

這是一個經典的循環依賴,但是它能正常運行,得益于spring的内部機制,讓我們根本無法感知它有問題,因為spring默默幫我們解決了。

spring内部有三級緩存:

  • singletonObjects 一級緩存,用于保存實例化、注入、初始化完成的bean實例
  • earlySingletonObjects 二級緩存,用于保存實例化完成的bean實例
  • singletonFactories 三級緩存,用于保存bean創建工廠,以便于後面擴展有機會創建代理對象。

下面用一張圖告訴你,spring是如何解決循環依賴的:

spring循環依賴怎麼使用(我是如何解決循環依賴的)5

細心的朋友可能會發現在這種場景中第二級緩存作用不大。 那麼問題來了,為什麼要用第二級緩存呢? 試想一下,如果出現以下這種情況,我們要如何處理?

@Service publicclass TestService1 { @Autowired private TestService2 testService2; @Autowired private TestService3 testService3; public void test1() { } }

@Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

@Service publicclass TestService3 { @Autowired private TestService1 testService1; public void test3() { } }

TestService1依賴于TestService2和TestService3,而TestService2依賴于TestService1,同時TestService3也依賴于TestService1。

按照上圖的流程可以把TestService1注入到TestService2,并且TestService1的實例是從第三級緩存中獲取的。

假設不用第二級緩存,TestService1注入到TestService3的流程如圖:

spring循環依賴怎麼使用(我是如何解決循環依賴的)6

TestService1注入到TestService3又需要從第三級緩存中獲取實例,而第三級緩存裡保存的并非真正的實例對象,而是ObjectFactory對象。說白了,兩次從三級緩存中獲取都是ObjectFactory對象,而通過它創建的實例對象每次可能都不一樣的。

這樣不是有問題?

為了解決這個問題,spring引入的第二級緩存。上面圖1其實TestService1對象的實例已經被添加到第二級緩存中了,而在TestService1注入到TestService3時,隻用從第二級緩存中獲取該對象即可。

spring循環依賴怎麼使用(我是如何解決循環依賴的)7

還有個問題,第三級緩存中為什麼要添加ObjectFactory對象,直接保存實例對象不行嗎?

答:不行,因為假如你想對添加到三級緩存中的實例對象進行增強,直接用實例對象是行不通的。

針對這種場景spring是怎麼做的呢?

答案就在AbstractAutowireCapableBeanFactory類doCreateBean方法的這段代碼中:

spring循環依賴怎麼使用(我是如何解決循環依賴的)8

它定義了一個匿名内部類,通過getEarlyBeanReference方法獲取代理對象,其實底層是通過AbstractAutoProxyCreator類的getEarlyBeanReference生成代理對象。

多例的setter注入

這種注入方法偶然會有,特别是在多線程的場景下,具體代碼如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } }

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

很多人說這種情況spring容器啟動會報錯,其實是不對的,我非常負責任地告訴你程序能夠正常啟動。

為什麼呢?

其實在AbstractApplicationContext類的refresh方法中告訴了我們答案,它會調用finishBeanFactoryInitialization方法,該方法的作用是為了spring容器啟動的時候提前初始化一些bean。該方法的内部又調用了preInstantiateSingletons方法。

spring循環依賴怎麼使用(我是如何解決循環依賴的)9

标紅的地方明顯能夠看出:非抽象、單例 并且非懶加載的類才能被提前初始bean。 而多例即SCOPE_PROTOTYPE類型的類,非單例,不會被提前初始化bean,所以程序能夠正常啟動。

如何讓他提前初始化bean呢?

隻需要再定義一個單例的類,在它裡面注入TestService1

@Service publicclass TestService3 { @Autowired private TestService1 testService1; }

重新啟動程序,執行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出現了循環依賴。

注意:這種循環依賴問題是無法解決的,因為它沒有用緩存,每次都會生成一個新對象。

構造器注入

這種注入方式是spring4.x以上的版本中官方推薦的方式,看看如下代碼:

@Service publicclass TestService1 { public TestService1(TestService2 testService2) { } } @Service publicclass TestService2 { public TestService2(TestService1 testService1) { } }

運行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出現了循環依賴,為什麼呢?

spring循環依賴怎麼使用(我是如何解決循環依賴的)10

從圖中的流程看出構造器注入隻是添加了三級緩存,并沒有使用緩存,所以也無法解決循環依賴問題。

單例的代理對象setter注入

這種注入方式其實也比較常用,比如平時使用:@Async注解的場景,會通過AOP自動生成代理對象。

我那位同事的問題也是這種情況。

@Service publicclass TestService1 { @Autowired private TestService2 testService2; @Async public void test1() { } }

@Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

從前面得知程序啟動會報錯,出現了循環依賴:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

為什麼會循環依賴呢?

答案就在下面這張圖中:

spring循環依賴怎麼使用(我是如何解決循環依賴的)11

說白了,bean初始化完成之後,後面還有一步去檢查:第二級緩存 和 原始對象 是否相等。由于它對前面流程來說無關緊要,所以前面的流程圖中省略了,但是在這裡是關鍵點,我們重點說說:

spring循環依賴怎麼使用(我是如何解決循環依賴的)12

那位同事的問題正好是走到這段代碼,發現第二級緩存 和 原始對象不相等,所以抛出了循環依賴的異常。

如果這時候把TestService1改個名字,改成:TestService6,其他的都不變。

@Service publicclass TestService6 { @Autowired private TestService2 testService2; @Async public void test1() { } }

再重新啟動一下程序,神奇般的好了。

what? 這又是為什麼?

這就要從spring的bean加載順序說起了,默認情況下,spring是按照文件完整路徑遞歸查找的,按路徑 文件名排序,排在前面的先加載。所以TestService1比TestService2先加載,而改了文件名稱之後,TestService2比TestService6先加載。

為什麼TestService2比TestService6先加載就沒問題呢?

答案在下面這張圖中:

spring循環依賴怎麼使用(我是如何解決循環依賴的)13

這種情況testService6中其實第二級緩存是空的,不需要跟原始對象判斷,所以不會抛出循環依賴。

DependsOn循環依賴

還有一種有些特殊的場景,比如我們需要在實例化Bean A之前,先實例化Bean B,這個時候就可以使用@DependsOn注解。

@DependsOn(value = "testService2") @Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } }

@DependsOn(value = "testService1") @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } }

程序啟動之後,執行結果:

Circular depends-on relationship between 'testService2' and 'testService1'

這個例子中本來如果TestService1和TestService2都沒有加@DependsOn注解是沒問題的,反而加了這個注解會出現循環依賴問題。

這又是為什麼?

答案在AbstractBeanFactory類的doGetBean方法的這段代碼中:

spring循環依賴怎麼使用(我是如何解決循環依賴的)14

它會檢查dependsOn的實例有沒有循環依賴,如果有循環依賴則抛異常。

4.出現循環依賴如何解決?

項目中如果出現循環依賴問題,說明是spring默認無法解決的循環依賴,要看項目的打印日志,屬于哪種循環依賴。目前包含下面幾種情況:

spring循環依賴怎麼使用(我是如何解決循環依賴的)15

生成代理對象産生的循環依賴

這類循環依賴問題解決方法很多,主要有:

  1. 使用@Lazy注解,延遲加載
  2. 使用@DependsOn注解,指定加載先後關系
  3. 修改文件名稱,改變循環依賴類的加載順序
使用@DependsOn産生的循環依賴

這類循環依賴問題要找到@DependsOn注解循環依賴的地方,迫使它不循環依賴就可以解決問題。

多例循環依賴

這類循環依賴問題可以通過把bean改成單例的解決。

構造器循環依賴

這類循環依賴問題可以通過使用@Lazy注解解決。

當然最好的解決循環依賴問題最佳方案是從代碼設計上規避,但是複雜的系統中有可能沒法避免。

,
Comments
Welcome to tft每日頭條 comments! Please keep conversations courteous and on-topic. To fosterproductive and respectful conversations, you may see comments from our Community Managers.
Sign up to post
Sort by
Show More Comments
推荐阅读
饑荒威爾遜怎麼玩
饑荒威爾遜怎麼玩
《饑荒》冒險模式大家喜歡玩嗎?今天就為大家帶來了饑荒威爾遜冒險模式玩法圖文解析,看看這位玩家的冒險模式怎麼玩的,喜歡冒險模式的玩家不要錯過,一起來看吧。進入了冒險模式,第一關是群島。第一個圖很巧的遇到第一個蟲洞,碰運去直接進入了,沒有探索其...
2024-10-05
娜紮未ps照
娜紮未ps照
2月13日,電影《不要忘記我愛你》“用一部電影告白”首映禮的一組照片發布在社交平台上,主演古力娜紮、劉以豪亮相活動,甜蜜互動,引發了網友的熱烈讨論。古力娜紮身穿一襲紅裙亮相首映禮現場,吊帶設計露出直角肩和纖瘦雙臂,精巧的鎖骨令人羨慕,身材十...
2024-10-05
火鍋店侵權賠償
火鍋店侵權賠償
河南商報記者韓忠林陳媛媛/文王訪賢/圖(此前資料圖)因為售賣了不起眼的小商品,而被廠家以“售賣假貨”的理由起訴到法庭,不少鄭州的商戶都曾遇到過這種情況。從河南商報的報道來看,這些東西有衛生巾、毛巾、白酒、保溫杯、地漏、刀片等。商戶們在收到傳...
2024-10-05
日産藍鳥多久後會降價
日産藍鳥多久後會降價
如果說到緊湊級的家用車,大家最先想到的肯定就是軒逸、朗逸、卡羅拉這三款銷量榜前三,不過今天的主角和它三都無關,而是講講藍鳥這款車,作為與軒逸同平台的産物,為何銷量慘淡,上個月僅賣出134輛,同級中排到了第90名。實拍的車型為2021款1.6...
2024-10-05
關于進一步開展
關于進一步開展
關于進一步開展?各旗縣區、經濟開發區文明辦,各旗縣區、經濟開發區婦聯,各級志願者團體:,我來為大家講解一下關于關于進一步開展?跟着小編一起來看一看吧!關于進一步開展各旗縣區、經濟開發區文明辦,各旗縣區、經濟開發區婦聯,各級志願者團體:在當前...
2024-10-05
Copyright 2023-2024 - www.tftnews.com All Rights Reserved