← 返回题目列表

Spring 如何解决循环依赖?三级缓存分别有什么作用?

高频 困难 第 15 / 30 题 更新于 2026/07/26
Spring循环依赖三级缓存早期引用

简化版

Spring 能通过提前暴露引用解决部分 singleton Bean 的属性或 setter 循环依赖,核心是 singletonObjectsearlySingletonObjectssingletonFactories 三级缓存。构造器循环依赖因对象尚未实例化而无法提前暴露,prototype 循环依赖也不能靠这套 singleton 缓存解决;循环依赖首先应视为设计问题,而不是容器能力。

详细版

三级缓存分别保存:完整初始化的单例、已创建但未完成初始化的早期单例引用、可按需产生早期引用的 ObjectFactory。创建 A 后、填充属性前,Spring 把 A 的早期引用工厂放入三级缓存;创建 B 需要 A 时调用该工厂取得早期引用,使 B 完成,随后 A 再完成初始化并进入一级缓存。

第三级不是单纯为了多放一份对象,而是延迟创建早期引用,并给 SmartInstantiationAwareBeanPostProcessor 提前返回代理的机会。这样其他 Bean 在循环依赖期间拿到的引用可以尽量与最终暴露对象保持一致。

完整版教学

一、先区分两类循环依赖

构造器循环在创建 A 时必须先得到 B,创建 B 又必须先得到 A,此时连一个可暴露的 A 实例都没有:

class A {
    A(B b) {}
}

class B {
    B(A a) {}
}

属性或 setter 循环不同:容器可以先调用构造器得到一个尚未填充属性的 A,再把这个早期引用提供给 B。因此三级缓存解决的是特定创建流程,不是所有循环图的通用算法。

二、三级缓存是什么

  • 一级 singletonObjects:已经完成创建、可正常使用的 singleton。
  • 二级 earlySingletonObjects:已经生成过的早期 singleton 引用,避免重复创建早期引用。
  • 三级 singletonFactories:能够按需生成早期引用的 ObjectFactory。

这些都是单例注册表内部的不同阶段状态。“三级”不是三种业务缓存,也不是用于提高查询性能的缓存设计。

三、A 与 B 的完整创建过程

以 A 属性依赖 B、B 属性依赖 A 为例:

  1. 容器实例化 A,但尚未注入 B;
  2. 将能生成 A 早期引用的工厂放进三级缓存;
  3. 为 A 注入 B,于是开始创建 B;
  4. B 实例化后需要 A,一级没有 A,便从三级工厂取得 A 的早期引用;
  5. 早期引用进入二级缓存,三级工厂被移除,B 完成初始化并进入一级缓存;
  6. A 得到完整的 B,继续初始化,最终 A 进入一级缓存并清理早期状态。

这里的“提前暴露”发生在实例化之后、完整初始化之前。因此早期对象不能被当成已经完成全部初始化的普通 Bean 随意使用。

四、为什么需要 ObjectFactory

如果创建完 A 就直接把原始对象放入二级缓存,遇到 A 需要 AOP 代理的情况,B 可能拿到原始 A,而其他 Bean 最终拿到代理 A,造成同一个 Bean 出现两种引用。

三级缓存中的工厂把“是否以及如何创建早期引用”推迟到真的发生循环依赖时。自动代理创建器可通过 getEarlyBeanReference 提前暴露代理;没有循环时则无需过早创建代理。第三级的关键价值是延迟与扩展点,不是仅为了计数达到三层。

五、哪些情况解决不了或不该解决

  • 构造器互相依赖:实例尚未产生,无法提前暴露。
  • prototype 互相依赖:不走 singleton 的完整缓存与提前暴露机制。
  • 代理、异步初始化或其他后处理导致早期引用与最终对象不一致:可能仍然启动失败。
  • 容器配置禁止循环引用:即使形式上可提前暴露,也会直接报错。

可以用 @Lazy 注入代理打断立即创建链,但这只是改变初始化时机。更可靠的方案是重新划分职责、抽出第三个协作对象、使用事件解耦,或把双向依赖改为单向数据流。

六、不要把循环依赖当正常设计

循环依赖会让初始化顺序、代理生成和单元测试都更难推理。能启动不代表对象关系合理,尤其不要为了让三级缓存生效而把清晰的构造器依赖改成字段注入。

面试中既要能解释容器机制,也要给出工程判断:三级缓存是兼容部分历史模型的机制,消除环才是优先方案。

七、用缓存状态表复盘一次循环

在 A、B 两个 singleton 属性互相依赖的例子里,A 刚实例化时三级缓存增加 1 个工厂,一级和二级都没有 A。B 查找 A 时才调用工厂并把所得早期引用放入二级;A 最终完成后进入一级,同时清除二、三级中的临时状态。数字“3”描述的是单例创建的三个状态槽位,不意味着一个 Bean 最终保留 3 份实例。

时刻一级 singletonObjects二级 earlySingletonObjects三级 singletonFactories
A 刚实例化无 A无 AA 的引用工厂
B 注入 A无 AA 的早期引用A 工厂被移除
A 完成初始化完整 A清除 A清除 A
create A → expose factory(A) → create B → B asks for A

complete A ← inject completed B ← early reference(A)

这张表也解释了二级缓存的必要性:早期引用一旦生成便保存下来,后续再请求时复用同一个引用,避免工厂多次产生不一致的早期代理。三级缓存则把“是否需要提前产生引用”的决定延迟到循环真正发生时。

记忆钩子:一级是成品区,二级是已拆封的早期引用,三级是尚未调用的引用工厂;关键不是缓存性能,而是对象身份一致性。

八、常见误区与追问

  • 误区:三级缓存可以解决所有循环依赖。 它主要服务 singleton 创建流程中的部分 setter 或字段依赖,构造器环和 prototype 环没有同样的提前暴露条件。
  • 误区:第三级缓存只是为了比二级缓存更快。 它的价值是延迟生成早期引用,并让后处理器有机会返回与 AOP 相关的早期代理。
  • 误区:能成功启动就说明循环依赖设计合理。 环会放大初始化顺序、代理身份和测试复杂度,应优先拆分职责或改成单向协作。
  • 追问:为什么构造器循环无法提前暴露? A 的构造器必须先得到 B,而创建 B 又需要 A,此时 A 实例尚不存在,容器没有可暴露引用。
  • 追问:@Lazy 为什么有时能打断构造器环? 它注入延迟解析代理,先满足构造参数的引用需求,把真实对象获取推迟到调用时;这改变时机但没有消除业务耦合。
  • 追问:早期引用一定是原始对象吗? 不一定,自动代理创建器可通过早期引用扩展点返回代理,目的正是尽量保持循环期间和最终暴露的对象身份一致。

九、加强记忆

一级放成品、二级放已生成的早期引用、三级放按需生成早期引用的工厂;工厂还能让 AOP 提前暴露合适的代理。它只解决部分 singleton 属性注入环,构造器和 prototype 环不行,而工程上应优先重构掉循环关系。