一文详解Spring如何解决单例Bean的循环依赖
一、什么是循环依赖
循环依赖:两个或多个单例 Bean 互相持有对方作为依赖,创建时形成死锁,无法完成实例化。
两种典型场景
1. 双向依赖(最常见)
A 里面依赖 B,B 里面依赖 A
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}2. 多向循环
A→B,B→C,C→A
补充:三种注入方式对循环依赖的支持
- setter / 字段注入(@Autowired 成员变量):Spring 可以解决单例循环依赖
- 构造器注入:无法解决,直接抛
BeanCurrentlyInCreationException - prototype 多例 Bean:完全不支持循环依赖,每次新建对象,缓存无效
二、Spring 解决单例循环依赖核心原理:三级缓存
Spring 通过 DefaultSingletonBeanRegistry 维护三个 Map 缓存,提前暴露半成品对象解决循环。
三级缓存定义
// 一级缓存:存放完全初始化好的成品单例 Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); // 二级缓存:存放实例化完成,但还未填充属性、未初始化的半成品 Bean private final Map<String, Object> earlySingletonObjects = new HashMap<>(); // 三级缓存:存放 Bean 的工厂 ObjectFactory,用于生成半成品 Bean(支持AOP代理) private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
各级缓存作用
- 一级缓存 singletonObjects 完整创建、属性填充、初始化完毕的 Bean,对外正常使用。
- 三级缓存 singletonFactories(关键) Bean 刚实例化(new 对象)后,立刻把生成当前 Bean 的工厂存入三级缓存。 作用:如果该 Bean 需要 AOP 动态代理,ObjectFactory 会返回代理对象;无 AOP 则返回原生半成品。
- 二级缓存 earlySingletonObjects 当出现循环依赖,其他 Bean 需要引用半成品时: 从三级缓存取出 ObjectFactory,生成半成品 / 代理对象,放入二级缓存,并删除三级缓存对应记录。 避免每次都执行代理逻辑,提升性能。
缓存查找顺序
获取单例 Bean 时顺序: 一级缓存 → 二级缓存 → 三级缓存
- 一级有:直接返回成品
- 一级无、二级有:返回半成品
- 一二都无、三级有:工厂创建半成品存入二级,返回
三、分步演示 A、B 循环依赖完整流程(字段注入)
前提:A、B 都是单例,使用 @Autowired 字段注入
创建 A
- 先判断一级缓存无 A,标记 A 为「正在创建」
- 反射 new A 对象(实例化完成,属性为空,半成品)
- 将 A 的 ObjectFactory 放入三级缓存 singletonFactories
- 开始填充 A 的属性,发现依赖 B,去获取 B
创建 B
- 一级缓存无 B,标记 B 正在创建
- new B 半成品,将 B 的工厂存入三级缓存
- 填充 B 的属性,发现依赖 A,去获取 A
B 获取依赖 A(核心解决点)
- 一级缓存无 A,二级缓存无 A
- 三级缓存存在 A 的 ObjectFactory,取出工厂生成 A 半成品
- 将 A 半成品放入二级缓存 earlySingletonObjects,删除三级缓存的 A
- 将半成品 A 注入给 B 的属性
B 填充完成,执行初始化(afterPropertiesSet、init-method)
- 完全初始化后的成品 B 放入一级缓存 singletonObjects
- 删除二、三级缓存中 B 的记录
回到 A 的属性填充
- 现在可以拿到完整的成品 B,注入 A 的属性
- A 完成初始化,成品 A 放入一级缓存,删除二三级缓存 A
整个流程走完,A、B 全部创建完成,循环依赖解除。
四、关键补充高频面试考点
1. 为什么构造器注入无法解决循环依赖?
三级缓存存入工厂的时机是实例化(new 对象)之后。 构造器注入需要在 new 对象阶段就传入依赖,此时对象还没创建,无法提前存入三级缓存暴露半成品,直接抛出异常。
2. 为什么需要三级缓存,只用二级行不行?
不行,三级缓存是为了AOP 动态代理:
- 如果 Bean 需要代理,ObjectFactory 内部会执行
getEarlyBeanReference()创建代理对象; - 若只有二级缓存,提前暴露的只能是原生对象,后续无法替换成代理对象,会导致注入原生对象,事务、切面失效。
3. prototype 多例 Bean 为什么不能解决循环依赖?
多例 Bean 不会存入任何一级缓存,每次获取都新建对象,没有半成品缓存复用机制,循环依赖直接无限创建对象报错。
4. 开发如何避免循环依赖?
- 业务分层解耦,调整依赖关系,不要互相依赖;
- 使用构造器注入,循环依赖会直接报错,提前发现问题;
- 使用
@Lazy懒加载依赖,注入代理对象,延迟创建目标 Bean。
五、总结
- 循环依赖:多个单例 Bean 互相依赖,创建闭环;
- Spring 仅支持 单例 + setter / 字段注入 的循环依赖;构造器、多例不支持;
- 核心方案:三级缓存提前暴露半成品 Bean;
- 一级:完整成品 Bean
- 二级:循环依赖临时半成品
- 三级:Bean 工厂,支持 AOP 代理生成;
- 流程核心:实例化后先放入三级缓存,填充属性时若出现循环,取出半成品注入对方,完成创建。
以上就是一文详解Spring如何解决单例Bean的循环依赖的详细内容,更多关于Spring解决单例Bean循环依赖的资料请关注脚本之家其它相关文章!
相关文章
spring boot整合mongo查询converter异常排查记录
这篇文章主要为大家介绍了spring boot整合mongo查询时抛出converter异常的排查解决记录,有需要的朋友可以借鉴参考下,希望能够有所帮助2022-03-03
浅谈一下RabbitMQ、Kafka和RocketMQ消息中间件对比
这篇文章主要介绍了浅谈一下RabbitMQ、Kafka和RocketMQ消息中间件对比,消息中间件属于分布式系统中一个字系统,关注于数据的发送和接收,利用高效可靠的异步信息传递机制对分布式系统中的其余各个子系统进行集成,需要的朋友可以参考下2023-05-05


最新评论