好的,天天ai助手已经帮你完成搜索和资料整理。以下是基于你要求的框架结构撰写的技术文章,开篇已自然植入核心关键词。

小编头像

小编

管理员

发布于:2026年05月05日

1 阅读 · 0 评论


天天ai助手深度解析:Spring三级缓存如何解决循环依赖?(2026.04.09)

开篇引入

在Spring框架的IoC体系中,Bean循环依赖(Circular Dependency)是每一位开发者绕不开的高频知识点,也是面试中必问的核心考点。很多学习者面临这样的困境:

日常用着@Autowired没问题,但一旦被问到“Spring底层怎么解决的”,就答不上来;或者知道“三级缓存”这个关键词,却说不出每一级分别存什么、为什么一定要三级而不是两级。 本文由天天ai助手基于最新技术资料(截至2026年4月9日)整理,将从问题痛点入手,逐层剖析三级缓存的概念、协作逻辑和底层原理,配合代码示例和高频面试题,帮你建立完整的知识链路。

一、痛点切入:没有三级缓存时,循环依赖会出什么问题?

传统方式的困境

假设我们有两个Service相互依赖:

java
复制
下载
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;    // A依赖B
    public void doA() { serviceB.doB(); }
}

@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;    // B依赖A,形成循环
    public void doB() { serviceA.doSomething(); }
}

如果不做任何特殊处理,Spring在启动时会在填充ServiceA的serviceB属性时发现需要ServiceB,于是去创建ServiceB;而在创建ServiceB时又发现需要ServiceA。此时ServiceA还没创建完成,两个Bean相互等待,最终抛出BeanCurrentlyInCreationException异常,应用启动失败-1

这个设计困境的本质是什么?

Spring Bean的创建遵循三步曲:实例化 → 属性填充 → 初始化。循环依赖的本质是:Bean A在“属性填充”阶段需要Bean B,但Bean B此时正处于“实例化”阶段,尚未完成创建,导致相互等待-32

想象一下这个场景:你要去办理身份证(Bean A),但办身份证需要先有户口本(Bean B);而你去办户口本时,工作人员说需要先有身份证(Bean A)才能办理——两个证件的办理相互依赖,最终陷入死循环。

如果没有三级缓存机制, 这种场景只能靠人工重构代码来解决(比如提取中间Bean、使用@Lazy等),但这恰恰说明:Spring的三级缓存正是为了解决这个“鸡蛋相生”问题而设计的精妙机制。

二、核心概念:什么是三级缓存?

概念A:三级缓存(Three-level Cache)

标准定义:三级缓存是Spring在DefaultSingletonBeanRegistry中维护的三个Map集合,分别存放不同状态的Bean实例,通过协同工作解决单例Bean的循环依赖问题-1

Spring用三个Map分别存放不同阶段的Bean:

级别缓存名称作用
一级缓存singletonObjects存放完全初始化完成的单例Bean(成品对象),供业务直接使用
二级缓存earlySingletonObjects存放提前暴露的半成品Bean(已实例化但未完成属性填充和初始化)
三级缓存singletonFactories存放ObjectFactory(对象工厂),仅在调用getObject()时才会创建Bean实例

一句话记住:一级存成品,二级存半成品,三级存“将来可能生成代理”的工厂。

概念B:ObjectFactory(对象工厂)

标准定义ObjectFactory是Spring定义的一个函数式接口,核心方法为getObject(),用于在需要时才真正创建或获取Bean实例。三级缓存存储的正是ObjectFactory<?>对象-1-4

ObjectFactory是一个lambda表达式或匿名内部类,例如:

java
复制
下载
() -> getEarlyBeanReference(beanName, mbd, bean)

这个工厂的作用是:在真正需要暴露早期引用时,才动态决定——是否要加AOP代理。如果Bean没有切面,就返回原始Bean;如果有@Transactional@Async,就在这里生成代理对象。这样既避免了提前代理带来的副作用,又保证了最终暴露出去的对象和最终单例一致-4

三、概念关系与区别:三级缓存如何协同工作?

三者的逻辑关系

对比维度一级缓存(成品)二级缓存(半成品)三级缓存(工厂)
存储内容完整Bean实例提前暴露的Bean引用ObjectFactory工厂
何时存入Bean初始化完成后发生循环依赖时实例化完成后立即存入
何时取出每次getBean优先查循环依赖时取早期引用第一次被引用时调用getObject
作用定位结果存储缓存中转按需创建

一句话高度概括

一级缓存存最终成品,二级缓存存早期引用,三级缓存存工厂用于按需生成代理对象——三级缓存的核心职责是:把“要不要代理”的判断延迟到真正被其他Bean引用时才执行-4

为什么一定要三级?两级不行吗?

这是一个高频面试追问点。简单来说:

  • 如果只有一级+二级,在实例化Bean后就要立即决定是否创建代理对象并放入二级缓存。

  • 但此时Bean还没有走到初始化阶段,无法判断是否需要AOP增强(比如@PostConstruct方法里才加标记)。

  • 三级缓存把“要不要代理”延迟到第一次被其他Bean引用时才计算,既按需代理,又不破坏Bean的生命周期-4

对于普通Bean(无AOP),两级缓存确实够用;但对于有AOP代理的Bean,三级缓存才是精髓所在-21

四、代码示例:完整流程演示

场景:ServiceA依赖ServiceB,ServiceB依赖ServiceA(字段注入)

假设ServiceA和ServiceB相互依赖,Spring的创建流程如下:

Step 1:创建ServiceA

java
复制
下载
// 1. 实例化ServiceA(调用构造器,生成原始对象)
Object aInstance = new ServiceA();

// 2. 将ServiceA的ObjectFactory放入三级缓存
singletonFactories.put("serviceA", () -> getEarlyBeanReference("serviceA", mbd, aInstance));

Step 2:填充ServiceA属性,发现需要ServiceB

java
复制
下载
// 3. 开始创建ServiceB
// 实例化ServiceB,将其ObjectFactory放入三级缓存
singletonFactories.put("serviceB", () -> getEarlyBeanReference("serviceB", mbd, bInstance));

Step 3:填充ServiceB属性,发现需要ServiceA

java
复制
下载
// 4. 此时发生循环依赖!Spring执行getSingleton("serviceA", true)
// 一级缓存:没有 → 二级缓存:没有 → 三级缓存:有!
ObjectFactory<?> factory = singletonFactories.get("serviceA");
Object earlyA = factory.getObject();  // 可能返回原始对象或代理对象
earlySingletonObjects.put("serviceA", earlyA);  // 升级到二级缓存
singletonFactories.remove("serviceA");          // 移除三级缓存
// 5. 将earlyA注入到ServiceB中,ServiceB完成属性填充和初始化
// 6. ServiceB放入一级缓存,移除二级缓存中的ServiceB

Step 4:ServiceA继续完成初始化

java
复制
下载
// 7. 从一级缓存获取ServiceB,注入到ServiceA
// 8. ServiceA完成属性填充和初始化,放入一级缓存

整个流程可以用一句话总结:先实例化后暴露工厂,遇循环依赖时从工厂拿引用,最后完成初始化存入成品池-1

五、底层原理与依赖支撑

三级缓存机制能够生效,底层依赖两个关键知识点:

1. Java的引用传递特性

Java中对象的引用是指针传递。当Bean A实例化完成后,它已经在堆内存中占据了一个地址。虽然此时它的属性(如serviceB)还是null,但这个对象的“地址”已经存在了。Spring正是利用了这一点:在属性填充之前就把对象的引用暴露出去,让其他Bean先用上,等引用方完成后,再回过头来填充原对象的属性-20

2. 动态代理(JDK Proxy / CGLIB)

AOP代理(如@Transactional@Async)是三级缓存存在的核心原因。在三级缓存的ObjectFactory中,通过getEarlyBeanReference方法判断Bean是否需要代理,如果需要,就在这里生成代理对象返回。这背后依赖的是Spring AOP的后置处理器机制,在Bean创建的生命周期中插入代理创建逻辑-4

这两个底层知识点支撑了三级缓存的核心功能,后续的进阶文章可以深入探讨。

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

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

参考答案(分层回答,面试官最想听的完整答案)

Spring通过三级缓存机制解决单例Bean的Setter注入和字段注入场景下的循环依赖问题。三级缓存分别是:

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

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

  • 三级缓存(singletonFactories) :存放ObjectFactory工厂,用于按需生成代理对象。

核心思路是提前暴露正在创建中的Bean的引用。当Bean A依赖Bean B、Bean B又依赖Bean A时,Spring在实例化Bean A后,将它的ObjectFactory放入三级缓存,然后开始填充属性。发现需要Bean B时,去创建Bean B,Bean B实例化后同样将工厂放入三级缓存。当Bean B需要Bean A时,从三级缓存拿到A的工厂,生成早期引用放入二级缓存,完成B的初始化。最后A拿到B的完整实例,完成自己的初始化-31

但需要注意:构造器注入和原型Bean的循环依赖无法解决,Spring会直接抛出异常。另外,Spring Boot 2.6+版本默认已禁止循环依赖,需要通过spring.main.allow-circular-references=true显式开启-31-13


Q2:为什么一定要三级缓存?两级不行吗?

参考答案

对于普通Bean(无AOP),两级缓存确实可以解决循环依赖。但Spring需要同时支持AOP代理场景

如果只用两级缓存,就需要在Bean实例化后立即决定是否创建代理对象并放入二级缓存。但此时Bean还没有走到初始化阶段,无法判断是否需要AOP增强(比如@PostConstruct方法里才加标记)。

三级缓存通过存储ObjectFactory,将“要不要代理”的判断延迟到第一次被其他Bean引用时才执行。这样既保证了AOP代理能按需生成,又不破坏Bean的生命周期。三级缓存的存在,本质是为了兼顾“循环依赖解决”和“AOP代理按需生成”两个需求-4-21


Q3:哪些情况下Spring无法解决循环依赖?

参考答案(列举完整,展示全面性)

Spring无法解决以下三种循环依赖:

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

  2. 原型作用域(Prototype)循环依赖:Spring不缓存原型Bean,每次都是新对象,无法提前暴露引用-13

  3. 多例循环依赖:涉及到多个Bean实例相互引用时,Spring也处理不了。

注意:Spring Boot 2.6+默认已禁止循环依赖,即使使用Setter/Field注入,也需要显式配置spring.main.allow-circular-references=true才能启动-31


Q4:三级缓存之间是如何协同工作的?

参考答案

以A依赖B、B依赖A为例,协同流程如下:

  1. 创建A:实例化A → 将A的ObjectFactory放入三级缓存 → 开始填充属性,发现需要B

  2. 创建B:实例化B → 将B的ObjectFactory放入三级缓存 → 开始填充属性,发现需要A

  3. 命中循环:执行getSingleton("A"),一级没有、二级没有、三级有 → 调用工厂获取A的早期引用 → 放入二级缓存 → 移除三级缓存 → 完成B的初始化并放入一级缓存

  4. 完成A:回到A的属性填充,从一级缓存拿到B → 完成A的初始化并放入一级缓存

从源码角度看,核心逻辑在DefaultSingletonBeanRegistry.getSingleton()方法中:先从一级缓存找 → 没有且Bean正在创建中 → 再从二级缓存找 → 没有且允许提前引用 → 最后从三级缓存取工厂并创建早期引用-1


Q5:如何通过配置或重构解决循环依赖?(实践题)

参考答案

如果项目中存在循环依赖,有几种解决方案:

  1. 临时兼容方案:在application.properties中添加spring.main.allow-circular-references=true,恢复Spring旧版的三级缓存处理逻辑-12

  2. 推荐方案:重构代码消除循环依赖。将相互依赖的部分提取到第三个Bean中,或使用接口分离职责。这是最彻底的解决方案-

  3. 使用@Lazy延迟加载:在循环依赖的一侧添加@Lazy注解,Spring会注入一个代理对象,首次调用时才真正初始化,从而打破循环-40

  4. Setter注入替代构造器注入:Spring可以处理Setter注入和字段注入的循环依赖,但无法处理构造器注入的循环依赖-40

注意:虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号,实际项目中应优先考虑设计层面的优化-31

七、结尾总结

核心知识点回顾

知识点核心要点
三级缓存职责一级存成品、二级存半成品、三级存工厂
为什么是三级为了兼顾循环依赖解决和AOP代理按需生成
能解决的场景单例 + Setter/字段注入
不能解决的场景构造器注入、原型Bean
底层依赖Java引用传递 + 动态代理
Spring Boot 2.6+变化默认禁止循环依赖,需显式开启

重点强调与易错点

重中之重:三级缓存的核心不是“三级”这个数字,而是每一级的职责和协作逻辑。一级存成品,二级存已确定的早期引用,三级存“将来可能生成代理”的工厂——逻辑清晰了,答案自然就对了。

易错点提醒

  • 错误理解:三级缓存解决了所有循环依赖。→ ✅ 正确理解:只解决单例 + Setter/字段注入的循环依赖,构造器注入和原型Bean无法解决。

  • 错误理解:三级缓存是为了性能优化。→ ✅ 正确理解:三级缓存是为了解决循环依赖问题,而不是性能优化,虽然它确实通过缓存提升了效率-

预告下一篇内容

下一篇我们将深入探讨 Spring AOP的底层原理,从动态代理(JDK Proxy vs CGLIB)到@Transactional的事务传播行为,结合源码和实战案例,帮你彻底打通Spring核心知识链路。


本文由天天ai助手基于最新技术资料整理发布,数据截止2026年4月9日。

标签:

相关阅读