小说助手AI带你彻底搞懂Spring循环依赖

北京时间:2026年4月9日

在Java后端开发的面试现场,面试官缓缓问出一句——“Spring如何解决循环依赖?”此时,如果你只能支支吾吾地说出“三级缓存”三个字,却讲不清为什么需要三级、二级够不够、构造器注入为何无法解决,那么这道“送分题”大概率会变成“送命题”。小说助手AI本期带你从零拆解Spring循环依赖,让概念不再模糊,让原理真正落地。

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

假设你在开发一个订单系统,OrderService需要调用UserService查询用户信息,而UserService又需要调用OrderService获取订单列表。这种“你中有我、我中有你”的依赖关系,就是典型的循环依赖(Circular Dependency)。

先看一个错误示例——构造器注入

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

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

启动项目后,你会看到如下报错:

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

为什么会失败? 创建OrderService时需要UserService,创建UserService时又需要OrderService,两个Bean相互等待对方先完成创建,形成死锁,最终抛出BeanCurrentlyInCreationException异常-1

这种问题在日常开发中并不罕见。据2024年Java生态调研报告显示,约23%的Spring应用开发者曾遇到过循环依赖问题,其中字段注入导致的循环依赖占比高达67%-1

二、核心概念:什么是循环依赖?

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

最典型的场景是:Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A。在Spring容器中,这种依赖关系如果不能被正确处理,会导致Bean初始化过程陷入死循环或抛出异常-5

用生活化类比来理解:

想象两个人面对面站在一条窄桥上,甲要过桥必须等乙先让路,乙要过桥必须等甲先让路。两人都站在桥上等待对方先动,结果谁也过不去——这就是循环依赖的“死锁”困境。

循环依赖的三种常见形态:

类型注入方式Spring能否自动解决常见程度
构造器循环依赖构造器参数注入❌ 不能较少见
Setter循环依赖setter方法注入✅ 能较常见
字段循环依赖@Autowired字段注入✅ 能最常见

三、Spring Bean的生命周期:理解循环依赖的前提

要理解Spring如何解决循环依赖,先得搞清楚一个Bean从出生到成品的完整过程。

简化来说,一个单例Bean的创建分为三个阶段-5

  1. 实例化(Instantiation) :通过反射调用构造器创建原始对象。此时的Bean只是一个“空壳”,属性全是null。

  2. 属性注入(Populate) :为@Autowired字段或setter方法注入依赖。

  3. 初始化(Initialization) :执行@PostConstruct方法、InitializingBean接口方法等。

问题恰恰出在第2步:当Bean A在第2步发现需要Bean B时,如果Bean B也依赖Bean A,而两者都卡在第2步无法推进,死锁就此形成。

Spring的破局思路:在第1步实例化完成后,立刻把这个“半成品”Bean提前暴露出去。这样一来,当Bean B需要Bean A时,不需要等待A完全创建完成,而是直接拿到A的早期引用,先完成自己的创建。这个策略,用大白话讲就是:先把“半成品的Bean”暴露出去,虽然属性还没填完,但对象地址已经有了,先拿去用,等后面再慢慢完善-

四、三级缓存:Spring的精妙设计

Spring通过三级缓存(Three-Level Cache) 机制来实现上述策略。这三个缓存位于DefaultSingletonBeanRegistry类中,是三个Map对象-7

缓存级别缓存名称存储内容作用
一级缓存singletonObjects完全初始化的成品Bean供业务直接使用
二级缓存earlySingletonObjects提前暴露的半成品Bean存储已实例化但未初始化的Bean引用
三级缓存singletonFactoriesObjectFactory对象工厂按需生成半成品Bean,支持AOP代理的延迟创建

三者关系一句话概括:三级缓存存工厂 → 工厂生成对象 → 对象升级到二级缓存 → 完整初始化后放入一级缓存。

为什么需要三级缓存?(面试高频考点)

一个常见的面试追问是:“解决循环依赖,二级缓存就够了,为什么非要三级?”

答案的核心:如果仅仅是为了解决循环依赖,二级缓存确实够用。只要在Bean实例化后,不管它需不需要AOP,都直接把它的代理对象生成出来放到二级缓存里,另一个Bean就可以拿到了-29

但这样做的代价是:提前生成所有Bean的代理对象,包括那些根本不需要AOP的Bean,会造成不必要的性能开销。

三级缓存的设计巧妙之处在于——延迟生成代理对象ObjectFactory是一个函数式接口,只有在真正需要(即循环依赖被触发)时,才会调用getObject()生成代理对象。如果没有循环依赖,三级缓存中的工厂对象永远不会被调用,Bean会按正常流程完成初始化后才生成代理-

一句话总结:二级缓存解决的是“循环依赖能不能解”的问题,三级缓存解决的是“循环依赖怎么解才优雅(支持AOP代理且性能最优)”的问题。

五、完整流程演示:从代码到原理

5.1 可运行的代码示例

下面是一个字段注入(@Autowired)的循环依赖示例,Spring可以自动解决:

java
复制
下载
@Service
public class OrderService {
    @Autowired
    private UserService userService;
    
    public void createOrder() {
        System.out.println("创建订单,关联用户:" + userService.getUserName());
    }
}

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    
    public String getUserName() {
        orderService.createOrder();  // 调用OrderService的方法
        return "张三";
    }
}

5.2 执行流程解析

假设OrderService先被创建,Spring通过三级缓存解决循环依赖的完整流程如下-2

步骤当前操作一级缓存二级缓存三级缓存说明
实例化OrderServiceOrderService的ObjectFactory构造完成后,将工厂对象放入三级缓存
填充OrderService属性,发现需要UserServiceOrderService的ObjectFactory转而去创建UserService
实例化UserServiceOrderService的ObjectFactory + UserService的ObjectFactoryUserService构造完成后放入三级缓存
填充UserService属性,发现需要OrderService同上开始寻找OrderService
从三级缓存获取OrderService的ObjectFactory,调用生成早期引用OrderService半成品同上将早期引用升级到二级缓存
UserService获得OrderService引用,完成初始化UserService成品OrderService半成品注入成功,UserService初始化完成
返回OrderService继续填充属性UserService成品OrderService半成品OrderService获得UserService成品
OrderService完成初始化OrderService成品 + UserService成品成品移入一级缓存,清理临时缓存

5.3 核心源码定位

Spring解决循环依赖的核心逻辑集中在DefaultSingletonBeanRegistrygetSingleton()方法中,关键判断是“一级缓存没有且当前Bean正在创建中”-7。当满足这个条件时,Spring会依次从二级缓存和三级缓存中查找早期引用。这部分源码建议结合Spring源码边debug边理解效果更佳-

六、底层原理:AOP代理与三级缓存

Spring的AOP(Aspect Oriented Programming,面向切面编程)功能,如@Transactional事务注解,底层通过动态代理实现。当一个Bean需要被代理时,Spring不能直接把原始对象暴露出去——暴露出去的必须是代理对象,否则事务等功能会失效。

三级缓存正是为此而设计:ObjectFactory在调用getObject()时,会检查当前Bean是否需要AOP代理,如果需要则返回代理对象,否则返回原始对象。这样既保证了循环依赖能够被解决,又保证了代理对象的正确性。

七、不能解决的情况(面试必问)

Spring的三级缓存并非万能,以下情况无法自动解决循环依赖-2

场景原因解决方案
构造器注入实例化阶段就需要依赖,此时Bean尚未放入任何缓存改用字段注入或setter注入
原型作用域(Prototype)每次请求创建新实例,无法提前暴露引用避免在设计中使用原型Bean的循环依赖
AOP代理对象异常代理对象的创建时机可能导致循环依赖失败使用@Lazy延迟加载

为什么构造器注入无法解决?

构造器注入要求在实例化时就提供所有依赖,而实例化发生在第1步。此时Bean还没有机会放入三级缓存,两个Bean相互等待对方先实例化,形成死锁-4

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

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

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

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

  • 二级缓存earlySingletonObjects :存放提前暴露的半成品Bean(已实例化但未初始化)。

  • 三级缓存singletonFactories :存放ObjectFactory工厂对象,支持AOP代理的延迟创建。

核心思路:在Bean实例化完成后,立即将其工厂对象放入三级缓存,提前暴露引用。当另一个Bean依赖它时,从三级缓存获取工厂并生成早期引用,从而打破依赖闭环。

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

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

参考答案:单纯解决循环依赖,二级缓存确实够用。但Spring需要兼顾AOP代理对象的正确生成性能优化。如果只用二级缓存,必须在实例化后立即生成代理对象(不管是否需要),会造成不必要的开销。三级缓存通过ObjectFactory延迟生成代理对象,只在真正发生循环依赖时才触发生成,既解决了循环依赖,又保证了AOP的正确性和性能。

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

参考答案:三种情况:

  1. 构造器注入的循环依赖:实例化阶段就需要依赖,Bean尚未放入缓存。

  2. 原型作用域的循环依赖:每次创建新实例,无法缓存早期引用。

  3. AOP代理对象的循环依赖:代理对象生成时机特殊,可能导致失败。

面试题4:从Spring Boot 2.6开始,循环依赖默认还能自动解决吗?

参考答案:不能。从Spring Boot 2.6(Spring Framework 5.3)开始,为了鼓励更清晰的代码设计,默认禁用了循环依赖的自动解决。如果项目中存在循环依赖,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-28

九、最佳实践与避坑指南

✅ 推荐做法

  1. 优先使用构造器注入:构造器注入本身就是一种“无法循环依赖”的设计,能迫使你写出更清晰的代码结构。

  2. 重构双向依赖:将A和B的共同逻辑提取到第三个类C中,让A和B都依赖C,从根本上消除循环依赖。

  3. 使用@Lazy延迟加载:在其中一个依赖上添加@Lazy,让Bean在首次使用时才初始化。

❌ 避坑提示

  1. 不要依赖Spring的循环依赖自动解决机制——它是“急救方案”,而非“设计规范”。

  2. 字段注入虽然方便,但会隐藏设计问题,且不利于单元测试。

  3. 从Spring Boot 2.6开始,新项目中如果出现循环依赖会直接启动失败,不要抱有侥幸心理。

十、总结

回顾本文核心知识点:

知识点核心结论
什么是循环依赖两个或多个Bean相互引用形成闭环
Spring如何解决三级缓存机制 + 提前暴露半成品Bean
三级缓存各司何职一级存成品、二级存半成品、三级存工厂
为什么需要三级支持AOP代理的延迟生成,兼顾性能与正确性
不能解决的情况构造器注入、原型作用域
版本变化Spring Boot 2.6+默认禁用,需显式开启

理解Spring循环依赖,不仅是应对面试的需要,更是深入理解IoC容器设计哲学的必经之路。下一期,小说助手AI将带你深入Spring AOP的底层原理,聊聊动态代理和切面执行的细节,敬请期待。