Spring如何通過(guò)@Lazy注解解決構(gòu)造方法循環(huán)依賴問(wèn)題
什么是循環(huán)依賴?
先定義兩個(gè)類 Apple、Orange,如下所示:
@Component public class Apple{ @Autowired private Orange orange; } @Component public class Orange { @Autowired private Apple apple; }
像這種在 Apple 里面有一個(gè)屬性 Orange、Orange 中有一個(gè)屬性 Apple,你中有我,我中有你,這樣可以稱之為循環(huán)依賴。循環(huán)依賴問(wèn)題不止在 Spring 中有,在 Mybatis 中也有,解決思想基本一樣,都需要借助額外的緩存進(jìn)行實(shí)現(xiàn)。
Spring 對(duì)于這種屬性注入的循環(huán)依賴是支持的,不會(huì)有任何問(wèn)題,今天這里探討一下 Spring 中構(gòu)造方法的循環(huán)依賴問(wèn)題,Spring 默認(rèn)是不支持的,但是也提供了方法解決。
構(gòu)造方法循環(huán)依賴
同樣把上面 Apple、Orange 兩個(gè)類改造下,如下所示:
@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)造方法"); } }
測(cè)試類如下:
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)依賴問(wèn)題呢?可以通過(guò)加 @Lazy 注解,但是也需要注意一些細(xì)節(jié)(后面我們會(huì)分析到),這里先看加 @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ù)上也行,都表示一個(gè)意思,就是后面會(huì)臨時(shí)創(chuàng)建出 Orange 的代理對(duì)象
@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)造方法"); } }
加上之后測(cè)試結(jié)果正常輸出,如下:
=====> 調(diào)用 Apple 構(gòu)造方法
======>調(diào)用 Orange 構(gòu)造方法
那么為什么加上 @Lazy 注解就能夠解決這樣一個(gè)問(wèn)題呢?繼續(xù)往下分析,這里我們主要看加上這個(gè) @Lazy 注解的執(zhí)行流程是什么樣的?
@Lazy 執(zhí)行流程源碼分析
首先第一次過(guò)來(lái)的是 Apple 類,先從緩存中查詢是否有實(shí)例化的對(duì)象,源碼如下:
第一次過(guò)來(lái)很顯然是沒(méi)有的,然后就要開(kāi)始去創(chuàng)建實(shí)例,但是創(chuàng)建實(shí)例 Spring 會(huì)做一個(gè)標(biāo)識(shí),避免重復(fù)創(chuàng)建實(shí)例,這個(gè)標(biāo)識(shí)標(biāo)識(shí)這個(gè) Apple 類正在創(chuàng)建中,當(dāng)創(chuàng)建成功之后就會(huì)刪除,此時(shí)創(chuàng)建好的實(shí)例就會(huì)存在緩存中。
記錄標(biāo)識(shí)源碼如下:注意如果標(biāo)識(shí) singletonsCurrentlyInCreation 容器中已經(jīng)存在,那么會(huì)直接添加失敗,拋出 BeanCurrentlyInCreationException 異常
異常信息也是大家非常熟悉的循環(huán)依賴問(wèn)題,源碼如下:
接著要開(kāi)始創(chuàng)建實(shí)例,如下所示:
會(huì)選擇出一個(gè)合適的構(gòu)造方法進(jìn)行實(shí)例化,由于我們只有且僅有一個(gè)構(gòu)造方法,所以肯定就用這個(gè)唯一的構(gòu)造方法了,然后就開(kāi)始進(jìn)入 autowireConstructor() 屬性注入環(huán)節(jié)
拿到構(gòu)造方法中所有的參數(shù),對(duì)每個(gè)參數(shù)一一遍歷進(jìn)行賦值操作,那么就要格外關(guān)注這個(gè)方法是怎么做的了
進(jìn)入 createArgumentArray() 方法內(nèi)部邏輯,源碼如下(核心部分,無(wú)關(guān)代碼省略):
很顯然這里是采用 for 循環(huán)對(duì)構(gòu)造方法中參數(shù)一一賦值,所以構(gòu)造方法中如果參數(shù)過(guò)多,性能也會(huì)降低許多,這個(gè)得注意了
看到 resolveDependency() 方法一定要有一種意識(shí),就是極大可能要出發(fā) getBean() 操作了,除了代理不會(huì)觸發(fā),但是也不一定(后面會(huì)講到)
然后下面這一段代碼是加了 @Lazy 注解的關(guān)鍵處理邏輯了(這段邏輯非常非常重要):
1、就是先判斷構(gòu)造方法中(構(gòu)造方法上,或者參數(shù)上)是否標(biāo)注了 @Lazy 注解,如果標(biāo)注了就會(huì)創(chuàng)建代理對(duì)象,不會(huì)立即觸發(fā) getBean() 操作
2、反之,就是走正常的邏輯,直接調(diào)用 getBean() ,但是這樣就會(huì)直接報(bào)異常了,因?yàn)?Spring 是不支持構(gòu)造方法的循環(huán)依賴的(還沒(méi)有來(lái)得及把半 Apple 類的半成品放到三級(jí)緩存),只有加了 @Lazy 注解臨時(shí)通過(guò)代理方法可以解決構(gòu)造方法循環(huán)依賴
然后進(jìn)入 getLazyResolutionProxyIfNecessary() 方法看是怎么判斷,和要怎么創(chuàng)建代理對(duì)象的,源碼如下:
可以清楚的看到 @Lazy 為什么可以標(biāo)注在構(gòu)造方法上和構(gòu)造方法的入?yún)⑸厦鎯煞N方式,可以從下面這段源碼中知道答案,如下所示:
找到了 @Lazy 注解就會(huì)通過(guò) buildLazyResolutionProxy() 方法去創(chuàng)建這個(gè)入?yún)⒌拇韺?duì)象,如下所示:
代理對(duì)象就是對(duì)原始目標(biāo)類的一種增強(qiáng),注意當(dāng)使用代理對(duì)象調(diào)用它的方法時(shí)會(huì)回調(diào)到 getTarget() 方法,這個(gè) getTarget() 方法中調(diào)用了 doResolveDependency() 方法,這個(gè)方法會(huì)觸發(fā)調(diào)用 getBean() 流程實(shí)例化 bean,要格外注意,后面會(huì)演示如何調(diào)用到這個(gè)方法的。
代理對(duì)象已經(jīng)創(chuàng)建好了,現(xiàn)在準(zhǔn)備通過(guò)構(gòu)造方法反射調(diào)用實(shí)例化 Apple 類即可,源碼如下:
其中 argsWithDefaultValues 值是 Orange 的一個(gè)代理對(duì)象,實(shí)例化好之后相當(dāng)于 Apple 已經(jīng)創(chuàng)建好了,然后放入到三級(jí)緩存中,如下所示:
然后就是刪除 singletonsCurrentlyInCreation 標(biāo)識(shí)容器中的標(biāo)識(shí)位(因?yàn)橐呀?jīng)實(shí)例化完成了,所以標(biāo)識(shí)位可以抹除),如下所示:
最后在把 Apple 實(shí)例化好的 bean 從三級(jí)緩存中刪除,然后移動(dòng)到一級(jí)緩存中,也就是我們經(jīng)常所說(shuō)的單例緩沖池中,如下所示:
至此,Apple 類實(shí)例化 bean 就已經(jīng)在 Spring 的單例緩沖池中存在了,其他地方如果想要使用直接從這個(gè)單例緩沖池中取值即可。
那么當(dāng) Orange 類過(guò)來(lái)實(shí)例化的時(shí)候,也是先從容器中查找是否有實(shí)例化 bean 存在,源碼如下:
然后打標(biāo)記,源碼如下:
異常信息也是大家非常熟悉的循環(huán)依賴問(wèn)題,源碼如下:
接著要開(kāi)始創(chuàng)建實(shí)例,如下所示:
會(huì)選擇出一個(gè)合適的構(gòu)造方法進(jìn)行實(shí)例化,由于我們只有且僅有一個(gè)構(gòu)造方法,所以肯定就用這個(gè)唯一的構(gòu)造方法了,然后就開(kāi)始進(jìn)入 autowireConstructor() 屬性注入環(huán)節(jié)
拿到構(gòu)造方法中所有的參數(shù),對(duì)每個(gè)參數(shù)一一遍歷進(jìn)行賦值操作,那么就要格外關(guān)注這個(gè)方法是怎么做的了
然后進(jìn)入代碼核心邏輯,此時(shí)因?yàn)樵?Orange 的構(gòu)造方法中是沒(méi)有標(biāo)注 @Lazy 注解的,所以這里不會(huì)進(jìn)入創(chuàng)建代理的邏輯,而知直接進(jìn)入 doResolveDependency() 邏輯,前面已經(jīng)提到很多遍歷,這個(gè)方法很重要,會(huì)觸發(fā)到 getBean() 流程。
那么 Orange 構(gòu)造方法中的入?yún)?Apple,Apple 在第一遍的時(shí)候就已經(jīng)在單例緩沖池中存在了,所以 Apple 在執(zhí)行 getBean() 流程的時(shí)候,直接就會(huì)從一級(jí)緩存中獲取到 Apple 實(shí)例化好的對(duì)象,賦值給 Orange 構(gòu)造方法中的 Apple
變量。
然后表示就是 Orange 通過(guò)反射調(diào)用構(gòu)造方法實(shí)例化 Orange 實(shí)例
然后后面的流程 Orange 也是要放入到三級(jí)緩存中,然后刪除標(biāo)識(shí)位,最后將 Orange 實(shí)例從三級(jí)緩存中刪除,移動(dòng)到一級(jí)緩存(單例緩存池)中。
至此 Apple、Orange 兩個(gè)構(gòu)造方法的循環(huán)依賴就分析完成了,下面是稍微改動(dòng)一點(diǎn),繼續(xù)分析。
使用 @Lazy 注解注意事項(xiàng)(特別小心)
將 Apple、Orange 類稍微變動(dòng)一下,如下所示:
@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)過(guò)上的分析,Apple 構(gòu)造方法中 @Lazy 注解修飾的 Orange,會(huì)創(chuàng)建一個(gè)代理對(duì)象來(lái)規(guī)避入?yún)?orange 調(diào)用 getBean() 流程,從而解決循環(huán)依賴問(wèn)題,現(xiàn)在我們?cè)?Apple 構(gòu)造方法中,直接把 orange 打印出來(lái)。
經(jīng)過(guò)測(cè)試直接報(bào)錯(cuò),錯(cuò)誤如下:
發(fā)現(xiàn)還是發(fā)生了循環(huán)依賴問(wèn)題,下面具體分析下是為什么呢?前面分析過(guò)的下面都會(huì)直接通過(guò)簡(jiǎn)短描述直接帶過(guò)
1、Apple 類首先會(huì)去緩存中查找是否已經(jīng)實(shí)例化 bean,第一次很顯然沒(méi)有
2、開(kāi)始記錄標(biāo)記位
3、調(diào)用 createBeanInstance() 方法實(shí)例化對(duì)象
4、給 Apple 類構(gòu)造方法的入?yún)⑦M(jìn)行屬性賦值,會(huì)創(chuàng)建代理類,如下所示:
注意這里面的 getTarget() 方法,下面會(huì)回調(diào)到這里,現(xiàn)在代碼繼續(xù)往后走,代理類創(chuàng)建好之后就要開(kāi)始通過(guò)反射調(diào)用構(gòu)造方法創(chuàng)建實(shí)例了,源碼如下:
注意此時(shí)的 argsWithDefaultValues 是 Orange 代理對(duì)象,當(dāng)我們通過(guò)反射調(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í)行輸出語(yǔ)句,打印出"=====> 調(diào)用 Apple 構(gòu)造方法"
,然后再打印下一句語(yǔ)句時(shí),請(qǐng)注意,orange 是一個(gè)代理對(duì)象,在 JVM 執(zhí)行這條輸出語(yǔ)句的時(shí)候,其實(shí)默認(rèn)調(diào)用了 toString() 方法,你要知道在創(chuàng)建代理對(duì)象的時(shí)候,并沒(méi)有限定哪個(gè)方法增強(qiáng),而是對(duì)整個(gè) Orange 中的方法增強(qiáng)了,所以在你輸出 Orange 的時(shí)候就會(huì)觸發(fā)代理對(duì)象的對(duì) toString() 方法的增強(qiáng),所以會(huì)回調(diào)到代理對(duì)象中的 intercept() 方法,然后再 intercept() 方法中有調(diào)用了 getTarget() 方法,注意哦在創(chuàng)建代理對(duì)象的時(shí)候,我特意說(shuō)明要注意 getTarget() 方法,因?yàn)楝F(xiàn)在就要被回調(diào)到了,恰好 getTarget() 方法中又會(huì)觸發(fā) getBean() 流程,所以最終又導(dǎo)致循環(huán)依賴問(wèn)題的產(chǎn)生。
對(duì)于 Apple 類構(gòu)造方法中的入?yún)?Orange, Spring 是通過(guò) cglib 進(jìn)行代理對(duì)象創(chuàng)建的,具體看 CglibAopProxy 類就知道為什么在執(zhí)行 toString() 方法最終會(huì)回調(diào)到 getTarget() 方法,這里就截取一段核心代碼,如下:
那么怎么解決這個(gè)問(wèn)題呢?
1、在 Apple 類構(gòu)造方法中不要調(diào)用任何代理對(duì)象的方法,比如這樣使用,如下所示:
@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 代理對(duì)象,并沒(méi)有調(diào)用任何 API,所以不會(huì)觸發(fā)代理對(duì)象執(zhí)行增強(qiáng)邏輯。
2、繼續(xù)在 Apple 構(gòu)造方法中觸發(fā)代理對(duì)象回調(diào)(調(diào)用 toString() 等方法),此時(shí)會(huì)出現(xiàn)循環(huán)依賴問(wèn)題,就是因?yàn)榉椒?toString() 的增強(qiáng)邏輯觸發(fā)了 Orange 的 getBean() 操作,然后 Orange 實(shí)例化時(shí),又觸發(fā)了 Orange 構(gòu)造方法中的入?yún)?Apple 類的實(shí)例化,此時(shí)你要知道 Apple 類還沒(méi)有實(shí)例化完成呢,緩存中壓根也還沒(méi)有,Apple 類現(xiàn)還停留在System.out.println("======>orange="+orange);
輸出語(yǔ)句呢
所以說(shuō)到這里了,我們也可以在 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 賦值先給定一個(gè)代理對(duì)象,避免 Apple 類觸發(fā) getBean() 操作,這樣 Orange 構(gòu)造方法的入?yún)⒕拖喈?dāng)于賦上值,那么 Orange 類就完成了實(shí)例化,代碼回調(diào)上層調(diào)用處,就是 Apple 類構(gòu)造方法中的輸出語(yǔ)句 System.out.println("======>orange="+orange);
這條輸出語(yǔ)句執(zhí)行完,相當(dāng)于 Apple 類構(gòu)造方法也實(shí)例化完成,從而沒(méi)有發(fā)生循環(huán)依賴問(wèn)題。
但是如果在將 Apple、Orange 類變動(dòng)一下,如下所示:
@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); } }
這樣是絕對(duì)沒(méi)辦法解決了,因?yàn)橄喈?dāng)于 @Lazy 注解沒(méi)有加上一樣,每個(gè)構(gòu)造方法中都會(huì)立即觸發(fā) getBean() 操作,此時(shí)以為緩存中根本還沒(méi)來(lái)得及放入實(shí)例化 bean。
以上只是個(gè)人對(duì) @Lazy 的理解,僅供參考。
總結(jié)
在構(gòu)造方法循環(huán)依賴問(wèn)題中,通過(guò) @Lazy 注解,只是臨時(shí)創(chuàng)建一個(gè)代理對(duì)象來(lái)為屬性賦值,避免觸發(fā)二次 getBean() 調(diào)用。
并且注意代理對(duì)象和被 @Lazy 修飾的類的實(shí)例并不是同一個(gè),完全是兩個(gè)對(duì)象,可以輸出 hashCode() 編碼即可查看,不能使用 toString() 來(lái)做驗(yàn)證,因?yàn)榇韺?duì)象會(huì)回調(diào)到切面邏輯,然后觸發(fā) getBean() 實(shí)例化 @Lazy 修飾的類,然后最終通過(guò) toString() 方法輸出的結(jié)果都是一樣的 com.gwm.circle.Banana@5149d738
,然后你就會(huì)誤認(rèn)為代理對(duì)象和被 @Lazy 修飾類的真正對(duì)象是相同的,其實(shí)并不相同,代理對(duì)象是代理對(duì)象,和真正實(shí)例完全是兩個(gè)對(duì)象!
到此這篇關(guān)于Spring如何通過(guò)@Lazy注解解決構(gòu)造方法循環(huán)依賴問(wèn)題的文章就介紹到這了,更多相關(guān)Spring解決構(gòu)造方法循環(huán)依賴內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud?Ribbon負(fù)載均衡流程分析
在Eureka注冊(cè)中心中我們?cè)谔砑油闌LoadBalanced注解,即可實(shí)現(xiàn)負(fù)載均衡功能,現(xiàn)在一起探索一下負(fù)載均衡的原理(Ribbon),感興趣的朋友一起看看吧2024-03-03深入了解Spring中的@Autowired和@Resource注解
Spring中的@Autowired和@Resource注解都可以實(shí)現(xiàn)依賴注入,但使用方式、注入策略和適用場(chǎng)景略有不同。本文將深入探討這兩種注解的原理、使用方法及優(yōu)缺點(diǎn),幫助讀者更好地理解和運(yùn)用Spring依賴注入機(jī)制2023-04-04Request的包裝類HttpServletRequestWrapper的使用說(shuō)明
這篇文章主要介紹了Request的包裝類HttpServletRequestWrapper的使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08java?使用BeanFactory實(shí)現(xiàn)service與dao層解耦合詳解
這篇文章主要介紹了java?使用BeanFactory實(shí)現(xiàn)service與dao層解耦合詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12使用SpringAop動(dòng)態(tài)獲取mapper執(zhí)行的SQL,并保存SQL到Log表中
這篇文章主要介紹了使用SpringAop動(dòng)態(tài)獲取mapper執(zhí)行的SQL,并保存SQL到Log表中問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Spring boot如何配置請(qǐng)求的入?yún)⒑统鰠son數(shù)據(jù)格式
這篇文章主要介紹了spring boot如何配置請(qǐng)求的入?yún)⒑统鰠son數(shù)據(jù)格式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11基于SpringBoot實(shí)現(xiàn)大文件分塊上傳功能
這篇文章主要介紹了基于SpringBoot實(shí)現(xiàn)大文件分塊上傳功能,實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,核心就是客戶端把大文件按照一定規(guī)則進(jìn)行拆分,比如20MB為一個(gè)小塊,分解成一個(gè)一個(gè)的文件塊,然后把這些文件塊單獨(dú)上傳到服務(wù)端,需要的朋友可以參考下2024-09-09