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<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:已实例化但未完成属性注入和初始化的“早期” Bean
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:ObjectFactory,用于延迟生成早期引用(关键!)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
查找顺序(getSingleton 方法):
- 一级缓存 → 命中直接返回
- 二级缓存 → 命中返回(早期引用)
- 三级缓存 → 调用工厂生成早期 Bean → 放入二级缓存 + 从三级移除
完整创建流程(以 A → B → A 为例):
getBean(A)→ A 不在任何缓存,开始doCreateBean- 实例化 A(
createBeanInstance)—— 只调用构造器,此时 A 是“裸对象” - 判断允许循环引用(
allowCircularReferences)且是单例 → 把 A 包装成ObjectFactory放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
- 属性注入(
populateBean)→ 需要 B →getBean(B) - 创建 B:
- 实例化 B → 放入三级缓存
- 属性注入时需要 A → 调用
getSingleton(A, false) - 查三级缓存 → 执行 ObjectFactory → 得到 A 的早期引用(放入二级缓存)
- B 初始化完成 → 放入一级缓存
- 回到 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. 实战解决方案(推荐顺序)
最佳方案(强烈推荐):
- 重构设计(根本解决)
- 拆分职责、使用接口 + 事件/观察者模式、中介者模式
- 让依赖单向流动
- 使用 Setter / 字段注入(最简单)
@Component
public class A {
@Autowired
private B b; // 或 setter 方法
}
- @Lazy 注解(延迟加载,推荐)
@Component
public class A {
@Autowired
@Lazy
private B b;
}
Spring 会注入一个代理对象,真正使用时才初始化 B。
- @PostConstruct + 手动注入
@Component
public class A {
@Autowired
private B b;
@PostConstruct
public void init() {
b.setA(this); // B 有 setter
}
}
- 实现 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 互相注入)或源码调试步骤,随时补充问我!