深入理解Spring中的循环依赖及解决方案
在Spring框架的日常使用中,"循环依赖"是一个高频出现且容易让人困惑的问题。新手往往在遇到BeanCurrentlyInCreationException
时手足无措,而即使是有经验的开发者,也可能对Spring解决循环依赖的底层逻辑一知半解。本文将从概念入手,深入剖析循环依赖的产生原因、Spring的处理机制,以及实战中的解决方案。
一、什么是循环依赖?
循环依赖,顾名思义,是指两个或多个Bean之间互相依赖,形成一个闭环的依赖关系。
1.1 直观案例
最典型的循环依赖是"双向依赖",例如:
此时在创建A
对象的同时需要使用的B
对象,在创建B
对象的同时需要使用到A
对象,A
和B
形成了A→B→A
的闭环,导致无限等待,这就是最基础的循环依赖。
1.2 复杂场景
循环依赖也可能是多节点的闭环,例如A→B→C→A
,或者更复杂的网状依赖(如A依赖B和C,B依赖C,C依赖A
)。无论结构如何,核心都是"依赖关系形成了无法直接按顺序初始化的闭环"。
二、循环依赖为什么会成为问题
在讨论Spring的处理逻辑前,我们需要先理解:为什么循环依赖会导致问题?
这要从Bean的初始化流程(Bean的生命周期)说起。Spring创建Bean的核心步骤是:
- 实例化:通过构造器创建Bean的对象(
new
操作); - 属性注入:为Bean的依赖属性赋值(如
@Autowired
标注的字段); - 初始化:执行
@PostConstruct
方法、实现InitializingBean
接口的afterPropertiesSet
方法等。
正常情况下,Bean的创建是"线性"的:先创建依赖的Bean,再创建当前Bean。例如A依赖B
时,Spring会先创建B,再创建A并注入B。
但循环依赖打破了这种线性关系。以A→B→A
为例:
- 要创建A,需要先创建B;
- 要创建B,又需要先创建A;
- 陷入"先有鸡还是先有蛋"的死循环。
三、Spring如何处理循环依赖?
Spring并非对所有循环依赖都束手无策。事实上,对于单例Bean的字段注入(或Setter注入),Spring能自动解决循环依赖,这得益于它的"三级缓存"机制。
3.1 三级缓存的核心设计
Spring通过三个缓存(称为"三级缓存")来协调单例Bean的创建与依赖注入,这三个缓存定义在DefaultSingletonBeanRegistry
中:
缓存名称 | 作用 | 级别 |
---|---|---|
singletonObjects |
存储完全初始化完成的单例Bean(key:Bean名称,value:Bean实例) | 一级 |
earlySingletonObjects |
存储提前暴露的未完全初始化的单例Bean(仅实例化未注入属性) | 二级 |
singletonFactories |
存储Bean工厂(用于提前暴露未初始化的Bean,避免重复创建) | 三级 |
3.2 三级缓存解决循环依赖的流程
解决一般对象的循环依赖
以A→B→A
的双向依赖为例,我们一步步拆解Spring的处理逻辑:
创建A的流程:
- Spring尝试获取A,发现三个缓存中都没有;
- 开始创建A:先实例化A(执行构造器,得到"半成品"A,仅分配了内存,未注入属性);
- 将A的“半成品”对象放入二级缓存
earlySingletonObjects
; - 准备为A注入属性,发现依赖B,于是暂停A的创建,转去创建B。
创建B的流程:
- Spring尝试获取B,三个缓存中都没有;
- 实例化B(得到"半成品"B);
- 将B的“半成品”放入二级缓存
earlySingletonObjects
; - 准备为B注入属性,发现依赖A,转去获取A。
解决A的依赖:
- 尝试获取A时,发现二级缓存
earlySingletonObjects
中有A的实例对象; - 取出A的"半成品",将"半成品"A注入到B中,B的属性注入完成;
- B完成初始化,放入一级缓存
singletonObjects
,并从二级缓存中移除B。
- 尝试获取A时,发现二级缓存
完成A的创建:
- 回到A的属性注入步骤,此时B已在一级缓存中,直接将B注入A;
- A完成初始化,放入一级缓存
singletonObjects
,并从二级缓存中移除A。
最终,A和B都成为"完全体",存储在一级缓存中,循环依赖被解决。
解决代理对象的循环依赖
以A→B→A
的双向依赖为例,我们一步步拆解Spring的处理逻辑:
创建A的流程:
- Spring尝试获取A,发现三个缓存中都没有;
- 开始创建A:先实例化A(执行构造器,得到"半成品"A,仅分配了内存,未注入属性);
- 将A的工厂(
singletonFactory
)放入三级缓存singletonFactories
; - 准备为A注入属性,发现依赖B,于是暂停A的创建,转去创建B。
创建B的流程:
- Spring尝试获取B,三个缓存中都没有;
- 实例化B(得到"半成品"B);
- 将B的工厂放入三级缓存
singletonFactories
; - 准备为B注入属性,发现依赖A,转去获取A。
解决A的依赖:
- 尝试获取A时,发现三级缓存
singletonFactories
中有A的工厂; - 通过工厂取出A的"半成品",放入二级缓存
earlySingletonObjects
,并从三级缓存中移除A的工厂; - 将"半成品"A注入到B中,B的属性注入完成;
- B完成初始化,放入一级缓存
singletonObjects
,并从二级缓存中移除B。
- 尝试获取A时,发现三级缓存
完成A的创建:
- 回到A的属性注入步骤,此时B已在一级缓存中,直接将B注入A;
- A完成初始化,放入一级缓存
singletonObjects
,并从二级缓存中移除A。
最终,A和B都成为"完全体",存储在一级缓存中,循环依赖被解决。
四、Spring无法解决的循环依赖场景
并非所有循环依赖都能被Spring自动处理。以下两种场景会导致BeanCurrentlyInCreationException
:
4.1 构造器注入的循环依赖
如果循环依赖通过构造器注入,Spring无法解决。例如:
@Service
public class AService {
private BService bService;
// 构造器注入B
@Autowired
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
private AService aService;
// 构造器注入A
@Autowired
public BService(AService aService) {
this.aService = aService;
}
}
原因:构造器注入要求"先获取依赖才能实例化当前Bean"。创建A时需要先创建B,创建B时又需要先创建A,而此时两者都未实例化,无法提前暴露到缓存中,导致死循环。
4.2 多例(Prototype)Bean的循环依赖
Spring默认仅处理单例(Singleton) Bean的循环依赖。对于多例(@Scope("prototype")
)Bean,循环依赖会直接报错。
原因:多例Bean每次获取都会创建新实例,不会存入三级缓存(缓存仅用于单例)。因此,创建A时需要B,创建B时又需要新的A,无限创建新对象导致溢出。
五、循环依赖的解决方案
针对不同场景的循环依赖,我们可以采用以下解决方案:
5.1 解决构造器注入的循环依赖
方案1:使用@Lazy延迟初始化
@Lazy
注解可以让依赖的Bean延迟到第一次使用时才初始化,而非在当前Bean创建时立即初始化。例如:
@Service
public class AService {
private BService bService;
@Autowired
public AService(@Lazy BService bService) { // 对B延迟初始化
this.bService = bService;
}
}
此时,Spring会为B创建一个代理对象注入A,当A第一次使用B时才会真正创建B实例,打破了初始化时的闭环。
方案2:改用字段注入或Setter注入
将构造器注入改为字段注入(@Autowired
标注字段)或Setter注入,利用Spring对单例字段注入的自动处理机制:
@Service
public class AService {
private BService bService;
// Setter注入
@Autowired
public void setBService(BService bService) {
this.bService = bService;
}
}
5.2 解决多例Bean的循环依赖
多例Bean的循环依赖无法通过缓存解决,需从设计上规避:
方案1:将多例Bean改为单例
如果业务允许,将@Scope("prototype")
改为默认的单例,利用三级缓存自动处理。
方案2:通过工厂手动获取
在多例Bean中,不直接注入依赖,而是通过ApplicationContext
或ObjectFactory
动态获取,避免初始化时的依赖:
@Service
@Scope("prototype")
public class AService {
@Autowired
private ObjectFactory<BService> bFactory; // 工厂
public void doSomething() {
BService b = bFactory.getObject(); // 需要时才获取
// ...
}
}
5.3 通用方案:重构代码,消除循环依赖
循环依赖往往是代码设计不合理的信号(例如职责划分不清晰)。最根本的解决方案是重构:
- 提取公共依赖:将A和B共同依赖的逻辑抽离为新的Bean(如
CService
),让A和B都依赖C,而非互相依赖; - 引入中间层:通过中介者模式(Mediator)减少Bean之间的直接依赖;
- 拆分Bean职责:如果一个Bean承担过多职责,可能导致与多个Bean产生依赖,拆分后可减少依赖关系。
六、总结
Spring的循环依赖处理是其Bean管理机制的重要组成部分,核心依赖三级缓存实现单例字段注入的自动处理。但对于构造器注入和多例Bean,仍需手动干预。
在实际开发中,建议:
- 优先通过重构消除循环依赖,这是最健康的方式;
- 必须保留循环依赖时,单例Bean优先用字段注入,构造器注入可配合
@Lazy
; - 多例Bean尽量避免循环依赖,必要时通过工厂动态获取。
理解循环依赖的本质和Spring的处理逻辑,不仅能解决实际问题,更能帮助我们设计出更清晰、低耦合的代码结构。