方舟AI助手带你彻底搞懂Spring循环依赖与三级缓存机制(2026年4月11日)

小编头像

小编

管理员

发布于:2026年05月08日

11 阅读 · 0 评论

本文导读:循环依赖是Spring面试中出场率超过5%的核心考点-,却让无数开发者“知其然而不知其所以然”。方舟AI助手今天就从痛点出发,带你打通“问题→概念→原理→面试”的完整链路,一篇搞定Spring循环依赖。

一、开篇引入:为什么循环依赖是Spring必考点

在Spring IoC容器中,循环依赖(Circular Dependency)是一个高频出现的问题,也是各大厂面试中的核心考点-1。所谓循环依赖,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系——最典型的就是Bean A依赖Bean B,同时Bean B又依赖Bean A-1

不少开发者在日常开发中会遇到这样的困境:

  • 代码跑得好好的,突然启动时报 BeanCurrentlyInCreationException,一脸懵

  • 面试时只知道“三级缓存”四个字,但说不清每一级存的是什么

  • 知道用 @Lazy 能解决,但说不明白为什么

  • 被追问“为什么需要三级,二级不够吗”就卡住了

这些痛点的根源在于:只停留在“会用”层面,没有建立完整的知识链路

本文将从问题剖析→核心概念→关联关系→代码示例→底层原理→面试要点六个维度,由浅入深地带你彻底搞懂Spring循环依赖。无论你是技术入门者、面试备考者,还是经验丰富的开发工程师,这篇文章都将帮你建立清晰的知识体系。

二、痛点切入:为什么需要循环依赖解决机制

2.1 传统依赖注入的困境

先看一个最简单的循环依赖场景:

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

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

在不做特殊处理的情况下,Spring启动时会按以下流程尝试创建这两个Bean:

  1. 开始创建A → 实例化A(调用构造函数生成原始对象)→ 填充属性,发现需要B

  2. 容器中找不到B,于是开始创建B → 实例化B → 填充属性,发现需要A

  3. 容器中找不到A,于是尝试创建A → 此时A正在创建中,无法重新创建 → 抛出 BeanCurrentlyInCreationException 异常-1

2.2 旧有方式的缺陷

如果没有Spring的三级缓存机制,面对这种相互依赖的场景,开发者只能采取以下几种“笨办法”:

  • 手动拆分类,将相互依赖的逻辑提取到第三方类

  • 使用getter/setter延迟获取,在第一次使用时才去容器中获取

  • 使用setter注入并在XML中配置依赖顺序

这些方式的共同缺陷是:耦合度高、侵入性强、可维护性差。 而Spring的设计初衷恰恰是“让开发者专注于业务,把依赖管理的复杂性交给框架”。这也是三级缓存机制诞生的核心动机——在框架层面透明地解决循环依赖,对开发者零侵入

三、核心概念讲解:什么是循环依赖

3.1 标准定义

循环依赖(Circular Dependency),英文全称 Circular Dependency,是指在Spring IoC容器管理的Bean之间,存在一个相互依赖的“环”。例如,Bean A的创建需要注入Bean B,而Bean B的创建又需要注入Bean A,导致容器无法确定谁应该先被完全初始化-

3.2 关键拆解

要理解循环依赖,需要抓住三个关键点:

  1. 依赖方向:依赖必须是双向的(A→B且B→A),单向依赖不会构成循环

  2. 作用域限定:Spring能解决的循环依赖仅限于单例(Singleton)作用域,原型(Prototype)作用域的循环依赖无法解决-55

  3. 注入方式限定:Spring能解决的仅限于setter注入或字段注入(即属性填充阶段发生的依赖),构造器注入无法通过三级缓存解决-55

3.3 生活化类比

想象一个场景:A和B同时要办身份证,但办证中心要求必须出示对方的担保函才能办理。

  • 传统的做法是:A等B先办完拿到担保函,B等A先办完拿到担保函——两人互相等待,永远办不完

  • Spring的做法是:A先领一张“承诺函”(半成品引用),B拿着这张承诺函去办证,办完后回来把真正的担保函给A——两人都能办完

这个“承诺函”就是Spring三级缓存中的“提前暴露的半成品Bean”。

四、关联概念讲解:三级缓存的三个层次

4.1 标准定义

三级缓存(Three-Level Cache),英文对应 singletonObjectsearlySingletonObjectssingletonFactories,是Spring框架为解决单例Bean循环依赖而设计的三个核心Map缓存结构-1

4.2 三层次详解

缓存级别缓存名称存储内容作用
一级缓存singletonObjects完全初始化完成的Bean(成品)供业务直接使用,所有getBean()优先从此获取
二级缓存earlySingletonObjects提前暴露的半成品Bean(已实例化,未填充属性)解决循环依赖时,让依赖方能拿到“半成品”引用
三级缓存singletonFactoriesObjectFactory对象工厂(Lambda表达式)按需生成Bean实例,延迟决定是否创建AOP代理

4.3 三级缓存与循环依赖的关系

  • 循环依赖是“问题” ,需要一种机制来打破依赖闭环

  • 三级缓存是“解决方案” ,通过提前暴露半成品引用来打破闭环

  • 三级缓存中的三级缓存(singletonFactories)是最关键的设计,它不直接存储Bean对象,而是存储一个“工厂”,等到真正需要暴露引用时才调用工厂生成对象-2

五、概念关系与区别总结:一句话串起逻辑

循环依赖是“病”,三级缓存是“药”;三级缓存是循环依赖的具体解决方案,循环依赖是三级缓存要解决的核心问题。

更精确地说:

维度循环依赖三级缓存
本质问题/现象解决方案/机制
触发条件双向依赖关系检测到循环依赖时启用
核心操作依赖闭环提前暴露半成品引用
适用场景单例+setter/字段注入同左

记忆口诀:一级存成品,二级存半成品,三级存工厂;依赖出现时,工厂变半成品,半成品变成品-2

六、代码示例:从现象到解决

6.1 场景:循环依赖的复现

java
复制
下载
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;   // A依赖B
    
    public void doSomething() {
        System.out.println("ServiceA执行中");
        serviceB.execute();
    }
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;   // B依赖A,形成循环
    
    public void execute() {
        System.out.println("ServiceB执行中");
        serviceA.doSomething();
    }
}

启动结果:Spring Boot 2.6+版本默认禁止循环依赖,直接抛出异常-54

text
复制
下载
APPLICATION FAILED TO START
Description:
The dependencies of some of the beans in the application context form a cycle:
   serviceA (field private com.example.ServiceB com.example.ServiceA.serviceB)
   serviceB (field private com.example.ServiceA com.example.ServiceB.serviceA)

6.2 Spring的三级缓存解决流程(以A↔B为例)

假设Spring容器开始创建Bean A和Bean B,详细步骤如下:

步骤1:开始创建A

  • 实例化A(调用构造函数,生成原始对象,此时b=null

  • 将A的ObjectFactory放入三级缓存singletonFactories

  • 开始填充A的属性,发现需要注入B

步骤2:开始创建B

  • 实例化B,生成原始对象

  • 将B的ObjectFactory放入三级缓存singletonFactories

  • 填充B的属性,发现需要注入A

  • 在容器中查找A → 一级缓存无 → 二级缓存无 → 从三级缓存获取A的ObjectFactory,调用后生成A的早期引用

  • 将A的早期引用放入二级缓存earlySingletonObjects,同时移除三级缓存中的A

  • 用二级缓存中的A引用完成B的属性注入,B继续完成初始化

  • B初始化完成后,存入一级缓存singletonObjects,清除二三级缓存中的B

步骤3:完成A的创建

  • A的属性注入继续,此时从一级缓存拿到B(已完成)

  • A完成初始化,存入一级缓存singletonObjects,清除二三级缓存中的A

核心要点:B在属性填充时拿到的是A的“早期引用”(已实例化但未完全初始化的A),这个引用虽然是“半成品”,但Java的引用传递机制保证了后续A完成初始化后,B持有的引用自动指向完整对象。

6.3 新旧对比:直观展示改进效果

维度传统做法Spring三级缓存
代码侵入需要手动拆分或使用延迟加载技巧零侵入,正常写@Autowired即可
可维护性依赖关系复杂,修改成本高框架透明处理,开发者专注业务
性能开销每次获取都需要额外处理仅首次创建时有Map操作开销
是否支持双向依赖需手动规避自动支持(限定场景内)

6.4 解决方案:调整注入方式

将构造器注入改为setter注入或字段注入,Spring就能正常处理:

java
复制
下载
@Component
public class A {
    private B b;
    
    @Autowired   // 字段注入,支持循环依赖
    public void setB(B b) { this.b = b; }
}

@Component
public class B {
    private A a;
    
    @Autowired   // 字段注入,支持循环依赖
    public void setA(A a) { this.a = a; }
}

七、底层原理支撑

三级缓存机制能够优雅地解决循环依赖,底层依赖以下几个关键技术:

7.1 Java引用传递机制

Java中的对象引用本质上是一个指向堆内存地址的指针。当B持有A的早期引用时,B只是拿到了A在堆中的“地址”。后续A完成初始化后,同一地址上的内容变为完整对象,B持有的引用自动“看到”了完整内容-

7.2 反射机制

Spring通过反射(Reflection)实现Bean的实例化和属性注入。Constructor.newInstance()Field.set()是属性填充阶段的核心操作。反射让Spring可以在编译时未知类结构的情况下动态操作对象。

7.3 动态代理(AOP相关)

当Bean需要被AOP代理(如添加@Transactional)时,Spring不会在实例化时就创建代理,而是通过三级缓存中的ObjectFactory第一次被其他Bean引用时才按需创建代理对象-2。这正是三级缓存必须存在而非二级缓存的核心原因。

7.4 工厂模式

三级缓存中的singletonFactories本质上是工厂设计模式的体现——它不直接存储对象,而是存储一个ObjectFactory(函数式接口),在getObject()被调用时才真正创建对象-。这种“延迟创建”的设计既解决了循环依赖,又为AOP代理保留了灵活性。

7.5 为什么必须用三级缓存?二级不够吗?

这是面试中的终极追问。答案是:如果只是解决简单的循环依赖(无AOP代理),二级缓存确实够用。

但Spring必须支持AOP场景。假设只有二级缓存:

  • Bean A实例化后,需要立即决定是否创建代理并放入二级缓存

  • 但此时还没走到BeanPostProcessor的初始化后处理阶段,无法知道A是否需要AOP增强

  • 如果提前创建了代理,可能和最终代理对象不一致;如果不创建代理,B拿到的就是原始对象而非代理对象

三级缓存将“要不要代理”的决策延迟到第一次被引用时,通过ObjectFactory中的getEarlyBeanReference()方法按需生成代理,保证了无论何时暴露出去的对象都和最终对象一致-2-

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

面试题1:Spring是如何解决循环依赖的?三级缓存分别是什么?

标准答案(踩分点)

  1. 核心机制:Spring通过三级缓存(Three-Level Cache)机制解决单例Bean的setter/字段注入循环依赖问题-13

  2. 三级缓存定义

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

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

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

  3. 解决思路:在Bean实例化后、属性填充前,将其ObjectFactory放入三级缓存。当出现循环依赖时,从三级缓存获取工厂生成早期引用,放入二级缓存供依赖方使用-55

  4. 适用范围:仅适用于单例作用域 + setter/字段注入的场景。

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

标准答案(高分版)

二级缓存可以解决简单的循环依赖,但无法正确处理AOP代理场景-13

详细分析

  • 如果只有二级缓存,Bean实例化后就必须决定是否创建代理对象并放入缓存

  • 但此时尚未执行BeanPostProcessor的初始化后处理,无法确定是否需要AOP增强

  • 三级缓存通过存储ObjectFactory,将代理对象的创建延迟到第一次被其他Bean引用时,通过getEarlyBeanReference()按需决定返回原始对象还是代理对象

  • 这保证了循环依赖中暴露的对象与最终单例池中的对象完全一致

一句话概括:三级缓存的核心价值是兼顾循环依赖破局与AOP代理的正确性

面试题3:构造器注入的循环依赖为什么解决不了?

标准答案

构造器注入要求在实例化阶段就提供所有依赖,而循环依赖场景下两个Bean的构造器相互等待对方完成实例化,形成死锁-13。三级缓存机制依赖于“实例化后、属性填充前”这个时间窗口来提前暴露半成品引用,但构造器注入的依赖在实例化阶段就被触发了,根本来不及走到暴露引用的步骤。

解决方案:改用setter注入或字段注入,或使用@Lazy注解延迟加载。

面试题4:Spring Boot 2.6+版本中循环依赖发生了什么变化?

标准答案

从Spring Boot 2.6.0开始,默认禁止了循环依赖。如果项目中存在循环依赖,启动时会直接抛出异常并拒绝启动-

原因:官方认为循环依赖本质上是“代码坏味道”,应当通过重构代码来消除而非依赖框架解决。

解决方案

  1. 重构代码(推荐):提取公共逻辑到新Service,采用单向依赖

  2. 使用@Lazy注解:延迟初始化,打破创建顺序依赖

  3. 临时回退:在配置文件中设置spring.main.allow-circular-references=true(不推荐)

面试题5:原型(prototype)作用域的循环依赖能解决吗?

标准答案

不能。Spring只在单例作用域下通过缓存机制解决循环依赖。原型作用域的Bean每次获取都会创建新实例,没有缓存可以复用,Spring无法提前暴露“半成品”引用-55。遇到原型作用域的循环依赖,会直接抛出异常。

九、结尾总结

9.1 核心知识点回顾

知识点核心内容
循环依赖定义Bean之间形成双向依赖闭环
三级缓存singletonObjects(成品)、earlySingletonObjects(半成品)、singletonFactories(工厂)
适用范围单例 + setter/字段注入
不适用场景构造器注入、原型作用域、AOP代理(三级缓存已解决)
解决原理实例化后提前暴露半成品引用,打破依赖闭环
底层支撑Java引用传递、反射、工厂模式、动态代理

9.2 重点与易错点提醒

  • 易错点1:三级缓存并非“三个缓存同时存同一个Bean”,而是Bean在不同生命周期存放在不同级别的缓存中

  • 易错点2:Spring能解决的循环依赖仅限于单例+setter/字段注入,不要认为“Spring能解决所有循环依赖”

  • 易错点3:Spring Boot 2.6+默认禁止循环依赖,生产项目中遇到循环依赖优先考虑重构而非开启配置

  • 面试踩分关键:答出三级缓存的三个Map名称各自存储内容、以及为什么需要三级而非两级(AOP场景)

9.3 进阶预告

本文完整梳理了Spring循环依赖的核心原理与三级缓存机制。如果读者想进一步深入,后续文章将围绕以下方向展开:

  • Spring Bean完整生命周期:从BeanDefinition到最终Bean的12步全过程

  • Spring AOP底层原理:JDK动态代理 vs CGLIB,代理对象的创建时机

  • 循环依赖与AOP的深度结合getEarlyBeanReference()的源码级分析


本文由方舟AI助手整理,数据截止2026年4月11日。如需获取最新技术资讯或深度源码分析,欢迎持续关注。

标签:

相关阅读