Spring 中的循環(huán)引用問題解決方法
本章來聊聊Spring 中的循環(huán)引用問題該如何解決。這里聊的很粗糙,并沒有那么細節(jié),只是大概了解了一點。
什么是循環(huán)引用?
如下圖所示:

圖中有兩個類,一個 Class A ,A 中又引用了 B,Class B 中又引用了 A 。(基于 setter 注入和字段注入)
當 Spring 容器想要創(chuàng)建 A 時發(fā)現(xiàn) A 中需要使用到 B 對象,此時就去創(chuàng)建 B ,在創(chuàng)建 B 對象時又發(fā)現(xiàn)還需要引用 A ,此時就發(fā)生循環(huán)引用了。
當然循環(huán)引用并非就是 A 引用 B ,B 引用 A 的情況,還有很多,如:

當然,此圖比較草率。
有了循環(huán)依賴就會有死循環(huán)的問題。
循環(huán)依賴
來看看死循環(huán)產(chǎn)生的過程:

為什么這里是半成品呢?要是熟悉 Spring-bean 的 生命周期就會知道,首先會去調(diào)用的是構(gòu)造函數(shù),像一些依賴注入啊,還有一些接口的實現(xiàn)的方法重寫啊還有后置處理器初始化方法,想這些都還沒有去執(zhí)行,所以說他還是一個半成品對象。
繼續(xù)向后執(zhí)行:

此時,在容器中找不到 A 對象啊,那么他又去實例化 A 了。此時死循環(huán)就產(chǎn)生了,這個就叫做 循環(huán)依賴。
三級緩存:
| 緩存級別 | 源碼名稱 | 作用 |
|---|---|---|
| 一級緩存 | singletonObjects | 存儲已經(jīng)完全初始化好的單例 Bean。當 Bean 初始化完成,所有依賴項都已注入,就會放入此緩存,供后續(xù)獲取 Bean 時直接使用,提高獲取單例 Bean 的效率。 簡單來說:單例池,緩存已經(jīng)經(jīng)歷了完整的生命周期,已經(jīng)初始化完成了的對象。 |
| 二級緩存 | earlySingletonObjects | 存儲提前暴露的原始 Bean,即已經(jīng)開始實例化但還未完成初始化(如未填充屬性)的 Bean。用于解決在構(gòu)造器注入過程中發(fā)生的循環(huán)依賴問題,確保在循環(huán)依賴情況下 Bean 只被創(chuàng)建一次,也能解決多線程并發(fā)下獲取不完整 Bean 的性能問題。 簡單來說:緩存早期的 bean 對象(生命周期未完成) |
| 三級緩存 | singletonFactories | 存儲 Bean 的 ObjectFactory,用于生成原始 Bean 的代理對象。當遇到循環(huán)依賴且涉及到代理對象創(chuàng)建時,Spring 會將 ObjectFactory 放入三級緩存,在后續(xù)需要時通過它來獲取 Bean 的實例,以處理代理相關(guān)情況,統(tǒng)一處理普通 Bean 和代理 Bean。 簡單來說:緩存 objectFactory,表示對象工廠,用來創(chuàng)建某個對象 |
我們一個個來說
三級緩存解決循環(huán)依賴
回到上個圖片:

按照一級緩存的邏輯,走完一個生命周期才能將對象存儲在緩存中啊(全是半成品),那么這個流程中必然是沒有對象存在緩存中的。
走到這里發(fā)現(xiàn)了,單憑一級緩存是沒有辦法解決循環(huán)依賴的。
要想打破這個循環(huán)依賴,需要一個中間人的參與(暫時存放一個半成品的 對象),這個中間人就是二級緩存。
二級緩存

我們重新走一遍流程:
實例化一個 A 對象,此時生成一個 原始對象 A(半成品的 A )只是開辟了空間,執(zhí)行了一個構(gòu)造方法
就將這個方法存入這個二級緩存中
發(fā)現(xiàn)創(chuàng)建 A 對象需要依賴 B 對象,就注入 B,B 又不存在,那么就實例化一個 B
B還是和之前的 A 一樣,同時也將 原始對象存入這個 二級緩存中
但是在 B 中注入 A 時,發(fā)現(xiàn)了緩存中有一個原始對象 A ,那么就可以打破循環(huán)依賴,使 B創(chuàng)建成功
此時 B 已經(jīng)創(chuàng)建成功了,就可以存儲到 一級緩存中了,那么也就可以將 B 注入進 A 了
A 也就可以創(chuàng)建成功,成功之后也需要存入一級緩存(此時二級緩存中的半成品對象也可以清理掉了)
此時似乎使用 二級緩存 + 一級緩存 就可以解決循環(huán)依賴的問題了啊,那還要三級緩存干嘛呢?
我們假設(shè),如果這個 A 是個代理對象呢?
兩個原因:
代理對象創(chuàng)建時機的問題:Spring 的代理對象通常是在 Bean 初始化過程的特定階段創(chuàng)建的(例如在初始化方法執(zhí)行之后)。在處理循環(huán)依賴時,如果僅使用一級和二級緩存,當一個 Bean 在創(chuàng)建過程中需要依賴另一個 Bean,而另一個 Bean 又依賴這個 Bean 時,由于代理對象還未創(chuàng)建,二級緩存中存儲的只是原始的 Bean 實例,不是代理對象。如果將原始 Bean 實例注入到依賴它的 Bean 中,就會導致依賴方持有的是原始 Bean 而不是代理 Bean,這不符合 Spring 的設(shè)計要求。
保證代理對象的一致性:對于同一個 Bean,無論從何處獲取,都應該保證是同一個代理對象。如果沒有三級緩存,在循環(huán)依賴場景下可能會出現(xiàn)多次創(chuàng)建代理對象的情況,從而破壞了單例 Bean 的唯一性和代理對象的一致性。
三級緩存
此時我們使用 三級緩存來代替二級緩存

一級緩存+二級緩存 不是不能解決 代理對象的問題嗎,那么這里就使用三級緩存來做,再注入 A 時,通過對象工廠生成,你是代理對象那就生成一個代理對象再注入,你是一個普通對象就生成一個普通對象 再注入。
雖然說 通過 A 的 ObjectFactory 對象創(chuàng)建了一個代理對象,但是此時還是一個半成品對象,這里就需要 二級緩存 來存放了。此時存放的是一個 A 的代理對象(半成品)。在完成的對象存放在一級緩存中后被刪除了。

到這里借助了三級緩存,解決了大部分循環(huán)依賴的問題;借助工廠類,幫助我們生成工廠對象來產(chǎn)生代理對象。
為什么是大部分的循環(huán)依賴呢,因為某些循環(huán)引用 spring 框架解決不了,需要手動來解決。例如:構(gòu)造方法注入產(chǎn)生的循環(huán)依賴。
class A {
private B b;
public A(B b) {
this.b = b;
}
}
class B {
private A a;
public B(A a) {
this.a = a;
}
}
三級緩存可以解決初始化過程中的循環(huán)依賴,卻無法解決構(gòu)造函數(shù)產(chǎn)生的循環(huán)依賴,那么該怎么辦解決呢?
我們只需要在 參數(shù)之前加上一個 @Lazy (延遲加載)
public A(@Lazy B b) {
System.out.println("A的構(gòu)造方法");
this.b = b;
}延遲加載的意思就是什么時候需要對象再進行對象的創(chuàng)建,而不再是直接把對象注入進來。
到此就解決循環(huán)依賴了。
三級緩存 + 一級緩存 存在的問題
再回顧三級緩存中,既然三級緩存代替了二級緩存,那么能否使用通過 一級緩存 + 三級緩存來解決循環(huán)依賴呢?
沒有二級緩存的后果:
如果沒有二級緩存,當出現(xiàn)循環(huán)依賴時,雖然三級緩存可以提供早期引用,但無法區(qū)分同一個 Bean 的不同狀態(tài)。例如,無法區(qū)分是正在創(chuàng)建的 Bean 還是已經(jīng)創(chuàng)建好但還未注入屬性的 Bean,可能會導致錯誤的 Bean 引用被使用,進而引發(fā)循環(huán)依賴問題無法正確解決。

每次需要使用工廠生成好的對象直接去二級緩存拿出來就可以了,不用再次去生成這個對象。這也是二級緩存的核心作用之一。
如果沒有二級緩存,那么就有可能產(chǎn)生多例的情況,此時處理起來就更麻煩了。
到此這篇關(guān)于Spring 中的循環(huán)引用問題的文章就介紹到這了,更多相關(guān)Spring 循環(huán)引用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用攔截器獲取請求的入?yún)⒉⑵滢D(zhuǎn)化為Java對象詳解
這篇文章主要介紹了如何使用攔截器獲取請求的入?yún)⒉⑵滢D(zhuǎn)化為Java對象的相關(guān)資料,文中介紹了兩種實現(xiàn)的方法,并給出了詳細的代碼示例,需要的朋友可以參考下2025-02-02
spring aop之鏈式調(diào)用的實現(xiàn)
這篇文章主要介紹了spring aop之鏈式調(diào)用的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02

