电子技术
HOME
电子技术
正文内容
2026面试必考:全AI智能助手带你吃透Spring循环依赖三级缓存
发布时间 : 2026-04-21
作者 : 小编
访问数量 : 10
扫码分享至微信

北京时间 2026-04-10 | 阅读时长:约 12 分钟


开篇引入

在 Spring 面试中,“Spring 如何解决循环依赖”是一道绕不开的高频考题。很多开发者背下了“三级缓存”这个答案,却说不清为什么需要三级、二级够不够、构造器注入为何无法解决-24。作为 Java 开发者日常使用频率最高的框架之一,Spring 的这一设计既精妙又复杂,理解它不仅是面试通关的关键,更是深入掌握 IoC 容器运作机制的基础。

本文将从问题痛点 → 核心概念 → 源码解析 → 代码示例 → 面试要点逐层展开,帮助你建立完整的知识链路。无论是技术入门者、面试备考者,还是希望深入理解 Spring 原理的开发者,都能从中获得可落地的收获。


一、痛点切入:为什么需要循环依赖解决方案?

1.1 什么是循环依赖?

循环依赖,指的是两个或多个 Bean 之间互相持有对方的引用,形成闭环依赖关系。最典型的就是 A 依赖 B,B 又依赖 A-1

java
复制
下载
@Service
public class A {
    @Autowired
    private B b;  // A 依赖 B
}

@Service
public class B {
    @Autowired
    private A a;  // B 依赖 A,形成循环
}

如果 Spring 不做特殊处理,应用启动时就会抛出 BeanCurrentlyInCreationException 异常-1

1.2 旧有方式的缺陷

在不支持循环依赖的传统 IoC 容器设计中,Bean 创建流程是“实例化 → 注入依赖 → 初始化”的顺序执行。当遇到 A 依赖 B、B 依赖 A 的情况时,容器会陷入无限等待——要创建 A 就必须先有 B,要创建 B 又必须先有 A。这种“鸡生蛋、蛋生鸡”的死锁问题会导致容器直接报错退出。

这一缺陷带来的问题:

问题具体表现
耦合度过高两个类职责边界模糊,互相引用意味着它们承担了过多耦合的职责
可维护性差修改其中一个类时,另一个类往往也需要同步调整
测试困难单元测试时需要同时 Mock 两个相互依赖的服务
启动失败应用无法正常启动,影响部署和交付

1.3 设计初衷

正是为了解决这一痛点,Spring 设计了三级缓存机制,通过“提前暴露正在创建中的 Bean”来打破依赖闭环,使容器能在不牺牲代码可读性的前提下优雅地处理循环依赖问题-2


二、核心概念:Spring 的三级缓存

Spring 通过三个 Map 来存储不同状态的 Bean,分别对应 Bean 生命周期的不同阶段。

2.1 一级缓存:singletonObjects(成品池)

定义private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);-1

作用:存放完全初始化完成的单例 Bean(成品对象),供业务代码直接使用。Bean 走完实例化 → 属性填充 → 初始化的全部流程后才会被放入这里-2

💡 类比:就像超市货架上已经包装好的商品,消费者可以直接取用。

2.2 二级缓存:earlySingletonObjects(半成品池)

定义private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);-1

作用:存放已实例化但尚未完成属性填充和初始化的提前暴露 Bean(半成品)。当 A 依赖 B、B 又依赖 A 时,A 在属性注入前会把自己“提前暴露”出来,放进这个缓存,供 B 在创建时获取 A 的早期引用-2

💡 类比:就像在生产线上的半成品,虽然没有全部完工,但已经有基本形状可供其他工序使用。

2.3 三级缓存:singletonFactories(对象工厂池)

定义private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);-1

作用:存放 ObjectFactory(对象工厂),这是一个函数式接口,仅在调用 getObject() 时才会创建 Bean 实例-1。它不存对象,而是存一个 lambda 表达式,如 () -> getEarlyBeanReference(beanName, mbd, bean)-2

💡 类比:就像“商品兑换券”,不直接给你商品,而是在需要的时候再去兑换——什么时候要,什么时候才生成。


三、关联概念:构造器注入 vs 字段/Setter 注入

3.1 两种注入方式的区别

对比维度构造器注入字段/Setter 注入
定义通过构造函数参数声明依赖通过 @Autowired 或 Setter 方法注入
依赖时机实例化时就需要所有依赖实例化后、属性填充阶段注入
循环依赖支持❌ 不支持✅ 支持(单例模式下)
Spring 推荐度⭐⭐⭐⭐⭐ 官方最推荐⭐⭐⭐ 兼容性强

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

构造器注入要求在实例化时就提供所有依赖,而循环依赖场景下两个 Bean 都无法完成实例化——Spring 的三级缓存机制依赖于“提前暴露早期引用”,这在构造器注入场景下无法实现-4

java
复制
下载
// ❌ 构造器注入——无法解决循环依赖,启动直接报错
@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;
    }
}

3.3 概念关系总结

一句话总结:构造器注入是设计规范,三级缓存是实现手段;前者强调“依赖的不可变性”,后者解决“依赖的循环性”。Spring 允许你在设计规范与运行灵活性之间做权衡


四、代码示例:三级缓存解决循环依赖的全流程

4.1 极简示例代码

java
复制
下载
// A 依赖 B
@Component
public class A {
    @Autowired
    private B b;
    
    public void doSomething() {
        System.out.println("A is doing something");
    }
}

// B 依赖 A
@Component
public class B {
    @Autowired
    private A a;
    
    public void doSomething() {
        System.out.println("B is doing something");
    }
}

4.2 执行流程详解

步骤动作缓存状态变化
1开始创建 Bean A将 A 的名称加入 singletonsCurrentlyInCreation
2A 实例化完成(调用构造器)将 A 的 ObjectFactory 放入三级缓存
3填充 A 的属性,发现需要 B触发 B 的创建流程
4开始创建 Bean BB 实例化完成,将 B 的 ObjectFactory 放入三级缓存
5填充 B 的属性,发现需要 A从三级缓存获取 A 的 ObjectFactory → 调用 getObject() 生成 A 的早期引用 → 放入二级缓存移除三级缓存中的 A
6B 的属性填充完成B 继续完成初始化,初始化后 B 移入一级缓存,移除二级缓存
7返回 A 的创建流程A 从一级缓存获取 B 并注入
8A 完成初始化A 移入一级缓存,移除二级缓存

🎯 关键理解:三级缓存中存的是工厂而不是 Bean 实例本身,这个设计让 Spring 能够在真正需要暴露早期引用时,才动态决定是否生成 AOP 代理对象-2


五、底层原理:为什么一定要三级缓存?

5.1 核心疑问

很多面试者都会问:用二级缓存不就能解决循环依赖了吗?为什么非要用三级缓存?

5.2 答案:AOP 代理是根本原因

如果只有二级缓存,Spring 就不得不在 Bean 实例化之后立刻决定是否生成 AOP 代理。但此时尚未走到初始化阶段,无法判断是否需要增强(比如 @PostConstruct 方法里才添加的切面标记)-2

三级缓存通过 ObjectFactory 工厂,将“是否生成代理”的判断延迟到第一次被其他 Bean 引用时,实现了两个目标:

  • 按需生成代理:只有当其他 Bean 真正需要这个早期引用时,才调用工厂生成对象

  • 保证代理唯一性:同一个 Bean 被多个地方依赖时,能确保返回的是同一个代理对象,避免地址不一致的问题

📌 一句话总结:三级缓存不是炫技,而是对“懒加载代理”和“循环依赖破局”的一次精巧兼顾-2

5.3 底层技术支撑

这一机制底层依赖了 Spring 的核心知识点:

  • 反射(Reflection):动态调用构造器和 Setter 方法

  • 代理模式(Proxy Pattern):CGLIB 和 JDK 动态代理实现 AOP

  • 函数式接口(ObjectFactory:延迟执行,按需创建对象

  • Map 缓存与并发控制:通过 ConcurrentHashMapsynchronized 保证线程安全


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

面试题 1:Spring 是如何解决循环依赖的?

参考答案(踩分点)

Spring 通过三级缓存机制解决单例模式下 Setter/字段注入的循环依赖问题:

  • 一级缓存(singletonObjects) :存放完全初始化的成品 Bean

  • 二级缓存(earlySingletonObjects) :存放提前暴露的半成品 Bean

  • 三级缓存(singletonFactories) :存放 ObjectFactory 对象工厂

核心思路是 “提前暴露正在创建中的 Bean” ,在 Bean 实例化后、属性填充前,将其工厂放入三级缓存,当依赖方需要该 Bean 时,通过工厂生成早期引用供其使用,从而打破循环依赖的死锁。

面试题 2:为什么需要用三级缓存,二级不行吗?

参考答案(踩分点)

不行。二级缓存解决不了 AOP 代理对象提前暴露的问题:

  • 如果只有二级缓存,Spring 必须在实例化后立即决定是否生成代理对象

  • 但此时 Bean 还未初始化,无法确定是否需要增强(如 @Transactional@Async 等注解是在初始化阶段生效的)

  • 三级缓存将“是否生成代理”的判断延迟到第一次被其他 Bean 引用时,既解决了循环依赖,又保证了 AOP 的正确生效

面试题 3:Spring 无法解决哪些循环依赖?

参考答案(踩分点)

以下三种场景 Spring 无法自动解决循环依赖:

  1. 构造器注入循环依赖:实例化时需要所有依赖,无法提前暴露半成品

  2. Prototype 多例模式循环依赖:Spring 不对 prototype 作用域的 Bean 使用缓存标记机制

  3. Spring Boot 2.6+ 默认配置下spring.main.allow-circular-references=false,默认禁止循环依赖,需要手动开启

面试题 4:Spring Boot 2.6+ 为什么要默认禁止循环依赖?

参考答案(踩分点)

Spring 团队基于以下考量:

  • 设计原则:循环依赖违反单一职责原则,鼓励开发者重构代码而非依赖框架

  • 运行风险:循环依赖可能导致事务代理失效(提前暴露的可能是原始对象而非代理对象)

  • 长期维护:强制开发者在早期就考虑更清晰的模块划分和依赖方向-33


七、总结与进阶

7.1 核心知识点回顾

知识点核心要点
问题定义A 依赖 B、B 依赖 A 形成的闭环依赖关系
解决方案三级缓存机制:一级存成品、二级存半成品、三级存工厂
适用条件单例模式 + 字段/Setter 注入
不适用场景构造器注入、Prototype 模式
设计精髓三级缓存的本质是为了解决 AOP 代理对象的提前暴露问题
最新趋势Spring Boot 2.6+ 默认禁止循环依赖,推动开发者走向更解耦的设计

7.2 易错点提醒

⚠️ 常见误区澄清:

  • “Spring 能解决所有循环依赖” ❌ → 构造器循环依赖无法解决

  • “三级缓存是为了性能” ❌ → 核心是为了解决 AOP 代理问题

  • “只要加 @Lazy 就行” ❌ → 治标不治本,且影响启动速度-24

7.3 进阶预告

下篇我们将深入 Spring AOP 的底层原理,讲解动态代理是如何在三级缓存机制中与循环依赖协作的,敬请关注!


本文由全AI智能助手基于 Spring 5.3.x 源码及业界最佳实践整理,旨在帮助开发者构建完整的 Spring 循环依赖知识体系。如有疑问,欢迎在评论区交流讨论。

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

QQ

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

热线

188-0000-0000
专属服务热线

微信

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