08 Spring 循环依赖怎么解决的?你背了三级缓存,但面试官其实想听这句

面过一个不错的候选人,Spring 源码看得挺多。问到循环依赖,他说:

“Spring 用三级缓存解决循环依赖。singletonObjects 存成品,earlySingletonObjects 存半成品,singletonFactories 存 ObjectFactory,用完后会删掉。”

对——完全对。市面上能说出这些的人已经不多了。

但我是面试官嘛,我又问了一句:“那为什么需要三级?为什么不是两级?”

他卡住了。

这道题的陷阱就在这里——几乎所有材料都讲了 Spring 三级缓存怎么用。几乎没有人讲为什么一定是三级,而不是两级、不是四级。

如果你到这里也卡住——来,我把这个“为什么”给你拆开。


先别管三级缓存——先搞清楚 Spring 里 beans 是怎么造的

假设 Spring 要创建一个 bean A,同时 A 依赖 B。

Spring 的流程大概是这样的:

① 反射创建 A 的实例(newInstance)——此时 A 还是空壳 ② 给 A 填充属性(populateBean)——发现 A 依赖了 B ③ Spring 去创建 B——同理:反射创建 B → 填充 B 的属性 → 发现 B 又依赖了 A 这时候 A 还是个空壳(还没完成 ②),但它的引用已经存在了 ④ 把空壳 A 的引用交给 B ⑤ B 建造完成(完整对象) ⑥ 回到 A——把完整的 B 注入给 A,A 建造完成

这就是循环依赖的基本流程。关键是第 ③~④ 步——Spring 把一个还没建造完全的 A 的半成品暴露给了 B

这个半成品怎么暴露?三级缓存。


三级缓存的真实威力

第一级:singletonObjects —— 存放完全成品的 bean

最普通的 HashMap。一个 bean 从创建到属性填充到初始化(init-method/@PostConstruct)全部完成后,才放进这个池。任何地方 getBean 就能拿到完整的对象。

第二级:earlySingletonObjects —— 存放半成品的 bean

一个 bean 刚反射创建出来(空壳),还没填充属性——但它已经有引用了。Spring 把它的引用放进二级缓存——让其他正在创建的 bean 能拿到这个半成品。

第三级:singletonFactories —— 存放产生半成品的工厂

这里不直接存 bean 实例——存的是一个 ObjectFactory(一个 lambda 表达式)。当 Spring 需要“从工厂里把半成品取出来”,这个 lambda 就执行——调用getEarlyBeanReference()方法,把你刚创建的空壳 A 包装一下(可能经过 AOP 代理),再返回给调用方。

为什么不是直接存半成品而要存一个工厂?因为 AOP 代理的存在。

如果你的 A 被 AOP 切面了(比如@Transactional@Async),Spring 需要创建一个代理类对象——这个代理对象就是半成品 A。但谁去创建这个代理——不是 Spring 初始化,而是SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference方法在第三级缓存的 lambda 里调用的。

如果没有第三级缓存——bean 刚反射创建后,Spring 还不知道它要被代理——直接把空壳塞进第二级缓存。后面 AOP 想要代理它——晚了。


如果只有两级缓存会怎样?

我们设想来一个只有 singletonObjects(成品)和 earlySingletonObjects(半成品)的体系:

① 反射创建 A 的空壳 → 立刻放进 earlySingletonObjects ② 填充 A → 需要 B → 创建 B ③ B 填充属性 → 依赖 A → 从 earlySingletonObjects 拿到空壳 A ④ B 完成 → 放进 singletonObjects ⑤ 回到 A,继续填充 → A 完成 → 从 earlySingletonObjects 挪到 singletonObjects

看起来没啥问题是吧?是——如果你没有 AOP。

假设 A 类的某个方法加了@Transactional。Spring 需要用 CGLIB 为它创建一个代理类——代码在AbstractAutoProxyCreator.getEarlyBeanReference()里。

你这时候调用getEarlyBeanReference(),它发现刚创建的空壳 A 需要被代理——但它应该把代理后的 A 放进第二级缓存,而不是把原始的空壳 A 放进第二级缓存。

这就是第三级缓存存在的全部意义。第三级缓存里的 ObjectFactory 在你需要半成品时才会执行——在它执行之前,你可以先把原始的空壳 A 跟传进来的 BeanPostProcessor 联合起来决定需不需要生成代理对象。

原始流程: 反射空壳 A → 立即放第二级缓存 → B 拿到原始空壳 → AOP 后 — 晚了 正确流程: 反射空壳 A → 放第三级缓存(ObjectFactory) → B 拿半成品时 — ObjectFactory 执行 → 在 lambda 里调 getEarlyBeanReference() → 如果有 AOP 就生成代理 → 返回代理对象给 B → 同时存入第二级缓存

面试最好的答法

第一档:说出三个缓存的名字——不及格。说明背过。

第二档:在第一档基础上说清它们的协作流程——“singletonObjects 存成品、earlySingletonObjects 存半成品、singletonFactories 存 ObjectFactory、当需要半成品时 lambda 执行、得到早期引用后从 singletonFactories 移入 earlySingletonObjects”——通过线。面试官承认你读过 Spring 源码。

第三档:在第二档基础上,关键的一句——“第三级缓存的存在本质是因为 Spring 的 AOP 代理时机——需要在属性填充之前就知道这个 bean 需不需要被代理。如果只有两级缓存,代理对象无法被正确传递到依赖方。Spring 选择用 ObjectFactory 做延迟架构,把 AOP 的决策点和 bean 的暴露点解耦。

你这段话说完,面试官在心里已经把你的评级调到了“这个人不只是知道源码——他理解了为什么要这么设计”。


面试官最后可能追问的三个问题

  • “那构造器注入能解决循环依赖吗?”不能。构造器注入时对象连空壳都还没产生,没法暴露半成品。

  • “prototype 作用域的 bean 能用三级缓存吗?”不能。prototype bean 每次创建新对象——Spring 不缓存 prototype bean,循环依赖直接抛异常。

  • “Spring Boot 解决循环依赖有没有更好的方案?”最好的方案是重构——循环依赖本身是设计问题。@Lazy注解延迟加载是紧急方案,但不是解药。重构代码,抽离出一个新的独立类(比如用@Configuration或用@Component提取共同依赖)——从根源干掉循环

这三个追问答出来一个,面试官大概率在心里给你盖章了。


Spring 循环依赖这道经典题,你是怎么面过的?面试官在你的回答后追了哪几个问题?留言区唠唠。


本文作者:资深 Java 开发老兵,前大厂面试官。不讲八股文,只讲面试官心里在想什么。系列持续更新,关注不迷路。

如果觉得有帮助,点个关注支持一下。

我最近整理了一份《大厂面试老兵资料包》,里面有评分表、高频场景题还有今年新出的 AIGC 面试题,

都是 200 多场面试里一点点攒的,市面上没人这么整理过。

需要的朋友私信我回复「666」,我发你领取方式。