什么是 spring 的循環依賴?
首先,認識一下什么是循環依賴,舉個例子:A 對象被 Spring 管理,并且引入的 B 對象,同樣的 B 對象也被 Spring 管理,并且也引入的 A 對象。這種相互被引用的情況,就是所謂的循環依賴
@Component
public class A {@Autowiredprivate B b;
}
@Component
public class B {@Autowiredprivate A a;
}
我們都知道,Spring 是將對象給管理起來,這些對象默認還都是單例的,需要的話從 Spring 中直接取出即可。
1. Spring 是如何存儲這些 Bean 呢?
Spring 通過 Map 結構將對象給緩存起來,這里的 Map 其實是分為三個:
-
一級緩存(singletonObjects):此緩存中的對象是已經完全創建好的,可以直接使用的 Bean;
Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
-
二級緩存(earlySingletonObjects):此緩存中存儲尚未完全初始化但已經創建了對象實例的 Bean(即提前暴露的 Bean 實例)。
Map<String, Object> earlySingletonObjects = new HashMap<>();
-
三級緩存(singletonFactories):比較特殊,存放的是 ObjectFactory,這是一個工廠,等到從緩存中取時,會執行其中的
getObject()
方法,來得到代理對象。Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>()
這樣設計是為了在有 AOP 的情況下,可以返回代理對象,而且也能滿足循環依賴。
2. 循環依賴的創建過程
-
當對象 a 被創建時,就會在緩存中進行查詢,先從一級緩存中進行查詢,如果沒有,接著再從二級緩存中進行查詢,如果依舊沒用,最后從第三級緩存中查詢,如果還是沒有,就接著執行后續操作;
-
由于緩存中沒有 a 對象,就要回到主流程,執行 a 對象的創建:
- 利用反射創建對象
- 這時候就要用到三級緩存,將創建出的對象包裝成 ObjectFctory 類型,放到三級緩存中
-
填充 a 對象的屬性(Bean 的引入也在此執行)
其中要引入 b 對象,同樣也要執行與 a 對象創建相同的流程:
-
查詢緩存
-
b 對象的創建:利用反射創建 b 對象,并存入三級緩存中
-
填充 b 對象的屬性
此時又要引入 a 對象,由于已經將 a 存儲到緩存中,因此這里要執行一些額外的操作(后面說)后,將得到的 a 對象填充到 b 對象中
-
初始化
-
緩存轉移(具體步驟后面說)
-
-
a 對象初始化
-
緩存轉移
總體流程大致如下:

3. 補充說明
3.1 在 b 對象創建中填充屬性時,從緩存中讀取 a 對象要經過什么操作?
參考源碼,讀取關鍵部分(紅框位置):

首先從三級緩存中獲取到 a 對象,由于這個緩存里存放的是 ObjectFactory 類型,并不是真正的對象,這里就要執行 getObject()
方法,從而創建出真正要使用的對象,將得到的真正的對象 a 存入二級緩存中,并將三級緩存中的 a 刪除。
3.2 緩存轉移的步驟是什么?
此時 b 已經完全創建完畢,所以要將緩存里面的對象進行轉移,參考源碼:

可以分析出具體操作為:
- 將 b 的完整對象放到一級緩存中
- 將三級緩存中的 b 移除掉
- 將二級緩存中的 b 移除掉(該場景下二級緩存中并沒有 b 對象)
a 對象的緩存轉移也是同理。
4. 擴展
4.1 第三級緩存的作用?
從上面的執行步驟,可以感覺到三級緩存和里面的 ObjectFactory 類型似乎有點多余,有一級、二級緩存也能搞定循環依賴,三級緩存的意義是什么?
其實主要作用就是為了 AOP。
舉例:假如對 b 對象使用了 AOP 切面功能,那么 a 對象引入的 b 對象就必須是 b 對象的代理對象,當 Spring 在沒有循環依賴的情況下,是先將普通的完整對象創建好之后,再生成對應的代理對象,然而 Spring 并沒有辦法提前知道這個對象有沒有循環依賴,也不能直接將每個對象都創建出代理對象,所以就需要吧對象包裝成 ObjectFactory 類型,提前曝光,等從三級緩存中獲取到 ObjectFactory 后,就可以通過 getObject()
方法生成代理對象。
4.2 避免循環依賴的最佳實踐
盡管 Spring 提供了循環依賴的解決方案,但在實際開發中應盡量避免循環依賴,因為它可能導致代碼耦合度過高、可維護性差等問題。以下是一些避免循環依賴的建議:
- 重構代碼:將公共邏輯提取到第三個類中,打破循環依賴。
- 使用接口或事件驅動:通過接口或事件機制解耦組件間的直接依賴。
- 謹慎使用構造器注入:對于可能存在循環依賴的場景,優先使用 Setter 注入。
4.3 構造器注入與 Setter 注入的區別
- Setter 注入:支持循環依賴。因為 Spring 可以通過三級緩存機制提前暴露部分初始化的 Bean 實例。
- 構造器注入:不支持循環依賴。原因在于構造器注入要求在創建 Bean 時必須提供所有依賴項,而循環依賴會導致死鎖(A 等待 B,B 等待 A)。
因此,如果使用構造器注入,循環依賴會導致 BeanCurrentlyInCreationException
異常。
BeanCurrentlyInCreationException
異常:表示在嘗試實例化一個 Bean 時,Spring 容器檢測到正在創建的 Bean 已經在創建過程中,導致循環依賴。這種異常通常是由于循環依賴問題引起的。
5. 總結
Spring 通過三級緩存機制解決了單例 Bean 之間的循環依賴問題,但對于原型 Bean 或構造器注入的場景,Spring 無法解決循環依賴。開發時應盡量避免循環依賴,保持代碼的清晰性和可維護性