Spring 源码系列(9): 循环依赖终极拷问:为什么用三级缓存而不是二级
引子
A 依赖 B,B 又依赖 A——这么写,Spring 为什么没崩?很多人的答案止步于"因为三级缓存"。但面试官要的是:三级缓存各自存什么?为什么二级不够、非得三级?
本篇把循环依赖的"前因后果 + 源码证据 + 反证"一次性讲透。这是整个 IoC 阶段最值得收藏的一篇。
💡一句话结论
三级缓存解决的是"在还没初始化完时,如何把一个可被他人引用的早期对象(可能是 AOP 代理)安全暴露出去"。二级缓存只能存"已决定的早期对象",无法延迟决定要不要生成代理;三级缓存用ObjectFactory把"是否生成代理"延迟到真正被依赖引用时才发生。这就是二级不够、必须三级的根本原因。
前置知识
- Spring只解决"单例 + 字段/setter 注入"的循环依赖;构造器注入、原型(prototype)作用域不解决。
- "提前暴露"动作发生在
doCreateBean:实例化后addSingletonFactory把() -> getEarlyBeanReference放进三级缓存(见第 6 篇)。 - 循环依赖的产生源于 DI 阶段的
getBean(依赖)递归(见第 8 篇)。
一、三级缓存到底存什么
都在DefaultSingletonBeanRegistry里,三个 Map:
| 缓存 | 字段 | 存的内容 | 写入时机 | 读取时机 |
|---|---|---|---|---|
| 一级 | singletonObjects | 成品 Bean(完全初始化完成) | addSingleton(创建完成) | getSingleton先查它 |
| 二级 | earlySingletonObjects | 早期 Bean(已实例化、未初始化,可能是代理) | getSingleton从三级取对象后转入 | 有人提前引用时 |
| 三级 | singletonFactories | ObjectFactory(能产出早期引用) | addSingletonFactory(实例化后) | 一级、二级都未命中时 |
// DefaultSingletonBeanRegistryprivatefinalMap<String,Object>singletonObjects=newConcurrentHashMap<>(256);// 一级privatefinalMap<String,Object>earlySingletonObjects=newHashMap<>(16);// 二级privatefinalMap<String,ObjectFactory<?>>singletonFactories=newHashMap<>(16);// 三级二、getSingleton:查缓存的顺序
protectedObjectgetSingleton(StringbeanName,booleanallowEarlyReference){ObjectsingletonObject=this.singletonObjects.get(beanName);// ① 一级if(singletonObject==null&&isSingletonCurrentlyInCreation(beanName)){singletonObject=this.earlySingletonObjects.get(beanName);// ② 二级if(singletonObject==null&&allowEarlyReference){ObjectFactory<?>singletonFactory=this.singletonFactories.get(beanName);// ③ 三级if(singletonFactory!=null){singletonObject=singletonFactory.getObject();// 调 getEarlyBeanReferencethis.earlySingletonObjects.put(beanName,singletonObject);// 转存二级this.singletonFactories.remove(beanName);// 清三级}}}returnsingletonObject;}三、为什么必须三级?核心论证
假设只有一 + 二级缓存(即没有三级的ObjectFactory),会发生什么?
- 实例化后必须立刻把一个"早期对象"放进二级缓存
earlySingletonObjects。 - 但此时 Bean还没走
initializeBean,AOP 代理还没生成。如果这里直接放"原始对象",而后面initializeBean又生成了代理——那么循环依赖方(B)手里拿的是原始对象,最终容器里却是代理,二者不是同一个引用,Spring 会抛BeanCurrentlyInCreationException。 - 那"实例化后就直接生成代理放进二级"行不行?不行——绝大多数 Bean 没有 AOP,凭空给每个 Bean 都生成代理既浪费又在语义上错误;而且代理本应在
initializeBean第 ⑦ 步(postProcessAfterInitialization)才确定。
三级缓存的价值 = 延迟决策:
- 三级缓存存的是
() -> getEarlyBeanReference(beanName, mbd, bean),是一个工厂/lambda,不是具体对象。 - 只有真正发生循环依赖(B 来取 A)时,才调用这个工厂 →
getEarlyBeanReference→ 此时才决定返回"原始对象"还是"代理"。 - 若不发生循环依赖,这个工厂永远不被调用,A 正常走到
initializeBean生成代理,没有任何副作用。
// 发生循环依赖、被引用时才调用protectedObjectgetEarlyBeanReference(StringbeanName,RootBeanDefinitionmbd,Objectbean){ObjectexposedObject=bean;for(SmartInstantiationAwareBeanPostProcessorbp:getBeanPostProcessors()){exposedObject=bp.getEarlyBeanReference(exposedObject,beanName);// AOP 在此可能生成代理}returnexposedObject;}📌结论:三级缓存不是为了"性能",而是为了"延迟生成代理 + 保证引用一致性"。二级缓存是三级的"结果缓存"——
getEarlyBeanReference拿到代理后存进二级,避免同一早期引用被重复生成(代理不能 new 多次)。
四、构造器循环依赖为何无解
构造器注入在createBeanInstance(实例化)阶段就要求依赖就绪,而addSingletonFactory(注册三级缓存)是在实例化之后才执行的。所以:
A 构造器要 B → getBean(B) → B 构造器要 A → getBean(A) → A 此时既不在一级(没建完),也不在二/三级(还没到 addSingletonFactory) → 只能再走创建 A → isSingletonCurrentlyInCreation(A) 已标记 → 抛 BeanCurrentlyInCreationException字段/setter 注入能解,正是因为"实例化"先于"注入"——实例化后立刻把工厂放进三级缓存,等populateBean时 B 来取,能从三级拿到 A 的早期引用。
五、循环依赖时序图
六、常见误区
| 误区 | 正解 |
|---|---|
| 三级缓存是为了性能 | 否,是为了延迟生成代理 + 引用一致 |
| 二级缓存就够了 | 不够,无法延迟决定要不要代理 |
| 构造器循环依赖能解 | 不能,实例化早于三级缓存注册 |
| 原型 Bean 的循环依赖能解 | 不能,原型不缓存、不提前暴露 |
七、面试题自测
- 三级缓存分别存什么?读取优先级是什么?
- 为什么不能只用一 + 二级缓存?
- 循环依赖场景中,AOP 代理是在哪一步生成的?
- 为什么构造器注入的循环依赖无法解决?
- 哪些场景 Spring 明确不解决循环依赖?
八、Debug 小技巧
- 构造 A↔B 字段循环依赖,在
getEarlyBeanReference打断电:观察 B 注入 A 时,这里返回的是 A 的代理(若 A 有 AOP)还是原始对象。 - 在
DefaultSingletonBeanRegistry.getSingleton三个get处分别打断电,看 A 创建时三个缓存的先后命中顺序。 - 把 A、B 改成构造器互相注入,启动观察异常栈顶端
BeanCurrentlyInCreationException,确认失败发生在createBeanInstance。
下篇预告
第 10 篇,IoC 收尾:FactoryBean与 Spring 事件机制。讲清 MyBatis 的 Mapper 为何是FactoryBean,以及事件如何实现组件解耦。
如果这篇对你有帮助,欢迎点赞 · 收藏 · 关注三连支持。
Spring 源码系列共 30 篇,由浅入深持续更新中。有疑问或想深挖的源码点,评论区告诉我,下篇见。
