Spring循环依赖详解

Spring 循环依赖详解(全面原理 + 实战解决方案)

循环依赖(Circular Dependency)是 Spring 开发者经常遇到的问题,指两个或多个 Bean 互相引用对方,形成闭环(如 A 依赖 B,B 依赖 A)。Spring 不会一上来就报错,只有无法自动解决时才会抛出 BeanCurrentlyInCreationException

本文从定义、检测机制、三级缓存原理、失败场景、解决方案到 Spring Boot 配置,一次讲透。

1. Spring 如何检测循环依赖

Spring 使用一个 Set<String> 集合 singletonsCurrentlyInCreation 来记录正在创建中的单例 Bean

当开始创建 Bean A 时:

  • 把 A 加入该集合
  • 如果后续创建 B 时发现 B 已经在集合里 → 检测到循环依赖

这是最基础的检测手段,放在 DefaultSingletonBeanRegistry 中。

2. Spring 如何解决循环依赖 —— 三级缓存机制(核心!)

Spring 仅支持单例 Bean 的属性注入(setter / @Autowired 字段注入) 的循环依赖,使用三级缓存提前暴露“半成品” Bean。

三级缓存定义(DefaultSingletonBeanRegistry):

// 一级缓存:完全初始化好的 Bean(最终使用)
private final Map&lt;String, Object> singletonObjects = new ConcurrentHashMap&lt;>(256);

// 二级缓存:已实例化但未完成属性注入和初始化的“早期” Bean
private final Map&lt;String, Object> earlySingletonObjects = new ConcurrentHashMap&lt;>(16);

// 三级缓存:ObjectFactory,用于延迟生成早期引用(关键!)
private final Map&lt;String, ObjectFactory&lt;?>> singletonFactories = new HashMap&lt;>(16);

查找顺序(getSingleton 方法):

  1. 一级缓存 → 命中直接返回
  2. 二级缓存 → 命中返回(早期引用)
  3. 三级缓存 → 调用工厂生成早期 Bean → 放入二级缓存 + 从三级移除

完整创建流程(以 A → B → A 为例):

  1. getBean(A) → A 不在任何缓存,开始 doCreateBean
  2. 实例化 A(createBeanInstance)—— 只调用构造器,此时 A 是“裸对象”
  3. 判断允许循环引用(allowCircularReferences)且是单例 → 把 A 包装成 ObjectFactory 放入三级缓存
   addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  1. 属性注入populateBean)→ 需要 B → getBean(B)
  2. 创建 B:
  • 实例化 B → 放入三级缓存
  • 属性注入时需要 A → 调用 getSingleton(A, false)
  • 查三级缓存 → 执行 ObjectFactory → 得到 A 的早期引用(放入二级缓存)
  1. B 初始化完成 → 放入一级缓存
  2. 回到 A 继续初始化 → 最终 A 也放入一级缓存

整个过程的关键:A 在实例化后、初始化前就被“提前暴露”了,所以 B 能拿到 A 的引用,打破死循环。

为什么必须是三级缓存?(不是二级就够)

因为 Spring AOP!
如果 A 需要被代理(@Aspect、事务等),最终 Bean 必须是代理对象。
三级缓存存的是工厂getEarlyBeanReference 会走 SmartInstantiationAwareBeanPostProcessor 生成代理),二级缓存存的是已生成的代理对象
如果只有二级缓存,就无法在 A 未初始化时提供正确的代理对象。
结论:三级缓存是为了同时支持循环依赖 + AOP。

3. Spring 无法自动解决的场景

Spring 只能解决单例 + 属性注入 的循环依赖,以下情况会直接报错:

场景是否可解决原因
构造器注入循环(A 构造器注 B,B 构造器注 A)❌ 否实例化阶段就必须拿到依赖,无法提前暴露
A 用构造器注入 B,B 用 setter 注入 A⚠️ 部分取决于顺序
Prototype(原型)Bean 循环❌ 否每次都新创建,无法缓存
多 Bean 复杂循环 + AOP 代理冲突❌ 可能失败代理时机问题

构造器注入是最大“雷区”,因为 Bean 还没出生就要求依赖已存在。

4. 实战解决方案(推荐顺序)

最佳方案(强烈推荐)

  1. 重构设计(根本解决)
  • 拆分职责、使用接口 + 事件/观察者模式、中介者模式
  • 让依赖单向流动
  1. 使用 Setter / 字段注入(最简单)
   @Component
   public class A {
       @Autowired
       private B b;          // 或 setter 方法
   }
  1. @Lazy 注解(延迟加载,推荐)
   @Component
   public class A {
       @Autowired
       @Lazy
       private B b;
   }

Spring 会注入一个代理对象,真正使用时才初始化 B。

  1. @PostConstruct + 手动注入
   @Component
   public class A {
       @Autowired
       private B b;

       @PostConstruct
       public void init() {
           b.setA(this);   // B 有 setter
       }
   }
  1. 实现 ApplicationContextAware + InitializingBean(老项目兼容)

5. Spring Boot 特殊配置(2.6+ 重要!)

Spring Boot 2.6 开始,默认禁止循环依赖(为了尽早暴露设计问题)。

application.properties / application.yml 中开启(仅临时使用):

spring.main.allow-circular-references=true

或 yml:

spring:
  main:
    allow-circular-references: true

不推荐长期使用!新项目应该彻底重构消除循环依赖。

6. 最佳实践总结

  • 优先构造器注入(除非有循环依赖)
  • 有循环依赖时立即考虑重构,不要依赖框架“魔法”
  • 复杂项目可结合 ArchUnit 等架构测试工具在 CI 中拦截循环依赖
  • 生产环境建议关闭 allow-circular-references,强制修复

掌握了三级缓存原理,你就能轻松定位“BeanCurrentlyInCreationException”到底是哪里出的问题,也能在面试中自信回答“Spring 如何解决循环依赖”。

需要具体代码 Demo(A、B 互相注入)或源码调试步骤,随时补充问我!

文章已创建 5160

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部