1. 循环依赖的本质与Spring的应对策略
当两个或多个Bean相互引用时,就形成了循环依赖。比如Bean A的构造器需要Bean B作为参数,而Bean B的构造器又需要Bean A作为参数。这种"鸡生蛋蛋生鸡"的问题在传统编程中几乎无解,但Spring通过三级缓存机制巧妙地化解了这个难题。
Spring处理循环依赖的核心在于 对象创建与属性注入的分离 。与直觉不同,Spring并不要求所有依赖在构造时就完全就绪,而是允许先创建半成品对象(已实例化但未初始化的对象),后续再通过setter方法补充依赖。这种"先上车后补票"的机制是解决循环依赖的基础。
关键理解:Spring只能解决通过setter注入或字段注入形成的循环依赖,无法解决构造器注入导致的循环依赖。因为构造器注入必须在对象创建时就提供完整依赖,而setter注入允许对象先存在再补充依赖。
2. Spring三级缓存机制深度解析
2.1 三级缓存的组成与职责
Spring使用三个Map结构来管理Bean的不同状态:
- singletonObjects (一级缓存):存储完全初始化好的单例Bean,业务代码实际获取到的就是这个缓存里的对象
- earlySingletonObjects (二级缓存):存储提前暴露的原始Bean(已实例化但未初始化),用于解决循环依赖
- singletonFactories (三级缓存):存储Bean的ObjectFactory,用于在需要时创建早期引用
// 简化版的三级缓存结构示意
public class DefaultSingletonBeanRegistry {
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
2.2 解决循环依赖的具体流程
以一个典型的循环依赖场景为例(A依赖B,B依赖A):
-
开始创建A:
- 实例化A(调用构造函数)
- 将A的ObjectFactory放入三级缓存(此时A是原始对象)
- 准备注入A的属性时发现需要B
-
开始创建B:
- 实例化B
- 将B的ObjectFactory放入三级缓存
- 准备注入B的属性时发现需要A
-
解决A的依赖:
- 从三级缓存中找到A的ObjectFactory
- 通过getObject()获取早期引用(可能经过AOP代理)
- 将A的早期引用放入二级缓存,并从三级缓存移除
- 将A的早期引用注入到B中
- 完成B的初始化,将B放入一级缓存
-
回到A的创建:
- 从一级缓存获取已完成的B
- 将B注入到A中
- 完成A的初始化
- 将A放入一级缓存,并从二级缓存移除
graph TD
A[开始创建A] --> B[实例化A]
B --> C[将A工厂放入三级缓存]
C --> D[发现需要B]
D --> E[开始创建B]
E --> F[实例化B]
F --> G[将B工厂放入三级缓存]
G --> H[发现需要A]
H --> I[从三级缓存获取A早期引用]
I --> J[将A引用放入二级缓存]
J --> K[将A注入到B]
K --> L[完成B初始化]
L --> M[将B放入一级缓存]
M --> N[回到A创建流程]
N --> O[获取完整B注入A]
O --> P[完成A初始化]
P --> Q[将A放入一级缓存]
3. 循环依赖处理的边界条件与限制
3.1 Spring无法解决的循环依赖场景
- 构造器循环依赖 :当两个Bean都通过构造器注入对方时,Spring无法处理。因为构造器注入必须在实例化时提供完整依赖,无法先创建半成品对象。
// 无法解决的构造器循环依赖示例
@Component
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Component
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}
-
原型(prototype)作用域的循环依赖 :Spring不会缓存prototype bean,因此无法提供早期引用。
-
@Async方法的循环依赖 :由于@Async会创建代理,可能破坏正常的创建顺序。
3.2 实际开发中的最佳实践
-
设计时避免循环依赖 :虽然Spring能解决部分循环依赖,但从设计角度应尽量避免。考虑:
- 提取公共逻辑到第三个Bean
- 使用事件驱动架构解耦
- 应用接口隔离原则
-
必须使用循环依赖时的建议 :
- 统一使用setter注入而非构造器注入
- 确保循环链中不包含@Async方法
- 避免在初始化方法(@PostConstruct)中调用循环依赖对象
-
检测工具 :
-
使用
DefaultListableBeanFactory#isEligibleForMetadataCaching检查Bean是否适合循环依赖处理 -
启动时关注
BeanCurrentlyInCreationException警告
-
使用
4. 循环依赖与AOP代理的协同工作
当循环依赖的Bean需要被AOP代理时,情况会变得更加复杂。Spring需要确保注入的是代理对象而非原始对象,这通过
SmartInstantiationAwareBeanPostProcessor
实现。
4.1 代理对象的早期暴露流程
- 在创建原始Bean后,Spring会检查是否需要AOP代理
- 如果需要代理,将返回代理对象而非原始对象
- 代理对象的工厂被放入三级缓存
- 当其他Bean请求依赖时,会通过工厂获取代理对象
// AbstractAutowireCapableBeanFactory中的关键代码
protected Object getEarlyBeanReference(String beanName, Object bean) {
Object exposedObject = bean;
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = ...;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
4.2 常见代理场景的处理
- JDK动态代理 :基于接口的代理,早期引用需要实现相同接口
- CGLIB代理 :基于子类化的代理,需要确保方法不被final修饰
- @Transactional代理 :事务增强可能影响循环依赖解决顺序
经验之谈:在开发中如果遇到循环依赖+AOP代理的问题,可以尝试使用
@Lazy注解延迟注入,或者重构代码消除循环依赖。
5. Spring循环依赖处理的实际调试技巧
5.1 日志分析技巧
在application.properties中增加日志级别配置:
logging.level.org.springframework.beans=DEBUG
logging.level.org.springframework.context=DEBUG
关键日志信息解读:
-
Creating instance of bean...:开始创建Bean -
Eagerly caching bean to allow for resolving potential circular references:将Bean工厂放入三级缓存 -
Returning eagerly cached instance of singleton bean that is not fully initialized yet:从二级缓存获取早期引用 -
Finished creating instance of bean...:完成Bean创建
5.2 断点调试建议
关键断点位置:
-
DefaultSingletonBeanRegistry#getSingleton -
AbstractAutowireCapableBeanFactory#doCreateBean -
AbstractAutowireCapableBeanFactory#populateBean -
AbstractAutowireCapableBeanFactory#initializeBean
调试时重点关注:
- 三级缓存的内容变化
-
earlySingletonObjects和singletonObjects的差异 - Bean当前状态(实例化/属性填充/初始化)
5.3 常见问题排查清单
-
出现BeanCurrentlyInCreationException :
- 检查是否是构造器循环依赖
- 确认Bean的作用域是否为prototype
- 检查@Async方法是否导致代理创建异常
-
注入的对象不是预期的代理对象 :
- 确认AOP配置是否正确
- 检查是否有多个BeanPostProcessor干扰
- 验证@Transactional等注解的位置
-
部分属性未正确注入 :
- 检查初始化顺序
- 确认没有在@PostConstruct中访问未就绪的依赖
- 验证setter方法是否可访问(非private)
在实际项目中遇到循环依赖问题时,我通常会先通过日志分析大致定位问题环节,然后使用调试器逐步跟踪Bean的创建过程,特别注意三级缓存的变化情况。同时,长期来看,重构代码消除不必要的循环依赖才是根本解决方案。

7909


被折叠的 条评论
为什么被折叠?



