ai助手演示:2026年4月Spring循环依赖三级缓存原理深度拆解

小编头像

小编

管理员

发布于:2026年04月28日

4 阅读 · 0 评论

原创首发:2026年4月9日
目标读者:技术入门/进阶学习者、在校学生、面试备考者、Java开发工程师
文章定位:技术科普 + 原理讲解 + 代码示例 + 面试要点
核心目标:让读者理解Spring循环依赖的底层逻辑,看懂三级缓存的设计精妙,拿下面试高频考点

在Spring生态中,循环依赖(Circular Dependency) 是一个高频出现的问题,也是Java面试中被问到“翻来覆去”的核心考点。很多开发者日常写代码时依赖@Autowired字段注入,项目跑得风生水起,但当面试官追问“Spring是如何解决循环依赖的?为什么需要三级缓存而不是二级?”时,却答不上来,甚至把“三级缓存”当成了面试“标准答案”背一背就过去了。本文由AI助手演示演示技术资料、内容整合与结构化写作能力,带你从“只会用”到“懂原理”,层层拆解Spring循环依赖的底层机制,配合代码示例和面试高频题,帮助你建立完整的知识链路。

📌 本文涉及的所有数据、版本信息均来源于公开技术资料及官方文档,核心原理讲解基于Spring 5.3.x源码,时效性截至2026年4月。

一、痛点切入:为什么会出现循环依赖?

在Spring IoC容器中,Bean的创建分为三个关键阶段:实例化(实例化)填充属性(注入依赖)初始化-30。问题就出在第二阶段。

1.1 传统实现方式的困境

假设有UserService依赖OrderServiceOrderService又依赖UserService,形成典型的循环依赖-2

java
复制
下载
@Service
public class UserService {
    @Autowired
    private OrderService orderService;
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;
}

这段代码在日常开发中完全可以正常启动,但背后的“真相”并非如此简单。

1.2 为什么会报错?——构造器注入的崩溃

如果把上面的字段注入改为构造器注入

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;
    }
}

启动后直接报错:

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

-2

为什么会这样? 构造器注入要求在实例化时就传入所有依赖参数,这意味着UserService在创建之前必须先有OrderService实例,而OrderService在创建之前又必须有UserService实例——死锁了。

1.3 传统方式的本质缺陷

无论是构造器注入的“无法解决”还是字段注入的“神奇解决”,传统实现方式暴露了以下问题:

缺陷说明
耦合度高两个Bean之间形成了强依赖关系,职责边界模糊
理解成本高开发者只知道“字段注入能跑通”,不知道背后原理
面试答不出背了“三级缓存”四个字,却说不出为什么需要三级、二级够不够
版本变化敏感Spring Boot 2.6.x开始默认禁止循环依赖,传统写法直接报错-

1.4 Spring的设计初衷

Spring引入三级缓存机制的核心目的,就是通过提前暴露半成品Bean的方式,在单例Bean的setter/字段注入场景下打破依赖闭环,让上述代码“跑起来”-13。但理解这个机制,需要先理清Spring Bean的完整生命周期。

二、核心概念讲解:Spring Bean的生命周期

2.1 什么是Bean的生命周期?

一个Spring Bean从创建到销毁,大致经历以下关键阶段-30

图表
代码
下载
全屏
.kvfysmfp{overflow:hidden;touch-action:none}.ufhsfnkm{transform-origin: 0 0}
mermaid-svg-4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}mermaid-svg-4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}mermaid-svg-4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}mermaid-svg-4 .error-icon{fill:552222;}mermaid-svg-4 .error-text{fill:552222;stroke:552222;}mermaid-svg-4 .edge-thickness-normal{stroke-width:1px;}mermaid-svg-4 .edge-thickness-thick{stroke-width:3.5px;}mermaid-svg-4 .edge-pattern-solid{stroke-dasharray:0;}mermaid-svg-4 .edge-thickness-invisible{stroke-width:0;fill:none;}mermaid-svg-4 .edge-pattern-dashed{stroke-dasharray:3;}mermaid-svg-4 .edge-pattern-dotted{stroke-dasharray:2;}mermaid-svg-4 .marker{fill:333333;stroke:333333;}mermaid-svg-4 .marker.cross{stroke:333333;}mermaid-svg-4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}mermaid-svg-4 p{margin:0;}mermaid-svg-4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:333;}mermaid-svg-4 .cluster-label text{fill:333;}mermaid-svg-4 .cluster-label span{color:333;}mermaid-svg-4 .cluster-label span p{background-color:transparent;}mermaid-svg-4 .label text,mermaid-svg-4 span{fill:333;color:333;}mermaid-svg-4 .node rect,mermaid-svg-4 .node circle,mermaid-svg-4 .node ellipse,mermaid-svg-4 .node polygon,mermaid-svg-4 .node path{fill:ECECFF;stroke:9370DB;stroke-width:1px;}mermaid-svg-4 .rough-node .label text,mermaid-svg-4 .node .label text,mermaid-svg-4 .image-shape .label,mermaid-svg-4 .icon-shape .label{text-anchor:middle;}mermaid-svg-4 .node .katex path{fill:000;stroke:000;stroke-width:1px;}mermaid-svg-4 .rough-node .label,mermaid-svg-4 .node .label,mermaid-svg-4 .image-shape .label,mermaid-svg-4 .icon-shape .label{text-align:center;}mermaid-svg-4 .node.clickable{cursor:pointer;}mermaid-svg-4 .root .anchor path{fill:333333!important;stroke-width:0;stroke:333333;}mermaid-svg-4 .arrowheadPath{fill:333333;}mermaid-svg-4 .edgePath .path{stroke:333333;stroke-width:2.0px;}mermaid-svg-4 .flowchart-link{stroke:333333;fill:none;}mermaid-svg-4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}mermaid-svg-4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}mermaid-svg-4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}mermaid-svg-4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}mermaid-svg-4 .cluster rect{fill:ffffde;stroke:aaaa33;stroke-width:1px;}mermaid-svg-4 .cluster text{fill:333;}mermaid-svg-4 .cluster span{color:333;}mermaid-svg-4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid aaaa33;border-radius:2px;pointer-events:none;z-index:100;}mermaid-svg-4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:333;}mermaid-svg-4 rect.text{fill:none;stroke-width:0;}mermaid-svg-4 .icon-shape,mermaid-svg-4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}mermaid-svg-4 .icon-shape p,mermaid-svg-4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}mermaid-svg-4 .icon-shape rect,mermaid-svg-4 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}mermaid-svg-4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}mermaid-svg-4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}mermaid-svg-4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

读取BeanDefinition

实例化
new 对象

填充属性
@Autowired等

初始化
@PostConstruct等

放入一级缓存
成品Bean

销毁

记住一个关键点实例化 ≠ 初始化。实例化只是调用了构造函数创建了对象(“半成品”),填充属性和初始化才是让Bean真正“完整”的过程。

2.2 为什么这个流程重要?

因为循环依赖问题的核心就在于:A在填充属性时需要B,但B还没创建完成;B在填充属性时又需要A,而A也还没创建完成。Spring的解决思路是:在A实例化之后、填充属性之前,提前把A的“半成品”暴露出去,让B可以先拿到A的引用-2

三、关联概念讲解:三级缓存

3.1 三级缓存的定义

Spring在DefaultSingletonBeanRegistry类中维护了三个Map(缓存),用于存储不同状态的Bean-15

缓存级别缓存名称数据类型作用
一级缓存singletonObjectsMap<String, Object>存放完全初始化完成的单例Bean(成品),业务代码直接使用的Bean
二级缓存earlySingletonObjectsMap<String, Object>存放提前暴露的半成品Bean(已实例化,未填充属性/初始化)
三级缓存singletonFactoriesMap<String, ObjectFactory<?>>存放ObjectFactory对象工厂,仅在调用getObject()时才创建Bean实例

3.2 三级缓存之间的关系

这三个缓存是互斥的——同一个Bean在任意时刻只会存在于其中一个缓存中,不会同时出现在多个缓存里-34

Spring获取Bean时的读取顺序是:一级缓存 → 二级缓存 → 三级缓存-34

java
复制
下载
// 源码来自 DefaultSingletonBeanRegistry
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);      // 一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);            // 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);     // 三级缓存

-15

四、概念关系与区别总结

为了帮助理解和记忆,这里用一句话概括三者关系:

三级缓存“管生产”(存放工厂,按需生成)→ 二级缓存“管过渡”(存放半成品)→ 一级缓存“管成品”(存放最终可用Bean)。

对比维度三级缓存(singletonFactories)二级缓存(earlySingletonObjects)一级缓存(singletonObjects)
存储内容工厂对象(Lambda/函数式接口)Bean实例(半成品)Bean实例(成品)
是否可被业务使用否,需调用getObject()否,仅用于循环依赖中的早期注入,业务代码直接获取
存在阶段实例化后→被消费前三级缓存被消费后初始化完成后
容量默认值1616256

一句话帮助记忆:一级放成品,二级放半成品,三级放“工厂生产线”。

五、代码示例与流程演示

5.1 模拟循环依赖场景

假设有A依赖BB依赖A的经典场景-15

java
复制
下载
@Component
public class A {
    @Autowired
    private B b;
    public void doA() { System.out.println("A do something"); }
}

@Component
public class B {
    @Autowired
    private A a;
    public void doB() { System.out.println("B do something"); }
}

5.2 如果没有三级缓存,会发生什么?

text
复制
下载
实例化A → 发现需要B → 实例化B → 发现需要A → 但A还没创建完 → 死循环 → 抛出异常

5.3 有了三级缓存后的完整流程

下面以A→B→A的场景,逐步拆解Spring的处理过程-15

步骤操作缓存变化
Step 1实例化A(调用构造函数)将A的ObjectFactory放入三级缓存 singletonFactories
Step 2开始给A填充属性,发现需要B去容器中查找B,发现B不存在,开始创建B
Step 3实例化B,将B的ObjectFactory放入三级缓存singletonFactories 中同时存在A和B的工厂
Step 4给B填充属性,发现需要A查找A:一级没有,二级没有,三级有 → 从三级缓存取出A的工厂,调用getObject()生成A的早期引用
Step 5将A的早期引用放入二级缓存 earlySingletonObjects从三级缓存移除A的工厂
Step 6B拿到A的引用后,完成属性填充和初始化将B放入一级缓存 singletonObjects,从二、三级缓存中移除B
Step 7A从“等待”中恢复,继续填充B此时B已经在一级缓存中,A直接拿到完整B
Step 8A完成填充和初始化将A放入一级缓存,从二级缓存中移除A

5.4 新旧实现方式对比

对比维度无三级缓存(传统)有三级缓存(Spring)
构造器注入❌ 启动报错❌ 仍然报错(无法解决)
setter/字段注入❌ 循环依赖异常✅ 正常启动
代理对象一致性不适用✅ 通过三级缓存保证同一代理对象
开发体验需要手动规避对开发者透明,自动处理

对比结论:三级缓存的核心价值不是“让所有循环依赖都能跑通”,而是在setter/字段注入场景下优雅地打破依赖闭环,同时保证AOP代理对象的唯一性。

六、底层原理与技术支撑

6.1 三级缓存的底层依赖

Spring解决循环依赖的底层依赖于以下几个关键设计:

① 实例化与初始化的分离

将Bean的创建过程拆分为“实例化”和“初始化”两个阶段,中间留出“提前暴露”的空间。这是三级缓存机制能起作用的前提-37

② 函数式接口ObjectFactory

三级缓存存储的不是Bean实例,而是ObjectFactory函数式接口。调用getObject()方法时才会真正创建对象。这个设计允许将Lambda表达式作为工厂对象传入,在需要时才执行,实现了“懒创建”的效果-34

java
复制
下载
// 三级缓存中存放的是这样的工厂对象
singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

③ AOP动态代理的支持

三级缓存引入的核心原因之一,就是为了支持AOP场景下的循环依赖。当Bean需要被代理时,三级缓存中的工厂返回的是代理对象而非原始对象,二级缓存则缓存这个代理对象,确保循环依赖中的多方拿到的是同一个代理对象,而不是多次生成的不同对象-63-

6.2 为什么二级缓存不够用?

这是面试中的高频追问。简单来说:

  • 没有AOP时:二级缓存确实足够解决循环依赖。

  • 有AOP时:如果只用二级缓存,需要在实例化时就判断是否需要生成代理对象,这违反了Bean生命周期的设计原则(代理通常在初始化后生成)。且如果循环依赖中有多个Bean需要注入同一个代理对象,没有二级缓存会导致每次取到的地址值不一样-63

三级缓存通过工厂模式延迟代理对象的生成时机,同时用二级缓存保证代理对象的唯一性,是兼顾设计优雅性与功能完备性的精妙方案。

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

以下是Spring循环依赖相关的核心面试题,建议背诵以下答题要点:

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

参考答案(踩分点:一句话概括 + 三级缓存 + 限制条件)

Spring通过三级缓存机制解决单例Bean在setter/字段注入场景下的循环依赖问题-3。具体来说:

  1. Spring维护了三个Map:singletonObjects(一级缓存,放成品)、earlySingletonObjects(二级缓存,放半成品)、singletonFactories(三级缓存,放工厂);

  2. Bean实例化后,会将其工厂对象放入三级缓存,提前暴露;

  3. 当循环依赖发生时,另一方可以从三级缓存获取工厂,生成早期引用后放入二级缓存并完成注入;

  4. 最终双方都完成初始化后进入一级缓存。

局限性:构造器注入、原型Bean、多例Bean的循环依赖无法解决。

Q2:为什么需要三级缓存,二级不行吗?

参考答案(踩分点:AOP场景 + 代理对象唯一性 + 设计原则)

二级缓存在没有AOP的场景下确实足够解决循环依赖,但引入三级缓存主要有两个原因--63

  1. AOP代理支持:如果Bean需要被代理,代理对象的生成时机通常在初始化后。三级缓存中的工厂可以在被调用时才生成代理对象,避免过早创建;

  2. 保证代理对象唯一性:当多个地方注入同一个代理对象时,二级缓存确保大家拿到的是同一个对象(地址值一致),而如果每次从三级缓存工厂重新生成,会得到不同对象,导致运行时错误。

Q3:什么情况下Spring无法解决循环依赖?

参考答案(踩分点:三种不可解场景 + 原因)

Spring无法解决以下三种循环依赖场景-3-2

  1. 构造器注入:构造函数在实例化时就需要依赖,无法“提前暴露”;

  2. 原型Bean(prototype) :Spring不缓存原型Bean,每次都是新对象,无法提前暴露;

  3. 多例Bean:涉及多个实例相互引用时同样无法解决。

Q4:Spring Boot 2.6.x版本之后有什么变化?

参考答案(踩分点:默认禁用 + 开启方式 + 设计理念)

从Spring Boot 2.6.x版本开始,循环依赖默认被禁用。如果项目中出现循环依赖,启动时会直接报错--3。如果需要临时开启,可以在配置文件中添加:

yaml
复制
下载
spring:
  main:
    allow-circular-references: true

但官方推荐的做法是重构代码,因为循环依赖通常意味着代码设计存在问题。

Q5:如何从设计层面彻底避免循环依赖?

参考答案(踩分点:4种重构方案)

虽然Spring提供了技术解决方案,但循环依赖通常是代码设计不合理的信号-3-46。推荐的重构方案:

  1. 提取公共逻辑:将相互依赖的逻辑提取到第三个Service;

  2. 使用接口抽象:让双方都依赖接口而非具体实现;

  3. 使用@Lazy延迟加载:临时方案,在其中一个依赖上添加@Lazy注解;

  4. 事件驱动解耦:使用Spring的ApplicationEvent将直接调用改为事件发布/订阅。

八、结尾总结

核心知识点回顾

知识点一句话总结
循环依赖定义两个或多个Bean相互持有对方引用,形成闭环-13
Spring解决方式三级缓存机制,提前暴露半成品Bean
三级缓存作用一级存成品、二级存半成品、三级存工厂
关键限制构造器注入、原型Bean、多例Bean无法解决
版本变化Spring Boot 2.6.x开始默认禁用循环依赖
设计建议重构优于技术规避,循环依赖通常是设计问题信号

重点强调与易错点

  • ⚠️ 不要误以为Spring能解决所有循环依赖——构造器注入的循环依赖依然会报错,这是面试中最容易混淆的点。

  • ⚠️ 三级缓存的核心价值不是“性能”,而是“AOP支持” ——很多开发者答错这一点。

  • ⚠️ Spring Boot 2.6.x后的默认行为变化——如果面试官问到版本问题,能答出这一点的考生明显更有竞争力。

  • ⚠️ 面试回答要“先讲技术实现,再说限制条件,最后给出设计建议” ——这种结构最能体现思维全面性-3

进阶预告

下一篇文章我们将深入Spring AOP的底层原理,解析JDK动态代理和CGLIB的区别,以及AOP与循环依赖之间的深层关联。敬请期待!


版权声明:本文由AI助手演示生成,内容基于公开技术资料整理,仅供学习交流使用。文中涉及的Spring源码分析基于Spring 5.3.x版本,各版本间可能存在差异,请以实际使用的版本为准。

标签:

相关阅读