Spring 循环依赖与三级缓存详解
目标:把 “Spring 为啥能解决一部分循环依赖” 讲清楚;把 三级缓存(singletonObjects / earlySingletonObjects / singletonFactories) 的作用讲透;以及 什么时候一定解决不了。
1. 什么是循环依赖
循环依赖(Circular Dependency)本质是:Bean A 的创建过程依赖 Bean B,而 Bean B 的创建过程又依赖 Bean A(直接或间接)。
常见形式:
- 直接循环:A → B → A
- 间接循环:A → B → C → A
依赖发生的位置也很关键:
- 构造器注入(constructor injection)
- Setter/字段注入(property/field injection)
Spring 能解决的,主要是 单例 + Setter/字段注入 这类。
2. Spring 解决循环依赖的核心思路:先“露个面”,后“补齐”
Spring 创建 Bean 的过程可以粗略拆成两段:
- 实例化(Instantiation):
new出对象(此时属性还没注入) - 属性填充(Populate Properties):注入依赖(A 需要 B)
- 初始化(Initialize):执行 Aware、BPP、init-method、@PostConstruct 等
- 注册成单例(Add to Singleton Cache)
循环依赖卡住的点一般在第 2 步:
A 还在创建中,A 注入 B → 创建 B → B 又要注入 A(但 A 还没创建完)。
解决办法:
当 A “刚实例化完但还没初始化完” 时,Spring 先把 A 的一个“早期引用”暴露出去,让别人(比如 B)先拿到 A 的引用把依赖链打通,后面再把 A 的初始化补齐。
这就是三级缓存要做的事。
3. 三级缓存分别是什么(DefaultSingletonBeanRegistry)
Spring 单例相关的缓存(简化理解):
3.1 一级缓存:singletonObjects(成品池)
Map<String, Object> singletonObjects- 存的是 完整创建完成 的单例 bean(成品)。
- 你平时从容器 getBean 拿到的,大多来自这里。
3.2 二级缓存:earlySingletonObjects(半成品引用池)
Map<String, Object> earlySingletonObjects- 存的是 早期暴露的 bean 引用(半成品引用)。
- 只有在发生循环依赖时,才会被用到。
3.3 三级缓存:singletonFactories(工厂池)
Map<String, ObjectFactory<?>> singletonFactories- 存的是一个 ObjectFactory,可以在“确实需要”时,创建 bean 的 早期引用。
- 关键点:它不是直接存 bean,而是存 “如何生成早期引用 的方法”。
直觉类比:
- 一级:成品仓库
- 二级:临时借出去的半成品
- 三级:半成品的“出库申请单/生成器”——只有别人真来借,才生成并借出
4. 为什么一定要三级?二级不够吗?
很多人卡在这里:如果只要“早期引用”,二级缓存不就够了?
三级缓存的存在,核心是为了支持 AOP 代理等“需要在早期就决定返回什么对象” 的场景:
- 早期引用有可能不是原始对象,而是 代理对象
- 是否需要代理,要看 BeanPostProcessor(例如 AOP 的 AutoProxyCreator)逻辑
- Spring 不希望“所有 bean 都提前生成早期引用”,只有在循环依赖真正发生时才生成
因此使用三级缓存:
- A 刚实例化时,先往三级缓存塞一个 factory
- 只有当 B 真的来要 A 的引用(且 A 正在创建中)时,才调用 factory
- factory 内部会走
getEarlyBeanReference,给你一个“正确的早期引用”(可能是代理)
这能避免:
- 过早创建代理导致不必要的开销
- 代理创建时机不对,出现 双重代理 / 代理丢失 / 代理对象不一致 等坑
5. 经典流程拆解:A → B → A(Setter 注入,单例)
假设:
@Component
class A { @Autowired B b; }
@Component
class B { @Autowired A a; }
5.1 关键流程(简化版)
- 创建 A
- 实例化 A:
new A() - 把 A 的 ObjectFactory 放入三级缓存(此时 A 还没注入 B)
- 实例化 A:
- A 需要注入 B → 创建 B
- 实例化 B:
new B() - 把 B 的 ObjectFactory 放入三级缓存
- 实例化 B:
- B 需要注入 A → 再次获取 A
- 发现 A “正在创建中”
- 一级缓存没有 A(还没创建完)
- 二级缓存也没有(还没暴露)
- 去三级缓存拿到 A 的 factory,生成 A 的早期引用
- 把这个早期引用放入二级缓存,并从三级缓存移除
- 返回 A 的早期引用给 B 注入
- B 完成属性填充与初始化 → 放入一级缓存
- 回到 A,拿到 B,完成属性填充与初始化 → 放入一级缓存
5.2 三张缓存的状态变化(直观)
- A 刚实例化:A 在 三级(factory)
- B 请求 A:A 从 三级(factory) → 生成早期引用 → 放到 二级
- A 初始化完成:A 从 二级(早期) → 升级到 一级(成品)
6. 关键源码位置(看懂这几个就够了)
不同版本方法细节可能有变化,但核心结构基本稳定。
6.1 提供早期引用的入口:getSingleton(beanName, allowEarlyReference)
典型逻辑(伪代码):
Object singleton = singletonObjects.get(name);
if (singleton == null && isCurrentlyInCreation(name)) {
singleton = earlySingletonObjects.get(name);
if (singleton == null && allowEarlyReference) {
ObjectFactory<?> factory = singletonFactories.get(name);
if (factory != null) {
singleton = factory.getObject(); // 生成早期引用(可能是代理)
earlySingletonObjects.put(name, singleton);
singletonFactories.remove(name);
}
}
}
return singleton;
6.2 何时放入三级缓存:addSingletonFactory
在 bean 实例化之后、属性填充之前:
- 实例化后:已经有了 raw bean
- 属性注入前:正是循环依赖会发生的位置
6.3 早期引用如何可能变成代理:getEarlyBeanReference
AOP 场景下通常由 SmartInstantiationAwareBeanPostProcessor 参与,例如:
AbstractAutoProxyCreator#getEarlyBeanReference
它会判断该 bean 是否需要代理,如果需要就提前返回代理对象。
7. Spring 能解决哪些循环依赖?不能解决哪些?
7.1 ✅ 能解决(典型)
- 单例(singleton)
- Setter/字段注入
- 依赖发生在“属性填充阶段”
- AOP 也可以(靠 getEarlyBeanReference)
7.2 ❌ 不能解决(高频面试点)
- 构造器注入循环依赖
- A 的构造器需要 B,B 的构造器需要 A
- 还没实例化就需要对方,根本没有“先露个面”的机会
- 三级缓存也救不了
- 原型(prototype)Bean 的循环依赖
- prototype 不进单例缓存体系(每次都是 new)
- 没有统一的缓存“早期暴露”机制
- 过早暴露导致的“代理/最终对象不一致”问题(复杂 AOP 链)
- 虽然 Spring 做了很多兼容,但某些极端组合仍可能翻车
- 常见表现:拿到的是 raw bean 而不是代理,导致事务/切面失效
8. Spring Boot 的默认行为(非常实用)
从 Spring Boot 2.6 开始,默认禁止 Bean 循环依赖(更鼓励你改代码结构),需要时可以手动打开。
application.yml:
spring:
main:
allow-circular-references: true
也可以通过 SpringApplication#setAllowCircularReferences(true) 设置。
现实建议:能不靠循环依赖就别靠。允许只是 “救急开关”,不是“架构特性”。
9. 如何“从根上”消除循环依赖(更像高级工程师的答案)
- 引入中间层/抽象
- A 依赖接口 X,B 实现 X 或由第三方协调者实现
- 事件驱动 / 发布订阅
- A 发布事件,B 监听事件,避免直接互相持有引用
- @Lazy 延迟注入
- 注入一个延迟代理,打断初始化期的强依赖
@Autowired @Lazy private A a;
- 把“互相调用”改成“单向调用 + 回调/策略”
- 循环依赖本质经常是职责混乱,拆职责通常就顺便拆掉依赖环
10. 面试速记版
- Spring 解决的是 单例 + setter/字段注入 的循环依赖
- 核心是 提前暴露早期引用 来打断依赖环
- 三级缓存:
- 一级:成品单例
- 二级:早期引用(半成品)
- 三级:ObjectFactory(按需生成早期引用,支持 AOP 提前代理)
- 构造器循环依赖、prototype 循环依赖基本无解
- Spring Boot 2.6+ 默认禁止循环依赖,可通过
spring.main.allow-circular-references=true临时开启
755

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



