Sunday面试指南

Spring 循环依赖怎么解决?三级缓存为什么解决不了构造器循环依赖?

下面是一段教学用的模拟面试。

🧑‍💻 面试官:Spring 为什么能解决循环依赖?

🙋‍♂️ 我:因为有三级缓存,可以提前把 Bean 放进去。

🧑‍💻 面试官:A 的构造器要 B,B 的构造器也要 A。此时 A 还没有创建出来,缓存里能放什么?

🙋‍♂️ 我:没有实例,就没法提前引用它。

🧑‍💻 面试官:所以,你说的是哪一类循环?允许提前拿到的对象已经初始化好了吗?现在的 Boot 又默认允许吗?

缓存不是凭空造对象。先问:「实例是否已经存在」,以及「容器是否允许提前交付引用」。

面试速答(60 秒版)

Spring 循环依赖是 Bean 创建过程中的依赖形成了环,例如 A 需要 B,B 又需要 A。

对于允许循环引用的某些 singleton 属性注入场景,可以先创建 A 的实例,再注册取得 A 提前引用的工厂;创建 B 时拿到这个提前引用,让 B 完成,之后再回头完成 A。

常说的三级缓存,分别保存可交付的单例、已经取得的提前引用,以及产生提前引用的工厂。工厂这一层也给代理等处理留下了参与机会,不是三个相同的对象仓库。

但构造器相互依赖时,实例还没创建出来,就没有这样的提前引用可用;prototype 等情况也不能直接套用这条路径。当前 Spring Boot 文档中,allow-circular-references 默认是 false。

所以,工程处理通常先拆清职责或引入上层协调者,消除依赖环。延迟获取或开启兼容配置只能在理解语义后使用,不能把“能启动”当成设计已经合理。

实例先存在,才有提前引用

知识点详解:按创建时间,把这个环走一遍

先区分方法调用有环,还是对象创建有环

假设 Service A 的字段需要 Service B,Service B 的字段又需要 A。

这里讨论的是容器为了创建 Bean,必须取得另一个 Bean 的依赖环。不是说两个业务方法互相调用几次,容器就一定需要三级缓存解决。

同样,对象创建成功后仍可能因为方法相互递归而无限调用。提前引用解决了创建过程的一部分问题,并没有替业务终止递归。

Spring 依赖注入文档直接对比了构造器环与 setter 注入环。

属性注入为什么有机会绕过等待?

先假设是普通单例、属性注入,并且容器配置允许相关循环引用。

创建 A 时,先有 A 的实例,但字段 B 还没注入。容器为 A 注册一个可以提供提前引用的工厂,再去创建 B。

B 的实例也创建后,需要注入 A。此时发现 A 正在创建,容器可能从提前引用路径取得 A,注入 B。B 完成后,再注回 A,A 才继续完成自己的初始化。

整个过程里,B 曾经拿到的是尚未完成普通初始化流程的 A 引用。因此,不能在这个阶段随意调用 A,假设它所有字段和内部资源都已经准备好。

这条路径解释了“为什么有时能启动”,也同时暴露了提前交付的风险。

B 拿到 A 时,A 还没准备好

三级缓存分别保留什么?

以 Spring Framework 6.2.12 的 DefaultSingletonBeanRegistry 源码为具体参照:

  • singletonObjects:正常完成后登记、可供普通获取的单例。
  • earlySingletonObjects:已经取得并保存的提前引用。
  • singletonFactories:需要时产生提前引用的工厂,不是完成实例本身。

第一次从工厂得到提前引用后,会缓存该引用,避免相关获取过程各自随意产生不同结果。普通创建完成后,再登记最终单例并清理提前阶段的相关记录。

这不是三份永久并存的 Bean,也不代表容器中只有这三张表。所谓“三级”是常用的讲解概括,源码中的并发协调和其他登记信息还有更多细节。

第三级为什么保存工厂,而不直接保存原始对象?

因为调用者最终取得的 Bean,不一定就是未包装的原始对象。

在相关自动代理处理支持提前引用的路径中,工厂可以触发提前引用处理,使依赖方取得与后续交付相协调的引用。把所有情况都写成“B 注入原始 A,最后容器再随意换一个代理 A”,容易制造身份或行为不一致。

但也不能反过来宣称“只要三级缓存在,所有代理环都能解决”。代理处理器、注入方式与最终对象是否兼容都会影响结果。

面试回答应说明工厂留出的处理时机,不需要把它拔高成任何容器设计都必须有三层缓存的数学定理。

三层缓存,各自保留什么?

构造器环,为什么在更早的地方就卡住?

如果 A 的构造器参数必须有 B,容器就得先准备 B,才有机会调用 A 构造器。

如果 B 的构造器又必须有 A,那么 B 也无法完成实例化。两边都在等待另一边的实例,A 尚不存在,属性注入中那条“先造 A,再给出引用”的路没有起点。

延迟代理或提供者可以改变取得依赖的时间,但这属于修改依赖取得方式,不是原样的直接构造器环突然被缓存解决了。初始化阶段就急着解开延迟对象,也可能把问题带回来。

真正修复时,先画职责,不要先改开关

假设 A 负责保存资料,B 负责发送通知,两者互相依赖只是因为各自都想控制完整流程。

可以把流程交给上层协调服务 C:C 先调用 A 保存,再调用 B 通知。A、B 不再彼此控制,创建依赖和业务职责都更清楚。

如果必须异步通知,还要处理事务提交后再发送、可靠投递和重复处理,不是把调用改成事件就完成所有设计。

Spring Boot 配置文档当前把 spring.main.allow-circular-references 默认列为 false。即使为了兼容开启,也只是允许容器尝试相关解决路径,不保证所有环可解。先核对使用版本和项目配置,再解释启动结果。

本题是 Spring 容器机制,不编造对应 TS/Python 的三级缓存框架 API。

面试官继续追问

改成 @Lazy,是不是完全解决了?

它可以延迟某个依赖的实际取得,但必须确认第一次使用时机。若初始化中立即触发,或者业务仍相互递归,问题可能只是推迟出现。

prototype 的 setter 循环,也可以按同样方法处理吗?

不能直接套用单例提前引用缓存路径。先说明作用域,再讨论实际异常,不说所有属性注入都能解决。

对象提前出现了,能拿来正常工作吗?

不能默认。实例存在不等于初始化完成,提前引用应避免被当成完整就绪承诺。

面试速记卡

  • 创建环:A 创建需要 B,B 创建又需要 A。
  • 提前引用:实例先存在,才能给出尚未完成初始化的引用。
  • 三层职责:正常单例、已有提前引用、产生提前引用的工厂。
  • 限制:构造器环、作用域与代理情况不能一概保证解决。
  • 修复顺序:先拆职责,再考虑延迟取得与明确的兼容配置。

公司面试真题

这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。

浏览公司面试真题 →
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历