Spring 循環依賴:從原理到解決方案的全面解析
一、循環依賴的定義與分類
1. 什么是循環依賴?
在 Spring 框架中,循環依賴指的是多個 Bean 之間形成了依賴閉環。例如:
- Bean A 依賴 Bean B
- Bean B 依賴 Bean C
- Bean C 又依賴 Bean A
此時,這三個 Bean 之間就形成了循環依賴關系。當容器嘗試初始化這些 Bean 時,會陷入無法完成初始化的死循環。
2. 循環依賴的三種類型
根據依賴注入方式的不同,循環依賴可分為三類:
類型 | 描述 |
---|---|
構造器循環依賴 | 多個 Bean 通過構造函數互相依賴,如A構造器注入B ,B構造器注入A 。 |
setter 循環依賴 | 多個 Bean 通過 setter 方法互相依賴,如A.setB(b) ,B.setA(a) 。 |
字段注入循環依賴 | 多個 Bean 通過字段直接注入互相依賴,如@Autowired private B b; ,@Autowired private A a; 。 |
二、Spring 如何處理循環依賴?
1. Spring 處理循環依賴的核心機制
Spring 通過三級緩存機制解決 setter 和字段注入的循環依賴,而構造器循環依賴則無法自動解決。
三級緩存的定義(DefaultSingletonBeanRegistry
類):
- 一級緩存(singletonObjects):存儲完全初始化的 Bean 實例。
- 二級緩存(earlySingletonObjects):存儲已創建但未完全初始化的 Bean 實例(早期暴露的對象)。
- 三級緩存(singletonFactories):存儲 Bean 的工廠對象,用于生成代理對象等后置處理。
2. 解決 setter 循環依賴的流程示例
以A依賴B,B依賴A
的 setter 循環依賴為例:
- 初始化 A:創建 A 的實例,放入二級緩存,并標記為 “未完全初始化”。
- 注入 B 到 A:發現 A 依賴 B,開始初始化 B。
- 初始化 B:創建 B 的實例,放入二級緩存,然后嘗試注入 A 到 B。
- 注入 A 到 B:此時 A 已在二級緩存中,B 獲取 A 的早期實例并完成注入,B 初始化完成后放入一級緩存。
- 完成 A 的初始化:A 獲取到已初始化的 B,完成注入后放入一級緩存。
3. 三級緩存的核心作用
- 三級緩存的存在是為了處理 AOP 代理:當 Bean 需要代理時,三級緩存存儲的工廠對象會在早期暴露階段生成代理實例,避免循環依賴中出現 “原始對象” 和 “代理對象” 的不一致問題。
三、構造器循環依賴為何無法解決?
1. 構造器循環依賴的初始化流程
假設A構造器注入B
,B構造器注入A
:
- 初始化 A 時,需要先創建 B 的實例。
- 初始化 B 時,又需要先創建 A 的實例。
- 由于構造器依賴必須在對象創建時完成,兩者互相等待,導致初始化阻塞。
2. 示例代碼與異常
@Component
public class A {private final B b;// 構造器注入B,導致循環依賴public A(B b) {this.b = b;}
}@Component
public class B {private final A a;public B(A a) {this.a = a;}
}
啟動 Spring 容器時會拋出org.springframework.beans.factory.UnsatisfiedDependencyException
,提示無法解析循環依賴。
四、循環依賴的解決方案
1. 針對構造器循環依賴:
方案一:使用 setter 注入替代構造器注入
@Component
public class A {private B b;// 使用setter注入,允許Spring通過三級緩存解決循環依賴public void setB(B b) {this.b = b;}
}
方案二:使用 @Lazy 延遲初始化
通過@Lazy
讓 Spring 注入代理對象,延遲依賴解析:
@Component
public class A {private final B b;// 注入B的代理對象,避免初始化時立即創建Bpublic A(@Lazy B b) {this.b = b;}
}
方案三:拆分 Bean,打破依賴鏈
將復雜 Bean 拆分為多個小 Bean,避免直接依賴。
2. 針對 setter / 字段循環依賴:
通常無需特殊處理,Spring 三級緩存可自動解決。若遇到問題,可能是以下原因:
- Bean 使用了
@PostConstruct
等初始化方法,且方法中存在循環邏輯。 - 自定義 BeanPostProcessor 干擾了三級緩存的正常工作。
3. 通用最佳實踐:
- 優先使用構造器注入:明確依賴關系,但需避免構造器循環依賴。
- 謹慎使用 @Autowired 字段注入:可能隱藏依賴關系,推薦搭配 setter 注入。
- 使用 @DependsOn:強制指定 Bean 初始化順序,打破隱性循環依賴。
- 模塊化設計:通過拆分服務或引入中間層,避免跨模塊的直接依賴。
五、Spring Boot 中循環依賴的排查與工具
1. 日志排查
啟動時添加 JVM 參數-Dspring.main.allow-circular-references=true
(Spring Boot 2.6+),允許循環依賴并打印警告日志。
2. IDE 工具輔助
- IntelliJ IDEA:通過
Analyze Dependencies
功能檢測循環依賴。 - Spring Tool Suite:使用依賴分析視圖定位問題 Bean。
3. 編程式排查
通過ConfigurableApplicationContext.getBeanFactory()
獲取DefaultListableBeanFactory
,調用isPrototypeCurrentlyInCreation()
等方法診斷循環依賴。
六、深度解析:三級緩存的源碼視角
1. 關鍵源碼路徑(AbstractBeanFactory.doGetBean
):
// 從一級緩存獲取Bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && !isSingletonCurrentlyInCreation(beanName)) {return getObjectForBeanInstance(sharedInstance, name, beanName, null);
}// 標記Bean為“正在創建”
beforeSingletonCreation(beanName);
try {// 從二級緩存獲取早期實例sharedInstance = getSingleton(beanName, false);if (sharedInstance != null) {// 處理早期實例return getObjectForBeanInstance(sharedInstance, name, beanName, null);}// 創建Bean實例(未初始化)BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);Object bean = instanceWrapper.getWrappedInstance();// 將早期實例放入三級緩存addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));// 填充依賴(可能觸發循環依賴)populateBean(beanName, mbd, instanceWrapper);// 初始化Bean(調用后置處理器等)exposedObject = initializeBean(beanName, exposedObject, mbd);// 將完全初始化的Bean放入一級緩存addSingleton(beanName, exposedObject);
} finally {afterSingletonCreation(beanName);
}
2. 核心方法解析:
- getSingleton(beanName, true):嘗試從一級緩存獲取 Bean,若不存在則創建。
- addSingletonFactory:將 Bean 的工廠對象存入三級緩存,用于生成早期實例。
- getEarlyBeanReference:處理 AOP 代理等后置操作,返回早期實例。
七、總結:循環依賴的本質與設計哲學
Spring 通過三級緩存解決循環依賴的核心,是利用 “早期暴露” 機制打破初始化死鎖:將未完全初始化的 Bean 提前暴露到二級緩存,允許其他 Bean 先獲取其引用,后續再完成初始化。
但構造器循環依賴無法解決,這體現了 Spring 的設計原則:構造器依賴應代表 “強依賴”,而強依賴不應形成循環。在實際開發中,合理的依賴設計(如模塊化、單向依賴)比依賴 Spring 的循環依賴處理機制更重要。
理解循環依賴的原理與解決方案,不僅能幫助開發者快速定位問題,還能加深對 Spring Bean 生命周期和依賴注入機制的理解,從而寫出更健壯的代碼。