Spring如何通過@Lazy注解解決構(gòu)造方法循環(huán)依賴問題
什么是循環(huán)依賴?
先定義兩個類 Apple、Orange,如下所示:
@Component
public class Apple{
@Autowired
private Orange orange;
}
@Component
public class Orange {
@Autowired
private Apple apple;
}
像這種在 Apple 里面有一個屬性 Orange、Orange 中有一個屬性 Apple,你中有我,我中有你,這樣可以稱之為循環(huán)依賴。循環(huán)依賴問題不止在 Spring 中有,在 Mybatis 中也有,解決思想基本一樣,都需要借助額外的緩存進(jìn)行實(shí)現(xiàn)。
Spring 對于這種屬性注入的循環(huán)依賴是支持的,不會有任何問題,今天這里探討一下 Spring 中構(gòu)造方法的循環(huán)依賴問題,Spring 默認(rèn)是不支持的,但是也提供了方法解決。
構(gòu)造方法循環(huán)依賴
同樣把上面 Apple、Orange 兩個類改造下,如下所示:
@Component
public class Apple{
public Apple(Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
}
}
@Component
public class Orange {
public Orange(Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}
測試類如下:
public class TestCircleMain {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(CircleConfig.class);
Orange orange = context.getBean( Orange.class);
Apple bean = context.getBean(Apple.class);
}
}
發(fā)現(xiàn)直接就拋出了循環(huán)依賴異常如下:

很顯然構(gòu)造方法的循環(huán)依賴,Spring 是不太支持的,但是我非要這樣使用,怎么解決呢?
加 @Lazy 注解解決構(gòu)造方法循環(huán)依賴
具體怎么解決構(gòu)造方法循環(huán)依賴問題呢?可以通過加 @Lazy 注解,但是也需要注意一些細(xì)節(jié)(后面我們會分析到),這里先看加 @Lazy 注解之后能夠解決構(gòu)造方法循環(huán)依賴的案例,如下所示:
@Component
public class Apple{
@Lazy
public Apple(Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
}
}
@Component
public class Orange {
public Orange(Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}
或者加載參數(shù)上也行,都表示一個意思,就是后面會臨時(shí)創(chuàng)建出 Orange 的代理對象
@Component
public class Apple{
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
}
}
@Component
public class Orange {
public Orange(Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}
加上之后測試結(jié)果正常輸出,如下:
=====> 調(diào)用 Apple 構(gòu)造方法
======>調(diào)用 Orange 構(gòu)造方法
那么為什么加上 @Lazy 注解就能夠解決這樣一個問題呢?繼續(xù)往下分析,這里我們主要看加上這個 @Lazy 注解的執(zhí)行流程是什么樣的?
@Lazy 執(zhí)行流程源碼分析
首先第一次過來的是 Apple 類,先從緩存中查詢是否有實(shí)例化的對象,源碼如下:


第一次過來很顯然是沒有的,然后就要開始去創(chuàng)建實(shí)例,但是創(chuàng)建實(shí)例 Spring 會做一個標(biāo)識,避免重復(fù)創(chuàng)建實(shí)例,這個標(biāo)識標(biāo)識這個 Apple 類正在創(chuàng)建中,當(dāng)創(chuàng)建成功之后就會刪除,此時(shí)創(chuàng)建好的實(shí)例就會存在緩存中。
記錄標(biāo)識源碼如下:注意如果標(biāo)識 singletonsCurrentlyInCreation 容器中已經(jīng)存在,那么會直接添加失敗,拋出 BeanCurrentlyInCreationException 異常


異常信息也是大家非常熟悉的循環(huán)依賴問題,源碼如下:

接著要開始創(chuàng)建實(shí)例,如下所示:

會選擇出一個合適的構(gòu)造方法進(jìn)行實(shí)例化,由于我們只有且僅有一個構(gòu)造方法,所以肯定就用這個唯一的構(gòu)造方法了,然后就開始進(jìn)入 autowireConstructor() 屬性注入環(huán)節(jié)

拿到構(gòu)造方法中所有的參數(shù),對每個參數(shù)一一遍歷進(jìn)行賦值操作,那么就要格外關(guān)注這個方法是怎么做的了

進(jìn)入 createArgumentArray() 方法內(nèi)部邏輯,源碼如下(核心部分,無關(guān)代碼省略):
很顯然這里是采用 for 循環(huán)對構(gòu)造方法中參數(shù)一一賦值,所以構(gòu)造方法中如果參數(shù)過多,性能也會降低許多,這個得注意了

看到 resolveDependency() 方法一定要有一種意識,就是極大可能要出發(fā) getBean() 操作了,除了代理不會觸發(fā),但是也不一定(后面會講到)

然后下面這一段代碼是加了 @Lazy 注解的關(guān)鍵處理邏輯了(這段邏輯非常非常重要):
1、就是先判斷構(gòu)造方法中(構(gòu)造方法上,或者參數(shù)上)是否標(biāo)注了 @Lazy 注解,如果標(biāo)注了就會創(chuàng)建代理對象,不會立即觸發(fā) getBean() 操作
2、反之,就是走正常的邏輯,直接調(diào)用 getBean() ,但是這樣就會直接報(bào)異常了,因?yàn)?Spring 是不支持構(gòu)造方法的循環(huán)依賴的(還沒有來得及把半 Apple 類的半成品放到三級緩存),只有加了 @Lazy 注解臨時(shí)通過代理方法可以解決構(gòu)造方法循環(huán)依賴

然后進(jìn)入 getLazyResolutionProxyIfNecessary() 方法看是怎么判斷,和要怎么創(chuàng)建代理對象的,源碼如下:

可以清楚的看到 @Lazy 為什么可以標(biāo)注在構(gòu)造方法上和構(gòu)造方法的入?yún)⑸厦鎯煞N方式,可以從下面這段源碼中知道答案,如下所示:

找到了 @Lazy 注解就會通過 buildLazyResolutionProxy() 方法去創(chuàng)建這個入?yún)⒌拇韺ο?,如下所示?/p>
代理對象就是對原始目標(biāo)類的一種增強(qiáng),注意當(dāng)使用代理對象調(diào)用它的方法時(shí)會回調(diào)到 getTarget() 方法,這個 getTarget() 方法中調(diào)用了 doResolveDependency() 方法,這個方法會觸發(fā)調(diào)用 getBean() 流程實(shí)例化 bean,要格外注意,后面會演示如何調(diào)用到這個方法的。

代理對象已經(jīng)創(chuàng)建好了,現(xiàn)在準(zhǔn)備通過構(gòu)造方法反射調(diào)用實(shí)例化 Apple 類即可,源碼如下:

其中 argsWithDefaultValues 值是 Orange 的一個代理對象,實(shí)例化好之后相當(dāng)于 Apple 已經(jīng)創(chuàng)建好了,然后放入到三級緩存中,如下所示:


然后就是刪除 singletonsCurrentlyInCreation 標(biāo)識容器中的標(biāo)識位(因?yàn)橐呀?jīng)實(shí)例化完成了,所以標(biāo)識位可以抹除),如下所示:


最后在把 Apple 實(shí)例化好的 bean 從三級緩存中刪除,然后移動到一級緩存中,也就是我們經(jīng)常所說的單例緩沖池中,如下所示:


至此,Apple 類實(shí)例化 bean 就已經(jīng)在 Spring 的單例緩沖池中存在了,其他地方如果想要使用直接從這個單例緩沖池中取值即可。
那么當(dāng) Orange 類過來實(shí)例化的時(shí)候,也是先從容器中查找是否有實(shí)例化 bean 存在,源碼如下:


然后打標(biāo)記,源碼如下:


異常信息也是大家非常熟悉的循環(huán)依賴問題,源碼如下:

接著要開始創(chuàng)建實(shí)例,如下所示:

會選擇出一個合適的構(gòu)造方法進(jìn)行實(shí)例化,由于我們只有且僅有一個構(gòu)造方法,所以肯定就用這個唯一的構(gòu)造方法了,然后就開始進(jìn)入 autowireConstructor() 屬性注入環(huán)節(jié)

拿到構(gòu)造方法中所有的參數(shù),對每個參數(shù)一一遍歷進(jìn)行賦值操作,那么就要格外關(guān)注這個方法是怎么做的了

然后進(jìn)入代碼核心邏輯,此時(shí)因?yàn)樵?Orange 的構(gòu)造方法中是沒有標(biāo)注 @Lazy 注解的,所以這里不會進(jìn)入創(chuàng)建代理的邏輯,而知直接進(jìn)入 doResolveDependency() 邏輯,前面已經(jīng)提到很多遍歷,這個方法很重要,會觸發(fā)到 getBean() 流程。

那么 Orange 構(gòu)造方法中的入?yún)?Apple,Apple 在第一遍的時(shí)候就已經(jīng)在單例緩沖池中存在了,所以 Apple 在執(zhí)行 getBean() 流程的時(shí)候,直接就會從一級緩存中獲取到 Apple 實(shí)例化好的對象,賦值給 Orange 構(gòu)造方法中的 Apple
變量。

然后表示就是 Orange 通過反射調(diào)用構(gòu)造方法實(shí)例化 Orange 實(shí)例

然后后面的流程 Orange 也是要放入到三級緩存中,然后刪除標(biāo)識位,最后將 Orange 實(shí)例從三級緩存中刪除,移動到一級緩存(單例緩存池)中。
至此 Apple、Orange 兩個構(gòu)造方法的循環(huán)依賴就分析完成了,下面是稍微改動一點(diǎn),繼續(xù)分析。
使用 @Lazy 注解注意事項(xiàng)(特別小心)
將 Apple、Orange 類稍微變動一下,如下所示:
@Component
public class Apple{
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
System.out.println("======>orange="+orange);
}
}
@Component
public class Orange {
public Orange(Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}經(jīng)過上的分析,Apple 構(gòu)造方法中 @Lazy 注解修飾的 Orange,會創(chuàng)建一個代理對象來規(guī)避入?yún)?orange 調(diào)用 getBean() 流程,從而解決循環(huán)依賴問題,現(xiàn)在我們在 Apple 構(gòu)造方法中,直接把 orange 打印出來。
經(jīng)過測試直接報(bào)錯,錯誤如下:

發(fā)現(xiàn)還是發(fā)生了循環(huán)依賴問題,下面具體分析下是為什么呢?前面分析過的下面都會直接通過簡短描述直接帶過
1、Apple 類首先會去緩存中查找是否已經(jīng)實(shí)例化 bean,第一次很顯然沒有
2、開始記錄標(biāo)記位
3、調(diào)用 createBeanInstance() 方法實(shí)例化對象
4、給 Apple 類構(gòu)造方法的入?yún)⑦M(jìn)行屬性賦值,會創(chuàng)建代理類,如下所示:

注意這里面的 getTarget() 方法,下面會回調(diào)到這里,現(xiàn)在代碼繼續(xù)往后走,代理類創(chuàng)建好之后就要開始通過反射調(diào)用構(gòu)造方法創(chuàng)建實(shí)例了,源碼如下:

注意此時(shí)的 argsWithDefaultValues 是 Orange 代理對象,當(dāng)我們通過反射調(diào)用 Apple 的構(gòu)造方法時(shí),立即回調(diào)到 Apple 的構(gòu)造方法中的邏輯,如下所示:
@Component
public class Apple{
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
System.out.println("======>orange="+orange);
}
}先執(zhí)行輸出語句,打印出"=====> 調(diào)用 Apple 構(gòu)造方法",然后再打印下一句語句時(shí),請注意,orange 是一個代理對象,在 JVM 執(zhí)行這條輸出語句的時(shí)候,其實(shí)默認(rèn)調(diào)用了 toString() 方法,你要知道在創(chuàng)建代理對象的時(shí)候,并沒有限定哪個方法增強(qiáng),而是對整個 Orange 中的方法增強(qiáng)了,所以在你輸出 Orange 的時(shí)候就會觸發(fā)代理對象的對 toString() 方法的增強(qiáng),所以會回調(diào)到代理對象中的 intercept() 方法,然后再 intercept() 方法中有調(diào)用了 getTarget() 方法,注意哦在創(chuàng)建代理對象的時(shí)候,我特意說明要注意 getTarget() 方法,因?yàn)楝F(xiàn)在就要被回調(diào)到了,恰好 getTarget() 方法中又會觸發(fā) getBean() 流程,所以最終又導(dǎo)致循環(huán)依賴問題的產(chǎn)生。
對于 Apple 類構(gòu)造方法中的入?yún)?Orange, Spring 是通過 cglib 進(jìn)行代理對象創(chuàng)建的,具體看 CglibAopProxy 類就知道為什么在執(zhí)行 toString() 方法最終會回調(diào)到 getTarget() 方法,這里就截取一段核心代碼,如下:
那么怎么解決這個問題呢?
1、在 Apple 類構(gòu)造方法中不要調(diào)用任何代理對象的方法,比如這樣使用,如下所示:
@Component
public class Apple{
private Orange orange;
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
this.orange = orange;
}
public void sop() {
System.out.println("this.orange = " + this.orange);
}
}
@Component
public class Orange {
private Apple apple;
public Orange(Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}
我只是在 Apple 構(gòu)造方法中使用了一下 Orange 代理對象,并沒有調(diào)用任何 API,所以不會觸發(fā)代理對象執(zhí)行增強(qiáng)邏輯。
2、繼續(xù)在 Apple 構(gòu)造方法中觸發(fā)代理對象回調(diào)(調(diào)用 toString() 等方法),此時(shí)會出現(xiàn)循環(huán)依賴問題,就是因?yàn)榉椒?toString() 的增強(qiáng)邏輯觸發(fā)了 Orange 的 getBean() 操作,然后 Orange 實(shí)例化時(shí),又觸發(fā)了 Orange 構(gòu)造方法中的入?yún)?Apple 類的實(shí)例化,此時(shí)你要知道 Apple 類還沒有實(shí)例化完成呢,緩存中壓根也還沒有,Apple 類現(xiàn)還停留在System.out.println("======>orange="+orange); 輸出語句呢
所以說到這里了,我們也可以在 Orange 中加上 @Lazy 注解,如下所示:
@Component
public class Apple{
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
System.out.println("this.orange = " + this.orange);
}
public void sop() {
System.out.println("this.orange = " + this.orange);
}
}
@Component
public class Orange {
private Apple apple;
public Orange(@Lazy Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
}
}當(dāng)給 Orange 類構(gòu)造方法中入?yún)?Apple 賦值先給定一個代理對象,避免 Apple 類觸發(fā) getBean() 操作,這樣 Orange 構(gòu)造方法的入?yún)⒕拖喈?dāng)于賦上值,那么 Orange 類就完成了實(shí)例化,代碼回調(diào)上層調(diào)用處,就是 Apple 類構(gòu)造方法中的輸出語句 System.out.println("======>orange="+orange); 這條輸出語句執(zhí)行完,相當(dāng)于 Apple 類構(gòu)造方法也實(shí)例化完成,從而沒有發(fā)生循環(huán)依賴問題。
但是如果在將 Apple、Orange 類變動一下,如下所示:
@Component
public class Apple{
public Apple(@Lazy Orange orange) {
System.out.println("=====> 調(diào)用 Apple 構(gòu)造方法");
System.out.println("this.orange = " + this.orange);
}
}
@Component
public class Orange {
private Apple apple;
public Orange(@Lazy Apple apple) {
System.out.println("======>調(diào)用 Orange 構(gòu)造方法");
System.out.println("this.orange = " + this.orange);
}
}這樣是絕對沒辦法解決了,因?yàn)橄喈?dāng)于 @Lazy 注解沒有加上一樣,每個構(gòu)造方法中都會立即觸發(fā) getBean() 操作,此時(shí)以為緩存中根本還沒來得及放入實(shí)例化 bean。
以上只是個人對 @Lazy 的理解,僅供參考。
總結(jié)
在構(gòu)造方法循環(huán)依賴問題中,通過 @Lazy 注解,只是臨時(shí)創(chuàng)建一個代理對象來為屬性賦值,避免觸發(fā)二次 getBean() 調(diào)用。
并且注意代理對象和被 @Lazy 修飾的類的實(shí)例并不是同一個,完全是兩個對象,可以輸出 hashCode() 編碼即可查看,不能使用 toString() 來做驗(yàn)證,因?yàn)榇韺ο髸卣{(diào)到切面邏輯,然后觸發(fā) getBean() 實(shí)例化 @Lazy 修飾的類,然后最終通過 toString() 方法輸出的結(jié)果都是一樣的 com.gwm.circle.Banana@5149d738,然后你就會誤認(rèn)為代理對象和被 @Lazy 修飾類的真正對象是相同的,其實(shí)并不相同,代理對象是代理對象,和真正實(shí)例完全是兩個對象!
到此這篇關(guān)于Spring如何通過@Lazy注解解決構(gòu)造方法循環(huán)依賴問題的文章就介紹到這了,更多相關(guān)Spring解決構(gòu)造方法循環(huán)依賴內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud?Ribbon負(fù)載均衡流程分析
在Eureka注冊中心中我們在添加完@LoadBalanced注解,即可實(shí)現(xiàn)負(fù)載均衡功能,現(xiàn)在一起探索一下負(fù)載均衡的原理(Ribbon),感興趣的朋友一起看看吧2024-03-03
深入了解Spring中的@Autowired和@Resource注解
Spring中的@Autowired和@Resource注解都可以實(shí)現(xiàn)依賴注入,但使用方式、注入策略和適用場景略有不同。本文將深入探討這兩種注解的原理、使用方法及優(yōu)缺點(diǎn),幫助讀者更好地理解和運(yùn)用Spring依賴注入機(jī)制2023-04-04
Request的包裝類HttpServletRequestWrapper的使用說明
這篇文章主要介紹了Request的包裝類HttpServletRequestWrapper的使用說明,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
java?使用BeanFactory實(shí)現(xiàn)service與dao層解耦合詳解
這篇文章主要介紹了java?使用BeanFactory實(shí)現(xiàn)service與dao層解耦合詳解,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
使用SpringAop動態(tài)獲取mapper執(zhí)行的SQL,并保存SQL到Log表中
這篇文章主要介紹了使用SpringAop動態(tài)獲取mapper執(zhí)行的SQL,并保存SQL到Log表中問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Spring boot如何配置請求的入?yún)⒑统鰠son數(shù)據(jù)格式
這篇文章主要介紹了spring boot如何配置請求的入?yún)⒑统鰠son數(shù)據(jù)格式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
基于SpringBoot實(shí)現(xiàn)大文件分塊上傳功能
這篇文章主要介紹了基于SpringBoot實(shí)現(xiàn)大文件分塊上傳功能,實(shí)現(xiàn)原理其實(shí)很簡單,核心就是客戶端把大文件按照一定規(guī)則進(jìn)行拆分,比如20MB為一個小塊,分解成一個一個的文件塊,然后把這些文件塊單獨(dú)上傳到服務(wù)端,需要的朋友可以參考下2024-09-09


