循环依赖
问题
什么是循环依赖?Spring 是如何通过三级缓存解决循环依赖的?哪些场景的循环依赖无法解决?
答案
什么是循环依赖?
A 依赖 B,B 又依赖 A,形成闭环:
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // A 依赖 B
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // B 依赖 A
}
如果不做处理,创建 A 时需要 B,创建 B 时需要 A,无限循环。
三级缓存
Spring 通过三级缓存解决 singleton + 属性注入的循环依赖:
// 一级缓存:存放完全初始化好的 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存:存放提前暴露的 Bean(半成品,已实例化但未填充属性)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
// 三级缓存:存放 Bean 的 ObjectFactory(工厂,用于生成早期引用)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
| 缓存 | 名称 | 存储内容 | 何时放入 |
|---|---|---|---|
| 一级 | singletonObjects | 完整的 Bean | 初始化完成后 |
| 二级 | earlySingletonObjects | 提前暴露的 Bean(可能是代理) | 发生循环依赖时从三级缓存晋升 |
| 三级 | singletonFactories | ObjectFactory(Bean 工厂) | 实例化后、属性填充前 |
解决循环依赖的流程
以 A 依赖 B、B 依赖 A 为例:
为什么需要三级缓存?两级不行吗?
关键在于 AOP 代理。
如果没有 AOP,两级缓存就够了(一级放成品,二级放半成品)。但如果 Bean 需要 AOP 代理:
- 正常情况下,代理是在 BeanPostProcessor 后置处理 阶段创建的
- 发生循环依赖时,需要提前创建代理对象给依赖方使用
三级缓存的 ObjectFactory 可以实现:
- 延迟判断:只有发生循环依赖时才调用 ObjectFactory 决定是否创建代理
- 保证单一代理:通过二级缓存保证同一个 Bean 的早期引用只被创建一次
// addSingletonFactory() 中的 lambda:
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// getEarlyBeanReference():
// 如果需要 AOP → 返回代理对象
// 如果不需要 AOP → 返回原始对象
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessors()) {
// AbstractAutoProxyCreator 在这里判断是否需要代理
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
return exposedObject;
}
无法解决的循环依赖
1. 构造器注入的循环依赖
@Service
public class A {
public A(B b) { } // 构造器注入 B
}
@Service
public class B {
public B(A a) { } // 构造器注入 A
}
// ❌ 抛出 BeanCurrentlyInCreationException
// 原因:实例化阶段就需要依赖,此时还没有机会放入三级缓存
解决方案:打破循环(重构)、使用 @Lazy 延迟加载。
2. prototype 作用域的循环依赖
prototype Bean 不使用缓存,每次都创建新实例,无法打破循环。
3. @Async + 循环依赖
@Async 代理创建时机特殊,可能导致循环依赖问题。
@Lazy 解决构造器循环依赖
@Service
public class A {
public A(@Lazy B b) { // @Lazy:注入 B 的代理,延迟加载
this.b = b; // 此时 b 是一个代理对象,并非真正的 B
}
}
@Lazy 注入的是一个代理对象,实际使用时才触发真正的 Bean 创建。
Spring Boot 2.6+ 默认禁止循环依赖
Spring Boot 2.6 起默认禁止循环依赖,启动时检测到会直接报错。如果确实需要,可以配置允许:
spring.main.allow-circular-references=true
循环依赖通常是设计问题的信号,推荐的解决方式:
- 重构:提取公共逻辑到第三个类
- 使用事件:用 ApplicationEvent 解耦
- 延迟注入:
@Lazy或ObjectProvider<T>
常见面试问题
Q1: Spring 如何解决循环依赖?
答案:
通过三级缓存:
- 一级缓存(singletonObjects):完整的 Bean
- 二级缓存(earlySingletonObjects):提前暴露的早期引用
- 三级缓存(singletonFactories):Bean 的 ObjectFactory
流程:A 实例化后将 ObjectFactory 放入三级缓存 → 填充属性发现依赖 B → 创建 B → B 填充属性发现依赖 A → 从三级缓存获取 A 的早期引用 → B 创建完成 → 回到 A 完成创建。
Q2: 为什么需要三级缓存而不是两级?
答案:
为了处理 AOP 代理。三级缓存存放 ObjectFactory,在发生循环依赖时才调用它来决定返回原始对象还是代理对象。这样保证了:
- 没有循环依赖时,代理创建时机不被提前
- 有循环依赖时,代理只创建一次(通过二级缓存保证)
如果没有 AOP 需求,两级缓存确实够用。
Q3: 构造器注入的循环依赖能解决吗?
答案:
Spring 无法自动解决。因为构造器注入在实例化阶段就需要依赖,而三级缓存是在实例化之后才写入的。
解决方案:
@Lazy注入代理对象,延迟加载- 重构代码消除循环依赖
- 改为 setter 注入(不推荐,治标不治本)
Q4: 为什么 Spring Boot 2.6+ 默认禁止循环依赖?
答案:
循环依赖是设计问题的信号,表示类之间的职责划分不清晰。默认禁止可以:
- 促使开发者改善代码设计
- 避免三级缓存带来的隐式复杂性
- 提前发现设计问题而不是隐藏它
相关链接
- Spring 循环依赖相关源码 - DefaultSingletonBeanRegistry
- IoC 容器与依赖注入 - Bean 的创建和注入方式
- Bean 生命周期 - 三级缓存在生命周期中的位置
- AOP 面向切面编程 - 三级缓存与 AOP 代理的关系