研讨会
HOME
研讨会
正文内容
三级缓存的终极奥义:Spring 如何用 3 个 Map 解决循环依赖?
发布时间 : 2026-04-29
作者 : 小编
访问数量 : 9
扫码分享至微信

北京时间 2026 年 4 月 10 日发布

在日常 Spring 开发中,你一定写过类似这样的代码:ServiceA 调用 ServiceB 的业务,ServiceB 又需要反向回调 ServiceA。不知不觉间,A 依赖 B、B 又依赖 A——这就是 循环依赖(Circular Dependency) 。绝大多数开发者天天用 @Autowired,项目能跑起来就不深究,但一到面试被问到“循环依赖是怎么解决的”,要么语焉不详只说“三级缓存”,要么被追问“为什么二级不够”“构造器注入为什么不行”时当场卡壳-3。今天我们就从概念到源码,再到面试必考的高频问题,彻底把这套机制吃透。本文目标读者: 技术入门/进阶学习者、在校学生、面试备考者、相关技术栈开发工程师。本文定位: 技术科普 + 原理讲解 + 代码示例 + 面试要点,兼顾易懂性与实用性。


一、痛点切入:先看这段“会报错”的代码

java
复制
下载
// ❌ 这样写,Spring Boot 2.6+ 启动直接报错

@Service public class UserService { private final OrderService orderService; public UserService(OrderService orderService) { this.orderService = orderService; } } @Service public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService = userService; } }

启动报错信息非常直白:

text
复制
下载
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  userService defined in ...
↑     ↓
|  orderService defined in ...
└─────┘

为什么字段注入能正常运行,构造器注入就不行? 这就引出了 Spring 的核心设计:三级缓存。但在分析它之前,先看看如果完全没有 Spring 的“帮忙”,我们自己在普通代码中会如何陷入死锁。

二、旧有实现方式的致命缺陷

假设没有 Spring 的 IOC 容器,我们自己手动管理对象依赖:

java
复制
下载
// 原始手动创建方式
public class A {
    private B b;
    public A() {
        // 想在构造时就注入B,但B还没创建
    }
    public void setB(B b) { this.b = b; }
}

public class B {
    private A a;
    public B() {
        // 想在构造时就注入A,但A还没创建
    }
    public void setA(A a) { this.a = a; }
}
java
复制
下载
// 手动组装
A a = new A();   // 先创建A
B b = new B();   // 再创建B
a.setB(b);       // 把B注入A
b.setA(a);       // 把A注入B

这套“先实例化空壳,再互相赋值”的做法其实可行——关键在于先创建对象,后注入依赖。但它的缺点非常明显:

  • 耦合高: 创建对象的顺序需要开发人员手动控制,代码零散。

  • 扩展性差: 一旦类数量增多,手动维护依赖链变得极其困难。

  • 代码冗余: 每个依赖都需要显式的 setter 调用,重复代码随处可见。

Spring 的三级缓存设计,本质上就是对上述“先创建后注入”思路的系统化封装,再加上对代理对象(AOP)的精细处理,让框架自动完成这套复杂流程。

三、核心概念详解

概念 A:循环依赖(Circular Dependency)

标准定义: Circular Dependency,指的是两个或多个 Bean 之间互相持有对方的引用,形成闭环依赖关系-1

一句话理解: “先有鸡还是先有蛋”的问题——Bean A 需要 Bean B 才能创建,Bean B 又需要 Bean A 才能创建。

典型表现形式:

  1. 构造器注入产生的循环依赖:在 new 对象时就被堵住了,属于“硬死锁”,Spring 无法解决-1

  2. Setter/字段注入产生的循环依赖(单例模式):Spring 可以通过三级缓存解决-1

  3. 多例(Prototype)模式产生的循环依赖:每次 getBean() 都产生新对象,无穷无尽最终导致 OOM,Spring 无法解决-1

作用和价值: 理解循环依赖的解决机制,有助于深入理解 Spring 容器管理 Bean 生命周期的全貌,也是面试中衡量候选人源码理解深度的重要考点。

概念 B:三级缓存(Three-Level Cache)

标准定义: 三级缓存是 Spring 在 DefaultSingletonBeanRegistry 中定义的三个 Map,用于在不同阶段存储单例 Bean 的状态,协同解决循环依赖问题-45

  • 一级缓存:singletonObjects —— 存放完全初始化完成的“成品”Bean。

  • 二级缓存:earlySingletonObjects —— 存放提前暴露的“半成品”Bean(已实例化但未填充属性)。

  • 三级缓存:singletonFactories —— 存放 ObjectFactory(对象工厂函数),仅在调用 getObject() 时才会生成 Bean 实例-45

概念 A 与 B 的关系

  • 关系梳理: 三级缓存是解决循环依赖的具体实现手段,循环依赖是三级缓存要解决的核心问题

  • 一句话总结: 三级缓存是 Spring 用来“提前暴露半成品 Bean 引用”的工具集,从而打破循环依赖的闭环。

两种注入方式的区别与示例

对比维度Setter/字段注入(可解决)构造器注入(不可解决)
注入时机Bean 实例化之后,通过反射调用 setterBean 实例化期间,通过构造器传入
Bean 状态允许“空壳对象”存在必须拿到所有依赖才能创建对象
解决循环依赖✅ 三级缓存机制❌ 直接抛出异常

代码示例对比:

java
复制
下载
// ✅ Setter注入(Spring可以解决循环依赖)
@Service
public class A {
    private B b;
    @Autowired
    public void setB(B b) { this.b = b; }
}

// ❌ 构造器注入(Spring无法解决)
@Service
public class A {
    private final B b;
    public A(B b) { this.b = b; }  // 必须在构造时拿到B,但B依赖A,形成死锁
}

四、核心机制:三级缓存如何协同工作

三级缓存的定义

缓存级别名称数据结构作用
一级缓存singletonObjectsConcurrentHashMap<String, Object>存放完全初始化完成的成品 Bean,供业务直接使用-40
二级缓存earlySingletonObjectsHashMap<String, Object>存放提前暴露的“半成品”Bean(已实例化,未完成属性填充)-40
三级缓存singletonFactoriesHashMap<String, ObjectFactory<?>>存放 ObjectFactory 回调函数,用于按需生成半成品 Bean(支持 AOP 代理延迟创建)-40

完整解决流程(以 A 依赖 B、B 依赖 A 为例)

  1. 开始创建 A:调用 getSingleton("A"),三级缓存均无 A。Spring 实例化 A(调用构造器),得到原始对象 a_原始

  2. 将 A 放入三级缓存singletonFactories.put("A", ObjectFactory用来生产A的早期引用)

  3. 开始填充 A 的属性:发现 A 需要 B → 调用 getSingleton("B")

  4. 开始创建 B:实例化 B,将 B 的 ObjectFactory 放入三级缓存。

  5. 填充 B 的属性:发现 B 需要 A → 调用 getSingleton("A")

  6. 关键步骤——三级缓存中获取 A

    • 一级缓存查找 A → 没有(A 还在创建中)

    • 二级缓存查找 A → 没有

    • 三级缓存查找 A → 找到 ObjectFactory → 调用 getObject() 生成 A 的早期引用(半成品)→ 将其移入二级缓存

    • 将 A 的早期引用注入 B,B 完成初始化 → B 移入一级缓存

  7. 回到 A 的创建:拿到 B 的完整实例注入 A,A 完成初始化 → A 移入一级缓存,清理二级/三级缓存-13-40

核心理解:三级缓存机制的本质,就是在 Bean 的依赖注入阶段,允许“先拿地址后填属性”——只要对象的内存地址已确定,就可以被其他 Bean 引用,即便它的属性还没填完-


五、底层原理:为什么需要三级而不是两级?

这是面试中最容易翻车的追问。核心答案:为了支持 AOP 代理对象在循环依赖中的正确生成

  • 如果只用二级缓存(早期暴露直接存原始对象),当 Bean 需要被 AOP 代理(如加了 @Transactional)时,就无法保证循环依赖链中所有地方拿到的是同一个代理对象

  • 三级缓存存储的是 ObjectFactory(函数式接口),只有当真正被依赖时才会调用它生成早期引用。如果 Bean 需要 AOP,ObjectFactory 会在返回对象前执行代理增强,确保最终暴露的是代理对象而非原始对象。

  • 若在实例化阶段就强行生成代理对象,虽然也能解决问题,但会导致对象过早创建,无法体现懒加载(Lazy)思想-36

一句话总结:二级缓存只能解决“无 AOP”的循环依赖;三级缓存通过函数式接口延迟生成早期引用,解决了“有 AOP”场景下的代理对象一致性问题。


六、Spring 的解决边界

⚠️ 重要结论:Spring 只解决「单例 Bean 的字段/setter 循环依赖」,靠的是三级缓存 + 提前暴露“半成品”引用;构造器注入、原型作用域的循环依赖直接抛异常-

✅ Spring 能解决的场景

  • 依赖注入方式:Setter 注入 或 @Autowired 字段注入

  • Bean 作用域:单例(Singleton)

❌ Spring 无法解决的场景

场景原因解决方案
构造器注入构造时就必须完成所有依赖,无法提前暴露改用 Setter/字段注入;或对一方使用 @Lazy
原型(Prototype)Bean每次都是新对象,无法缓存和提前暴露避免设计上的循环依赖;改用单例 + 对象池
多例之间的循环无穷无尽创建,最终 OOM重新设计依赖关系

七、代码示例:字段注入的正常运行

java
复制
下载
// ✅ 字段注入(Spring可以解决循环依赖)
@Service
public class UserService {
    @Autowired
    private OrderService orderService;   // 字段注入
    
    public void createUser() {
        System.out.println("User created");
        orderService.createOrderForNewUser();
    }
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;     // 字段注入
    
    public void createOrderForNewUser() {
        System.out.println("Order created for user");
        // 这里可以反向调用 UserService 的方法
    }
}

启动项目,一切正常!因为字段注入允许 Bean 先被实例化(空壳对象),再通过反射完成依赖注入,三级缓存机制在其中发挥了作用-13


八、Spring Boot 2.6+ 的变化(加分项)

Spring Boot 2.6.0(Spring Framework 5.3)开始,为了鼓励更清晰的设计,默认禁用了循环依赖。如果项目中存在,启动时会直接报错,需要显式设置配置才能恢复旧版行为-14

yaml
复制
下载
 application.yml
spring:
  main:
    allow-circular-references: true    显式开启循环依赖

💡 面试加分技巧:提到这个版本变化,能体现你对技术动态的关注,表明你不是只背了“三级缓存”这个词。


九、高频面试题与参考答案

Q1:Spring 如何解决循环依赖?

标准答案要点(踩分点):

  1. Spring 通过三级缓存机制解决单例 Bean 的 Setter/字段注入循环依赖。

  2. 三级缓存包括:singletonObjects(一级,成品)、earlySingletonObjects(二级,半成品)、singletonFactories(三级,对象工厂)。

  3. 核心原理:在 Bean 实例化后、属性填充前,将早期引用提前暴露到三级缓存,打破依赖闭环-14

  4. 解决限制:构造器注入和多例模式无法解决。

Q2:为什么需要三级缓存?两级不够吗?

标准答案要点(踩分点):

  • 两级缓存只能解决不带 AOP 的循环依赖。

  • 三级缓存存储的是 ObjectFactory 函数式接口,在 Bean 被实际依赖时才调用生成早期引用。

  • 当 Bean 需要 AOP 代理(如 @Transactional)时,ObjectFactory 会在返回对象前执行代理增强,保证循环依赖链中所有地方拿到的是同一个代理对象-36

  • 若在实例化阶段直接生成代理对象存二级缓存,会破坏懒加载思想,且无法灵活处理 AOP 增强逻辑。

Q3:构造器注入的循环依赖为什么无法解决?

标准答案要点(踩分点):

  • 构造器注入要求在 Bean 实例化(调用构造器)时必须传入所有依赖。

  • 如果 A 的构造器需要 B、B 的构造器需要 A,两者都无法先完成实例化,没有“空壳对象”可供提前暴露。

  • 因此 Spring 会在启动阶段直接抛出 BeanCurrentlyInCreationException-51

Q4:原型(Prototype)Bean 的循环依赖为什么无法解决?

标准答案要点(踩分点):

  • 原型 Bean 每次请求都会创建新实例,Spring 不对其进行缓存。

  • 没有缓存就意味着无法提前暴露“半成品”引用供循环依赖中的其他 Bean 使用。

  • 若 A(原型)依赖 B(原型)、B 又依赖 A,会陷入无限创建循环,最终导致内存溢出(OOM)-1

Q5:除了三级缓存,还有哪些方式可以解决循环依赖?

标准答案要点(踩分点):

  1. @Lazy 延迟注入:对循环依赖的一方使用 @Lazy,Spring 注入代理对象,延迟实际初始化-60

  2. 手动从容器获取:实现 ApplicationContextAware,在需要时通过 getBean() 获取依赖-60

  3. 接口隔离/中间层解耦:抽取共同逻辑到中间组件,打破直接依赖(推荐长期方案)-60

  4. 事件驱动机制:通过 ApplicationEvent 发布/订阅模式解耦-60


十、总结回顾

核心知识点要点总结
循环依赖定义两个或多个 Bean 互相持有对方引用,形成闭环
Spring 解决范围仅单例 Bean + Setter/字段注入(构造器注入不行)
解决机制三级缓存:一级存成品、二级存半成品、三级存 ObjectFactory
为什么三级二级无法处理 AOP 代理对象的循环依赖
版本变化Spring Boot 2.6+ 默认禁用循环依赖,需显式开启
底层原理基于反射 + 代理 + 函数式接口(ObjectFactory)

重点提醒:

  • 循环依赖通常是代码设计有问题的信号,应优先考虑重构而非依赖框架的“兜底”机制。

  • 面试时不要只背“三级缓存”四个字,要能说清每级缓存存什么、为什么需要三级、构造器注入为何不行。

  • 提到 Spring Boot 2.6+ 的默认禁用策略,是展示技术敏感度的加分项。


下一篇预告:我们将深入 Spring AOP 的底层实现,剖析 JDK 动态代理与 CGLIB 的原理区别,以及 AOP 如何与三级缓存协同处理代理对象的循环依赖。敬请关注!

推荐阅读:

  • Spring 官方文档:Core Technologies

  • 三级缓存的终极奥义:Spring 如何用 3 个 Map 解决循环依赖

王经理: 180-0000-0000(微信同号)
10086@qq.com
北京海淀区西三旗街道国际大厦08A座
©2026  上海羊羽卓进出口贸易有限公司  版权所有.All Rights Reserved.  |  程序由Z-BlogPHP强力驱动
网站首页
电话咨询
微信号

QQ

在线咨询真诚为您提供专业解答服务

热线

188-0000-0000
专属服务热线

微信

二维码扫一扫微信交流
顶部