1. 什么是循環依賴?
在Spring框架中,循環依賴是指兩個或多個bean之間相互依賴,形成了一個循環引用的情況。如果不加以處理,這種情況會導致應用程序啟動失敗。導致 Spring 容器無法完成依賴注入。
例如:
@Service public class A {@Autowiredprivate B b; }@Service public class B {@Autowiredprivate A a; }
此時,A
?依賴?B
,B
?又依賴?A
,Spring 無法確定先初始化哪個 Bean。
要解決循環依賴問題的限制
Spring解決循環依賴是有一定限制的:
●首先就是要求互相依賴的Bean必須要是單例的Bean;
????????為什么呢?Spring循環依賴的解決方案主要是通過對象的提前暴露來實現的。當一個對象在創建過程中需要引用到另一個正在創建的對象時,Spring會先提前暴露一個尚未完全初始化的對象實例,以解決循環依賴的問題。這個尚未完全初始化的對象實例就是半成品對象。
在 Spring 容器中,單例對象的創建和初始化只會發生一次,并且在容器啟動時就完成了。這意味著,在容器運行期間,單例對象的依賴關系不會發生變化。因此,可以通過提前暴露半成品對象的方式來解決循環依賴的問題。
相比之下,原型對象的創建和初始化可以發生多次,并且可能在容器運行期間動態地發生變化。因此,對于原型對象,提前暴露半成品對象并不能解決循環依賴的問題,因為在后續的創建過程中,可能會涉及到不同的原型對象實例,無法像單例對象那樣緩存并復用半成品對象。
●另外就是依賴注入的方式不能都是構造函數注入的方式
為什么呢?Spring無法解決構造函數的循環依賴,是因為在對象實例化過程中,構造函數是最先被調用的,而此時對象還未完成實例化,無法注入一個尚未完全創建的對象,因此Spring容器無法在構造函數注入中實現循環依賴的解決,像下面這樣
@Component
public class ClassA {
????????private final ClassB classB;
????????@Autowired
????????public ClassA(@Lazy ClassB classB) {
????????????????this.classB = classB;
????????}
????????// ...
}
@Component
public class ClassB {
????????private final ClassA classA;
????????@Autowired
????????public ClassB(ClassA classA) {
????????????????this.classA = classA;
?????????}
????????// ...
}
但是這樣可以通過一些方法解決的
1、重新設計,徹底消除循環依賴
循環依賴,一般都是設計不合理導致的,可以從根本上做一些重構,來徹底解決,
2、改成非構造器注入
可以改成setter注入或者字段注入。
3、使用@Lazy解決(解決構造器循環依賴的)
首先要知道 Spring利用三級緩存是無法解決構造器注入這種循環依賴的。
@Lazy 是Spring框架中的一個注解,用于延遲一個bean的初始化,直到它第一次被使用。在默認情況下,Spring容器會在啟動時創建并初始化所有的單例bean。這意味著,即使某個bean直到很晚才被使用,或者可能根本不被使用,它也會在應用啟動時被創建。@Lazy 注解就是用來改變這種行為的。
也就是說,當我們使用 @Lazy 注解時,Spring容器會在需要該bean的時候才創建它,而不是在啟動時。這意味著如果兩個bean互相依賴,可以通過延遲其中一個bean的初始化來打破依賴循環。
缺點:過度使用 @Lazy 可能會導致應用程序的行為難以預測和跟蹤,特別是在涉及多個依賴和復雜業務邏輯的情況下。
下面是一些例子
@Lazy 可以用在bean的定義上或者注入時。以下是一些使用示例:
@Component
@Lazy
public class LazyBean {
// ...
}
在這種情況下,LazyBean 只有在首次被使用時才會被創建和初始化。
@Component
public class SomeClass {
????????private final LazyBean lazyBean;
????????@Autowired
????????public SomeClass(@Lazy LazyBean lazyBean) {
????????????????this.lazyBean = lazyBean;
????????}
}
在這里,即使SomeClass在容器啟動時被創建,LazyBean也只會在SomeClass實際使用LazyBean時才被初始化。
2. Spring 如何檢測循環依賴?依賴三級緩存機制
Spring 在?創建 Bean 的流程?中會檢查循環依賴,主要通過?三級緩存(3-level cache)?機制:
-
一級緩存(Singleton Objects)
存放?完全初始化好的 Bean(成品對象)。 -
二級緩存(Early Singleton Objects)
而當一個對象只進行了實例化,但是還沒有進行初始化時,我們稱之為半成品對象。所以,所謂半成品對象,其實只是 bean 對象的一個空殼子,還沒有進行屬性注入和初始化。
存放?半成品 Bean(已實例化但未完成屬性注入)。 -
三級緩存(Singleton Factories)
存放?Bean 的工廠對象(用于生成代理對象,如 AOP 場景)。
如果 Spring 發現某個 Bean 正在創建中(存在于二級緩存),但又再次被依賴,則判定為循環依賴。
3. Spring 如何解決循環依賴?spring提供了一種三級緩存的機制
前面說過Spring 的三級緩存僅能解決?單例(Singleton)作用域?且?通過屬性注入(@Autowired)?的循環依賴,核心步驟如下:
首先,Spring中Bean的創建過程其實可以分成兩步,第一步叫做實例化,第二步叫做初始化。
具體流程如下:
(1) 創建 Bean A 的流程
-
實例化 A(調用構造函數,生成原始對象)。
-
將 A 的工廠對象放入三級緩存(用于后續可能的 AOP 代理)。
-
注入 A 的依賴(發現需要 B)。
-
去容器中獲取 B(觸發 B 的創建)。
(2) 創建 Bean B 的流程
-
實例化 B(生成原始對象)。
-
將 B 的工廠對象放入三級緩存。
-
注入 B 的依賴(發現需要 A)。
-
從三級緩存獲取 A 的工廠,生成 A 的早期引用(可能是代理對象,半成品)并放入二級緩存。
-
B 完成屬性注入,變成一個完整 Bean,放入一級緩存。
(3) 回到 A 的創建流程
-
從二級緩存拿到 B 的早期引用(半成品),注入到 A。
-
A 完成初始化,從二級緩存移除,放入一級緩存。
最終,A 和 B 都成功創建,且互相持有對方的代理或真實對象。
以下是DefaultSingletonBeanRegistry#getSingleton方法,代碼中,包括一級緩存、二級緩存、三級緩存的處理邏輯,該方法是獲取bean的單例實例對象的核心方法:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
????????// 首先從一級緩存中獲取bean實例對象,如果已經存在,則直接返回
????????Object singletonObject = this.singletonObjects.get(beanName);
????????????????if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
????????????????// 如果一級緩存中不存在bean實例對象,而且當前bean正在創建中,則從二級緩????????????????存中獲取bean實例對象
????????????????????????singletonObject = this.earlySingletonObjects.get(beanName);
????????????????????????if (singletonObject == null && allowEarlyReference) {
????????????????????????// 如果二級緩存中也不存在bean實例對象,并且允許提前引用,則需要在鎖定一級緩存之前,
????????????????????????// 先鎖定二級緩存,然后再進行一系列處理
synchronized (this.singletonObjects) {
????????????????????????// 進行一系列安全檢查后,再次從一級緩存和二級緩存中獲取bean實例對象
singletonObject = this.singletonObjects.get(beanName);
????????????????????????????????if (singletonObject == null) {
????????????????????????????????????????singletonObject = this.earlySingletonObjects.get(beanName);
????????????????????????????????????????????????if (singletonObject == null) {
????????????????????????????????????????????????????????// 如果二級緩存中也不存在bean實例對象,則從三級緩存中獲取bean的ObjectFactory,并創建bean實例對象
????????????????????????????????????????????????????????ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
????????????????????????????????????????????????if (singletonFactory != null) {
????????????????????????????????????????????????????????singletonObject = singletonFactory.getObject();
????????????????????????????????????????????????????????// 將創建好的bean實例對象存儲到二級緩存中
????????????????????????????????????????????????????????this.earlySingletonObjects.put(beanName, singletonObject);
????????????????????????????????????????????????????????// 從三級緩存中移除bean的ObjectFactory
????????????????????????????????????????????????????????this.singletonFactories.remove(beanName);
??????????????????????????????????????????????????}
??????????????????????????????????????????}
????????????????????????????????}
????????????????????????}
????????????????}
????????}
return singletonObject;
}
Spring解決循環依賴一定需要三級緩存嗎?()
上面的流程可以看見只用到了Spring二級緩存就能解決依賴注入的問題。
其實,在大多數簡單的循環依賴場景(沒有AOP代理)中,二級緩存(Early Singleton Objects)?已經足夠解決問題。但 Spring 仍然使用?三級緩存(Singleton Factories,存放?Bean 的工廠對象(用于生成代理對象,如 AOP 場景)),主要是為了處理?AOP 代理?等特殊情況,確保返回的 Bean 是經過完整代理增強的對象。AOP又是Spring中很重要的一個特性,代理不能忽略。
所以三級緩存(Singleton Factories
)的核心作用是?處理 AOP 代理,確保返回的 Bean 是代理對象而非原始對象。
(1)AOP 代理的問題
如果 Bean 需要被代理(如 @Transactional、@Async),Spring 不能直接返回原始對象,而是返回代理對象。
代理對象的生成時機:需要在 Bean 初始化完成后(即 postProcessAfterInitialization 階段)。
(2)第三級緩存的作用
三級緩存存儲的是 ObjectFactory,它可以在需要時生成代理對象。
流程示例:
實例化 A(原始對象)。
將 A 的 ObjectFactory 放入三級緩存(而非原始對象)。
注入 A 的依賴時,調用 ObjectFactory.getObject():
如果 A 需要代理,則返回代理對象;
如果不需要代理,則返回原始對象。
最終放入二級緩存的是 代理對象(或原始對象),而非直接暴露原始對象。
(3)如果沒有三級緩存
如果直接將原始對象放入二級緩存,后續 AOP 代理無法替換它,導致 注入的是原始對象而非代理,可能引發問題(如事務失效)。
4. 哪些情況是三級緩存無法解決循環依賴?
場景 | 原因 |
---|---|
構造器注入(Constructor Injection) | Spring 必須先完成構造器調用,無法提前暴露半成品 Bean。,可以使用@Lazy |
原型(Prototype)作用域 | Spring 不緩存原型 Bean,無法通過三級緩存機制解決。 提一點 對于原型對象,如果要解決循環依賴問題,要維護到底是哪兩個對象之間的循環依賴,解決成本變高,而且循環依賴本來就不對,所以spring不支持。 如果檢測到原型 Bean 的循環依賴,Spring 會直接報錯: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'A': Requested bean is currently in creation: Is there an unresolvable circular reference? |
@Async/@Transactional 等代理類 | 如果循環依賴涉及 AOP 代理,可能因代理生成時機( 1:代理對象生成時機沖突,注入的可能是原始對象而非代理 2:構造器注入 + AOP 代理?? ?無法提前暴露半成品 Bean 3:原型 Bean + AOP 代理?? ?原型 Bean 無法緩存半成品 )問題導致失敗(需用? 一些極為特殊的情況。最好使用@Lazy,避免在復雜代理場景(如? |
5. 如何避免或修復循環依賴?
(1) 代碼設計層面
-
避免雙向依賴:重構代碼,使用?單向依賴?或?接口隔離。
-
提取公共邏輯:將共用邏輯抽到第三個 Bean 中。
(2) Spring 提供的解決方案
-
使用?
@Lazy
?延遲加載
在其中一個依賴上添加?@Lazy
,讓 Spring 暫時不注入真實對象,而是注入一個代理。@Service public class A {@Autowired@Lazy // 延遲加載 Bprivate B b; }
-
改用 Setter/Field 注入
替換構造器注入為屬性注入:@Service public class A {private B b;@Autowired // Setter 注入public void setB(B b) { this.b = b; } }
-
使用?
ApplicationContext
?手動獲取 Bean
在需要時再獲取依賴(不推薦,破壞 IoC 設計):@Service public class A {@Autowiredprivate ApplicationContext context;public void doSomething() {B b = context.getBean(B.class); // 使用時再獲取} }
6. 總結
要點 | 說明 |
---|---|
可解決的循環依賴 | 單例 Bean + 屬性注入(@Autowired) |
Spring不可自動解決的循環依賴 | 構造器注入、原型 Bean、某些 AOP 代理場景 |
解決方案 | @Lazy 、Setter 注入、代碼重構 |
Spring 底層機制 | 三級緩存(Singleton Objects、Early Singleton Objects、Singleton Factories) |
最佳實踐:
-
優先通過?代碼設計?避免循環依賴。
-
必要時使用?
@Lazy
?或調整注入方式。 -
避免在復雜項目中濫用循環依賴,降低維護成本。