Spring IoC容器通过三级缓存机制巧妙地解决了单例Bean之间的循环依赖问题,这一设计是Spring框架的核心机制之一。下面我将从原理、实现到应用场景全面解析这一机制。
一、循环依赖的产生与问题本质
循环依赖是指两个或多个Bean相互持有对方的引用,形成依赖闭环。例如:
less
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
这种相互依赖会导致传统初始化流程陷入死锁:创建A需要先初始化B,而创建B又需要先初始化A。
二、三级缓存的核心架构
Spring在DefaultSingletonBeanRegistry
类中定义了三级缓存结构:
-
一级缓存(singletonObjects)
- 类型:
ConcurrentHashMap<String, Object>
- 作用:存储完全初始化完成的单例Bean,是最终可用的成品对象
- 类型:
-
二级缓存(earlySingletonObjects)
- 类型:
HashMap<String, Object>
- 作用:存储已实例化但未初始化的Bean早期引用(半成品对象),用于解决普通循环依赖
- 类型:
-
三级缓存(singletonFactories)
- 类型:
HashMap<String, ObjectFactory<?>>
- 作用:存储Bean的
ObjectFactory
,用于生成代理对象或原始对象,是解决AOP代理循环依赖的关键
- 类型:
三、解决循环依赖的完整流程
以A→B→A的循环依赖为例,Spring容器处理流程如下:
-
开始创建A
-
标记A为"创建中"(加入
singletonsCurrentlyInCreation
集合) -
通过反射调用A的构造器创建实例(此时A是原始对象,未注入属性)
-
将A的
ObjectFactory
放入三级缓存:scssaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
这个工厂对象能在必要时生成A的早期引用(原始对象或代理对象)
-
-
处理A的依赖B
- 发现A需要注入B,触发B的创建流程
- 同样地:标记B为创建中,实例化B,将B的工厂放入三级缓存
-
处理B的依赖A
-
发现B需要注入A,此时:
-
从一级缓存查找A → 不存在
-
从二级缓存查找A → 不存在
-
从三级缓存获取A的
ObjectFactory
并调用getObject()
:iniObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { earlySingleton = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, earlySingleton); this.singletonFactories.remove(beanName); }
-
将生成的A早期引用存入二级缓存,并从三级缓存移除
-
-
-
完成B的初始化
- 将A的早期引用注入B
- 执行B的初始化回调(@PostConstruct等)
- 将完全初始化的B放入一级缓存
-
完成A的初始化
- 将B的成品对象注入A
- 执行A的初始化回调
- 将A的成品对象从二级缓存提升到一级缓存
四、三级缓存的关键设计意义
-
解决普通循环依赖
二级缓存存储的早期对象允许相互依赖的Bean在未完成初始化时就能被引用,打破循环僵局
-
支持AOP代理
三级缓存的
ObjectFactory
通过getEarlyBeanReference()
方法:-
对普通Bean:直接返回原始对象
-
对需要AOP代理的Bean:返回代理对象
这确保最终注入的是同一个代理实例,避免出现原始对象与代理对象混用的情况
-
-
性能优化
- 通过缓存层级减少重复创建
- 通过对象工厂延迟代理对象的生成时机
- 使用不同粒度的锁控制并发访问
五、机制的限制条件
-
仅适用于单例Bean
原型(prototype)作用域的Bean每次请求都创建新实例,无法通过缓存解决循环依赖
-
不支持构造器循环依赖
构造器注入必须在实例化前完成依赖解析,此时Bean尚未创建,无法存入缓存
-
Spring Boot 2.6+默认关闭循环引用
可通过
spring.main.allow-circular-references=true
重新启用
六、替代解决方案
当三级缓存无法解决问题时,可考虑:
-
@Lazy延迟注入
less@Component public class A { @Lazy @Autowired private B b; // 实际注入的是代理对象 }
首次调用b的方法时才触发真实初始化
-
重构代码结构
- 提取公共逻辑到第三个Bean
- 使用接口解耦
- 将部分依赖改为方法参数
-
改用Setter注入
Setter注入允许先创建实例再注入依赖,比构造器注入更灵活
七、典型应用场景
-
领域模型互引用
如用户-角色关系:用户拥有角色集合,角色又关联用户列表
-
服务层交叉调用
如订单服务需要用户服务查用户,用户服务需要订单服务查历史订单
-
AOP代理链
如事务管理器的AOP代理相互依赖
Spring的三级缓存机制通过"提前暴露引用"的设计思想,在保持单例约束的同时,优雅地解决了循环依赖这一经典难题,体现了框架设计的精妙之处。