Spring循环依赖(Circular Dependency)深度解析:2026年最新版三级缓存原理与面试考点(2026年4月发布)

小编 2 0

Spring循环依赖(Circular Dependency)是Java后端面试中几乎100%会出现的核心考点,也是许多开发者在项目启动时遇到的“死锁式”报错根源。_ai健康助手发现,多数开发者虽然能背出“三级缓存”这四个字,却说不清每一级缓存存的是什么、为什么需要三级、二级缓存够不够、构造器注入为何无法解决。本文将从概念、原理、代码示例到面试要点,带你彻底搞懂Spring循环依赖的前世今生。

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

java
复制
下载
// ❌ 典型循环依赖场景:ServiceA 依赖 ServiceB,ServiceB 依赖 ServiceA
@Service

public class OrderService { @Autowired private UserService userService; // OrderService -> UserService } @Service public class UserService { @Autowired private OrderService orderService; // UserService -> OrderService(形成闭环) }

上面这段代码看似简单,但在Spring Boot 2.6+版本中,默认会禁止循环依赖,启动时直接抛出异常-1。问题出在哪里?

通俗理解:就像两个人去领证,A说“B来了我才签”,B说“A来了我才签”,结果谁也签不了-4。Spring在创建Bean时,同样会陷入这种“鸡生蛋,蛋生鸡”的困境。

传统方案只能靠开发者手动规避,代码耦合度高、维护困难。于是,Spring设计了三级缓存机制,在容器层面自动解决这个问题。

二、核心概念:三级缓存(Three-Level Cache)

2.1 标准定义

三级缓存是Spring IoC容器在创建单例Bean过程中,用于管理不同阶段Bean对象的一组Map集合,核心目的是通过提前暴露半成品Bean的方式打破循环依赖闭环

Spring通过三级缓存机制,可以解决单例Bean在Setter/Field注入场景下的循环依赖问题-1

2.2 生活化类比

三级缓存就像医院的挂号系统

  • 一级缓存:已就诊完成的病人档案(成品)

  • 二级缓存:正在就诊的病人(半成品,正在处理中)

  • 三级缓存:挂号排队单(可随时生成就诊卡)

当医生(B)需要查阅另一个正在就诊病人(A)的资料时,可以从“排队单”生成临时病历先使用,等A看完病再补全。

三、关联概念:三级缓存详细拆解

Spring内部维护了三个Map,各司其职-2

级别缓存名称存储内容作用
一级singletonObjects完全初始化完成的单例Bean供业务直接使用
二级earlySingletonObjects提前暴露的半成品Bean(已实例化,未完成属性填充)存放早期引用,避免重复创建
三级singletonFactoriesObjectFactory(对象工厂)存储Lambda表达式,仅在调用getObject()时才创建Bean实例

概念关系梳理

  • 三级 vs 二级:三级缓存存的是“如何创建对象”的工厂(懒加载),二级缓存存的是“已经创建好的半成品”对象(提前暴露)

  • 思想 vs 实现:三级缓存是一种设计思想(提前暴露引用),三个Map是其具体实现手段

一句话记忆:一级存成品,二级存半成品,三级存工厂;一、二级是“存东西”,三级是“存怎么造东西”。

四、循环依赖解决流程(图文示例)

4.1 场景设定

  • A 依赖 B,B 依赖 A(字段注入)

4.2 完整执行流程

text
复制
下载
1. 创建A:实例化A → 将A的ObjectFactory放入三级缓存
2. 填充A属性:发现需要B → 转去创建B
3. 创建B:实例化B → 将B的ObjectFactory放入三级缓存
4. 填充B属性:发现需要A → 从三级缓存获取A的工厂 → 生成A的早期引用 → 放入二级缓存,移除三级
5. B拿到A的引用后完成初始化 → 放入一级缓存
6. A继续填充属性 → 从一级缓存获取B → 完成初始化 → 放入一级缓存

-1

4.3 关键源码解析(Spring 5.3.x)

Spring处理循环依赖的核心逻辑在DefaultSingletonBeanRegistrygetSingleton方法中-2

java
复制
下载
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 第一步:从一级缓存获取成品Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    
    // 如果一级缓存没有,且当前Bean正在创建中(循环依赖核心判断条件)
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 第二步:从二级缓存获取半成品Bean
        singletonObject = this.earlySingletonObjects.get(beanName);
        
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 双重检查锁
                // 第三步:从三级缓存获取ObjectFactory → 创建早期引用 → 移入二级
            }
        }
    }
    return singletonObject;
}

五、概念关系与区别总结

5.1 三级缓存 vs 二级缓存:为什么需要三级?

很多面试者会问:“二级缓存够用吗?为什么非要三级?”

如果只是为了解决循环依赖,二级缓存确实够用——只要在Bean实例化后,不管是否需要AOP,都直接生成代理对象放到二级缓存,B就能拿到引用-

但这样会有两个严重问题

  1. 性能浪费:如果这个Bean从未参与循环依赖,提前生成代理对象就是多余操作

  2. AOP代理冲突:一个Bean可能有多个代理(事务代理、日志代理等),提前生成哪个?

三级缓存的精妙之处:用ObjectFactory(Lambda表达式)实现懒加载,只有真正需要提前暴露时才生成代理对象,既保证性能又确保AOP正确性。

5.2 构造器注入 vs Setter/字段注入

注入方式是否支持循环依赖原因
构造器注入❌ 不支持实例化时必须传入依赖,无法提前暴露引用
Setter/字段注入✅ 支持实例化后可先暴露半成品,再注入依赖

-43

六、代码示例:从报错到解决

6.1 完整示例项目结构

text
复制
下载
src/main/java/com/example/
├── DemoApplication.java
├── service/
│   ├── OrderService.java
│   └── UserService.java
└── config/
    └── AppConfig.java

6.2 ❌ 错误示例(启动报错)

java
复制
下载
// OrderService.java
@Service
public class OrderService {
    private final UserService userService;  // final字段,构造器注入
    
    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

// UserService.java  
@Service
public class UserService {
    private final OrderService orderService;
    
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

报错信息BeanCurrentlyInCreationException: Requested bean is currently in creation

6.3 ✅ 正确示例(解决方案)

方案一:改用字段注入(Spring Boot 2.6+默认禁止,不推荐)

java
复制
下载
@Service
public class OrderService {
    @Autowired  // 字段注入
    private UserService userService;
}

方案二:使用@Lazy延迟加载(推荐)

java
复制
下载
@Service
public class OrderService {
    private final UserService userService;
    
    public OrderService(@Lazy UserService userService) {  // 加@Lazy
        this.userService = userService;
    }
}

@Lazy会在构造函数参数上注入一个代理对象,真正调用方法时才加载真实Bean,从而打破循环依赖-1

方案三:重构代码消除循环依赖(最佳实践)

提取公共逻辑到第三方服务,从根本上消除相互依赖-1

java
复制
下载
// 提取中介服务
@Service
public class CustomerOrderService {
    @Autowired
    private CustomerService customerService;
    @Autowired
    private OrderService orderService;
    
    public List<Order> getCustomerOrders(Long customerId) {
        Customer customer = customerService.getCustomer(customerId);
        return orderService.getOrdersByCustomer(customerId);
    }
}

七、底层原理与面试进阶要点

7.1 底层技术支撑

三级缓存机制底层依赖以下核心技术:

  • 反射(Reflection):动态创建Bean实例

  • 代理模式(Proxy Pattern):AOP代理对象的生成

  • Lambda表达式ObjectFactory的函数式编程特性

  • ConcurrentHashMap:保证高并发下的线程安全

7.2 Spring版本变化关键提示(面试加分项)

Spring Boot 2.6(Spring Framework 5.3)开始,官方默认禁止循环依赖,鼓励更清晰的代码设计。如果项目中存在循环依赖,启动时会直接报错,需要显式配置spring.main.allow-circular-references=true才能开启-22-30

Spring Boot 3.x(基于Spring Framework 6),默认禁用的策略延续,且问题暴露得更明显-29

7.3 Spring无法解决的循环依赖情况

  1. 构造器注入的循环依赖:实例化阶段就死锁,Spring无能为力

  2. 原型(Prototype)作用域的循环依赖:Spring不缓存原型Bean,无法提前暴露

  3. 多例Bean之间的循环依赖

-22

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

面试题一:Spring是如何解决循环依赖的?请详细说明。

标准答案(踩分点)

  1. 结论先行:Spring通过三级缓存机制解决单例BeanSetter/字段注入场景下的循环依赖。

  2. 三级缓存分别是什么

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

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

    • 三级singletonFactories:存放ObjectFactory工厂

  3. 核心流程:实例化A后放入三级缓存→发现需要B→创建B→实例化B后放入三级缓存→B发现需要A→从三级缓存获取A的工厂生成早期引用放入二级→B完成初始化→A继续初始化。

  4. 为什么需要三级:二级缓存虽能解决循环依赖,但无法处理AOP代理对象的提前暴露问题,三级缓存通过ObjectFactory实现懒加载,按需生成代理。

-2-7

面试题二:为什么构造器注入无法解决循环依赖?

标准答案

构造器注入要求在实例化阶段就传入所有依赖,而循环依赖发生时,两个Bean都处于“正在创建但未完成”的状态,彼此无法获取对方的实例。三级缓存机制依赖“实例化后、属性填充前”这个时间窗口来提前暴露引用,构造器注入恰恰没有这个窗口,因此Spring在启动时就能检测到并抛出异常。

-22

面试题三:使用@Lazy注解解决循环依赖的原理是什么?

标准答案

@Lazy会为依赖的Bean生成一个代理对象注入到当前Bean中,而不是真正的Bean实例。当第一次调用该依赖的方法时,代理对象才会触发真实的Bean加载和初始化。这个机制打破了循环依赖的时间死锁——A不需要B的真实实例,只需要一个“占位符”代理即可完成初始化。

-4

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

标准答案

循环依赖本质上是代码设计问题的信号,而非框架缺陷。Spring官方希望引导开发者写出更清晰、解耦的代码。默认禁止循环依赖后,开发者会被迫审视代码设计,主动通过重构、接口隔离、事件驱动等方式消除不必要的循环依赖,提升代码质量。同时,禁止默认支持也简化了框架的启动逻辑。

-22

面试题五:如果同时使用AOP和循环依赖,Spring如何处理?

标准答案

当Bean需要AOP代理时,三级缓存中的ObjectFactory会在调用getObject()提前生成代理对象而非原始对象。Spring通过getEarlyBeanReference()方法判断是否需要代理,如果需要在三级缓存阶段就生成代理对象放入二级缓存。这样即使发生循环依赖,B获取到的A也是正确的代理对象,而不是原始对象。这正是三级缓存优于二级缓存的关键所在。

--48

九、结尾总结

回顾核心知识点

  • 循环依赖:两个或多个Bean互相持有对方引用,形成闭环

  • 三级缓存singletonObjects(成品)→ earlySingletonObjects(半成品)→ singletonFactories(工厂)

  • 解决范围:✅ Setter/字段注入 | ❌ 构造器注入 | ❌ 原型Bean

  • 版本变化:Spring Boot 2.6+默认禁止,需要显式开启或重构

  • 最佳实践:优先通过重构消除循环依赖,而非依赖框架特性

易错提醒

  1. 不是所有循环依赖Spring都能解决——构造器注入的原型Bean循环依赖会直接报错

  2. @Lazy是治标方案,真正的解决之道是代码重构

  3. Spring Boot 2.6+默认禁止循环依赖,升级项目时需要注意

进阶预告

下一篇我们将深入探讨:Spring AOP底层原理与动态代理的源码实现,敬请期待。


本文基于Spring Framework 5.3.x / 6.x版本编写,发布于2026年4月,数据来源包括官方文档、阿里云开发者社区、CSDN技术博客等公开资料。


📌 系列文章预告

  • 下篇:Spring AOP底层原理深度解析——动态代理的源码之旅

  • 后续:Spring事务管理机制与失效场景全解