用AI文档创作助手拆解Spring循环依赖:从三级缓存到面试通关

小编 产品中心 12

2026年4月10日 北京时间

开篇引入

用AI文档创作助手拆解Spring循环依赖:从三级缓存到面试通关-第1张图片

循环依赖(Circular Dependency)在Spring框架中的地位,堪称面试中最“高频”且最容易“翻车”的知识点之一。简单来说,当Bean A依赖Bean B,而Bean B又反过来依赖Bean A时,便形成了一个“相互等待”的闭环。

不少开发者在日常工作中天天用Spring,却只知其然不知其所以然——遇到项目启动抛出BeanCurrentlyInCreationException时,要么一头雾水,要么盲目添加@Lazy草草了事;到了面试环节,虽然能背出“三级缓存”四个字,却说不清为什么需要三级、二级缓存到底够不够、构造器注入为何无法解决-22

用AI文档创作助手拆解Spring循环依赖:从三级缓存到面试通关-第2张图片

本文将从痛点切入 → 概念拆解 → 关系梳理 → 代码示例 → 底层原理 → 高频面试题这一完整链路,帮你彻底搞懂Spring循环依赖的来龙去脉。无论你是技术入门/进阶学习者、在校学生、面试备考者,还是相关技术栈开发工程师,这篇文章都将兼顾易懂性实用性,让你读完后既能写出正确代码,也能在面试中对答如流。

一、痛点切入:为什么需要三级缓存?

先看一段“会出问题”的代码:

java
复制
下载
// 构造器注入——Spring 无法解决循环依赖
@Service
public class A {
    private final B b;
    public A(B b) { this.b = b; }
}

@Service
public class B {
    private final A a;
    public B(A a) { this.a = a; }
}

启动项目,Spring容器直接抛出:

text
复制
下载
┌─────┐
|  a defined in ...  ↑
└─────┘     ↓
|  b defined in ...
└─────┘
BeanCurrentlyInCreationException

传统/旧有实现方式的痛点分析:

  1. 构造器注入循环依赖:要求在实例化时就提供所有依赖,而循环依赖场景下两个Bean都无法完成实例化,Spring会直接抛出异常-11

  2. 原型(prototype)作用域:Spring不缓存原型Bean,每次请求都是新对象,无法提前暴露引用,因此也无法解决循环依赖-11

  3. 代码设计信号:循环依赖往往是代码设计存在缺陷的信号——模块之间耦合过高、职责边界不清-23

  4. 危害深远:未妥善处理的循环依赖会导致应用启动失败、内存泄漏(部分半成品Bean无法被GC回收)、调试困难、测试障碍等问题-1

正是为了解决上述问题,Spring框架设计者才在DefaultSingletonBeanRegistry类中引入了三级缓存机制——通过提前暴露“半成品Bean”的方式,优雅地打破了依赖闭环-1

二、核心概念讲解:什么是“循环依赖”与“三级缓存”?

2.1 循环依赖

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

场景化类比:就像两个人同时等对方先开口——A说“等B先说话我就说”,B说“等A先说话我就说”,结果谁也没法开口,陷入死锁。

2.2 三级缓存

标准定义:三级缓存是Spring容器内部维护的三个Map集合,用于存储单例Bean在不同创建阶段的状态-6

缓存级别缓存名称存储内容作用
一级缓存singletonObjects完全初始化完成的单例Bean(已注入所有属性、完成初始化方法)对外提供“成品”Bean
二级缓存earlySingletonObjects提前曝光的“半成品”Bean(已实例化,未完成属性注入)存储早期引用,避免重复生成
三级缓存singletonFactoriesObjectFactory对象工厂延迟生成早期引用,支持AOP代理

场景化类比:可以把三级缓存想象成工厂的“流水线跟踪系统”——一级缓存是“成品仓库”(贴好标签可直接发货),二级缓存是“半成品暂存区”(框架搭好了但零件还没装),三级缓存是“模具登记表”(存着每个产品如何被制造出来的配方,需要时才调用)-

三、关联概念讲解:为什么需要三级,两级不够吗?

3.1 核心概念辨析

如果仅仅是为了解决循环依赖,二级缓存其实就够了。只要在Bean实例化(new)之后,不管它需不需要AOP,都直接把它的代理对象生成出来放到二级缓存里,B就可以拿到了-

3.2 第三级缓存的核心价值

那么第三级缓存singletonFactories存在的意义是什么?答案藏在AOP代理中。

如果Bean需要被AOP代理(比如加了@Transactional@Async),代理对象必须在属性注入阶段就得准备好。如果只暴露原始对象,注入给B的是原生A,后面再生成代理就晚了——B拿到的永远不是原始对象而非代理对象-4

三级缓存通过存储ObjectFactory,实现了延迟生成:只有在真正需要早期引用时才调用getObject()去生成代理对象。这正是两级缓存做不到的精妙之处。

3.3 概念关系总结

对比维度一级缓存二级缓存三级缓存
存储内容成品Bean半成品BeanBean工厂
触发时机初始化完成后循环依赖发生时实例化完成后立即放入
核心价值对外提供可用实例缓存早期引用延迟生成代理对象

四、概念关系与区别总结

一句话高度概括:

一级缓存存成品,二级缓存存半成品,三级缓存存“配方”;前两级解决“有没有”,第三级解决“怎么造”。

更精炼的记忆口诀:

一级给外用,二级防重造,三级管代理,三级缺一不可。

五、代码/流程示例演示

5.1 构建循环依赖场景(Setter注入方式)

java
复制
下载
@Component
public class ServiceA {
    private ServiceB b;
    
    @Autowired
    public void setB(ServiceB b) {    // Setter 注入
        this.b = b;
    }
    
    public void doSomething() {
        System.out.println("ServiceA doing something...");
        b.help();
    }
}

@Component
public class ServiceB {
    private ServiceA a;
    
    @Autowired
    public void setA(ServiceA a) {    // Setter 注入
        this.a = a;
    }
    
    public void help() {
        System.out.println("ServiceB helping...");
    }
}

关键注释:✅ 使用@Autowired在Setter方法上,属于Setter注入,Spring可通过三级缓存解决循环依赖-11

5.2 执行流程逐步拆解(以A→B→A为例)

步骤当前创建操作缓存变化
1AgetBean("a")开始创建A
2A实例化new A(),此时A属性全为空
3A将A的ObjectFactory放入三级缓存singletonFactories
4A填充A属性,发现需要B暂停A,去创建B
5B实例化new B()
6B将B的ObjectFactory放入三级缓存singletonFactories
7B填充B属性,发现需要A去容器找A
8B一级缓存无A→二级缓存无A→三级缓存命中A的工厂
9B执行factory.getObject()生成A的早期引用earlySingletonObjects(二级)
10BB拿到A的引用,注入成功,B创建完成singletonObjects(一级)
11A回到A,拿到已完成的B注入成功singletonObjects(一级)

-4

核心洞察:B在创建过程中通过三级缓存拿到了“还在创建中的A”的早期引用,从而打破了循环依赖链条。

六、底层原理/技术支撑点

6.1 底层依赖的核心技术

三级缓存机制底层主要依赖以下几个关键设计:

  1. 反射:Spring通过反射调用构造器完成Bean的实例化,通过反射调用Setter方法完成属性注入。

  2. ObjectFactory函数式接口:这是一个函数式接口,仅在调用getObject()时才会真正创建Bean实例,实现了延迟创建-32

  3. DefaultSingletonBeanRegistry:Spring管理单例Bean的核心类,getSingletonaddSingletonFactoryremoveSingletonFactory是处理循环依赖的关键方法-

6.2 源码关键点(定位版)

核心代码位于DefaultSingletonBeanRegistrygetSingleton方法中:

java
复制
下载
// 大致逻辑(非完整源码,仅供理解流程)
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存拿
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 再从二级缓存拿
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 3. 最后从三级缓存拿ObjectFactory
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();  // 生成早期引用
                this.earlySingletonObjects.put(beanName, singletonObject);  // 放入二级
                this.singletonFactories.remove(beanName);  // 移除三级
            }
        }
    }
    return singletonObject;
}

-32

上述查找顺序 —— 一级 → 二级 → 三级 —— 构成了Spring解决循环依赖的核心逻辑。值得留意的是,从Spring Boot 2.6开始(Spring Framework 5.3),为了鼓励更清晰的设计,默认已禁用循环依赖支持,若项目中存在循环依赖,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-23

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

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

踩分点:三级缓存 + 提前暴露 + 作用域限制

参考答案:Spring通过三级缓存机制解决单例Bean的Setter/字段注入场景下的循环依赖。三个缓存分别是:singletonObjects(一级,存成品)、earlySingletonObjects(二级,存半成品)、singletonFactories(三级,存对象工厂)。当Bean A依赖Bean B时,Spring在实例化A后立即将其ObjectFactory放入三级缓存,然后在填充A属性时发现需要B,便去创建B;B在填充属性时发现需要A,此时从三级缓存拿到A的工厂生成早期引用,B拿到A的引用后完成初始化,A随后也完成初始化-23

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

踩分点:AOP代理 + 延迟生成

参考答案:如果只是为了解决循环依赖,两级缓存确实够了。引入第三级缓存的核心原因是支持AOP代理。当Bean需要被AOP代理(如@Transactional)时,代理对象必须在属性注入阶段就准备好。三级缓存通过存储ObjectFactory,实现了延迟生成——只有在真正需要早期引用时才生成代理对象,而不是在实例化后就立即生成。这样既保证了循环依赖的解决,又避免了对所有Bean提前生成代理的性能开销-4-

面试题3:什么情况下Spring无法解决循环依赖?

踩分点:构造器注入 + 原型Bean + 多例

参考答案:Spring无法解决以下三种循环依赖:①构造器注入——实例化阶段就需要完成依赖注入,没有机会提前暴露引用;②原型作用域(prototype)——Spring不缓存原型Bean,每次请求都是新对象,无法提前暴露;③多例Bean之间的循环依赖——同样无法缓存和提前暴露-11

面试题4:如何从设计层面避免循环依赖?(加分题)

踩分点:重构意识 + 设计模式

参考答案:虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号,我更倾向于从设计层面解决:①提取公共接口或抽象类,让双方都依赖抽象;②使用@Lazy延迟加载作为临时方案;③重新分析类职责,看是否可以拆分成三个类;④使用事件驱动机制(ApplicationEvent)将直接调用改为事件发布/订阅-23

八、结尾总结

本文围绕Spring循环依赖这一核心知识点,完成了以下内容的梳理:

模块核心要点
痛点剖析构造器注入循环依赖会抛出BeanCurrentlyInCreationException,原因是实例化阶段无法提前暴露引用
三级缓存一级存成品、二级存半成品、三级存工厂——核心是第三级的ObjectFactory延迟生成机制
执行流程实例化 → 放三级 → 填充属性发现依赖 → 创建依赖Bean → 从三级取工厂生成早期引用
适用边界✅ Setter/字段注入 + 单例;❌ 构造器注入 + 原型Bean
设计优化优先从设计层面解耦,而非完全依赖框架兜底

重点记忆:循环依赖的解决依赖于三级缓存 + 提前暴露早期引用;第三级缓存的核心价值是支持AOP代理的延迟生成;Spring Boot 2.6+默认禁用循环依赖,需显式开启。

下一篇预告:我们将深入Spring AOP的底层原理,从动态代理到切面执行顺序,带你彻底搞懂声明式事务和日志切面的实现奥秘。

抱歉,评论功能暂时关闭!