Spring 循环依赖:从原理到解决方案的全面解析
一、循环依赖的定义与分类
1. 什么是循环依赖?
在 Spring 框架中,循环依赖指的是多个 Bean 之间形成了依赖闭环。例如:
- Bean A 依赖 Bean B
- Bean B 依赖 Bean C
- Bean C 又依赖 Bean A
此时,这三个 Bean 之间就形成了循环依赖关系。当容器尝试初始化这些 Bean 时,会陷入无法完成初始化的死循环。
2. 循环依赖的三种类型
根据依赖注入方式的不同,循环依赖可分为三类:
类型 | 描述 |
---|---|
构造器循环依赖 | 多个 Bean 通过构造函数互相依赖,如A构造器注入B ,B构造器注入A 。 |
setter 循环依赖 | 多个 Bean 通过 setter 方法互相依赖,如A.setB(b) ,B.setA(a) 。 |
字段注入循环依赖 | 多个 Bean 通过字段直接注入互相依赖,如@Autowired private B b; ,@Autowired private A a; 。 |
二、Spring 如何处理循环依赖?
1. Spring 处理循环依赖的核心机制
Spring 通过三级缓存机制解决 setter 和字段注入的循环依赖,而构造器循环依赖则无法自动解决。
三级缓存的定义(DefaultSingletonBeanRegistry
类):
- 一级缓存(singletonObjects):存储完全初始化的 Bean 实例。
- 二级缓存(earlySingletonObjects):存储已创建但未完全初始化的 Bean 实例(早期暴露的对象)。
- 三级缓存(singletonFactories):存储 Bean 的工厂对象,用于生成代理对象等后置处理。
2. 解决 setter 循环依赖的流程示例
以A依赖B,B依赖A
的 setter 循环依赖为例:
- 初始化 A:创建 A 的实例,放入二级缓存,并标记为 “未完全初始化”。
- 注入 B 到 A:发现 A 依赖 B,开始初始化 B。
- 初始化 B:创建 B 的实例,放入二级缓存,然后尝试注入 A 到 B。
- 注入 A 到 B:此时 A 已在二级缓存中,B 获取 A 的早期实例并完成注入,B 初始化完成后放入一级缓存。
- 完成 A 的初始化:A 获取到已初始化的 B,完成注入后放入一级缓存。
3. 三级缓存的核心作用
- 三级缓存的存在是为了处理 AOP 代理:当 Bean 需要代理时,三级缓存存储的工厂对象会在早期暴露阶段生成代理实例,避免循环依赖中出现 “原始对象” 和 “代理对象” 的不一致问题。
三、构造器循环依赖为何无法解决?
1. 构造器循环依赖的初始化流程
假设A构造器注入B
,B构造器注入A
:
- 初始化 A 时,需要先创建 B 的实例。
- 初始化 B 时,又需要先创建 A 的实例。
- 由于构造器依赖必须在对象创建时完成,两者互相等待,导致初始化阻塞。
2. 示例代码与异常
@Component
public class A {
private final B b;
// 构造器注入B,导致循环依赖
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
public B(A a) {
this.a = a;
}
}
启动 Spring 容器时会抛出org.springframework.beans.factory.UnsatisfiedDependencyException
,提示无法解析循环依赖。
四、循环依赖的解决方案
1. 针对构造器循环依赖:
方案一:使用 setter 注入替代构造器注入
@Component
public class A {
private B b;
// 使用setter注入,允许Spring通过三级缓存解决循环依赖
public void setB(B b) {
this.b = b;
}
}
方案二:使用 @Lazy 延迟初始化
通过@Lazy
让 Spring 注入代理对象,延迟依赖解析:
@Component
public class A {
private final B b;
// 注入B的代理对象,避免初始化时立即创建B
public A(@Lazy B b) {
this.b = b;
}
}
方案三:拆分 Bean,打破依赖链
将复杂 Bean 拆分为多个小 Bean,避免直接依赖。
2. 针对 setter / 字段循环依赖:
通常无需特殊处理,Spring 三级缓存可自动解决。若遇到问题,可能是以下原因:
- Bean 使用了
@PostConstruct
等初始化方法,且方法中存在循环逻辑。 - 自定义 BeanPostProcessor 干扰了三级缓存的正常工作。
3. 通用最佳实践:
- 优先使用构造器注入:明确依赖关系,但需避免构造器循环依赖。
- 谨慎使用 @Autowired 字段注入:可能隐藏依赖关系,推荐搭配 setter 注入。
- 使用 @DependsOn:强制指定 Bean 初始化顺序,打破隐性循环依赖。
- 模块化设计:通过拆分服务或引入中间层,避免跨模块的直接依赖。
五、Spring Boot 中循环依赖的排查与工具
1. 日志排查
启动时添加 JVM 参数-Dspring.main.allow-circular-references=true
(Spring Boot 2.6+),允许循环依赖并打印警告日志。
2. IDE 工具辅助
- IntelliJ IDEA:通过
Analyze Dependencies
功能检测循环依赖。 - Spring Tool Suite:使用依赖分析视图定位问题 Bean。
3. 编程式排查
通过ConfigurableApplicationContext.getBeanFactory()
获取DefaultListableBeanFactory
,调用isPrototypeCurrentlyInCreation()
等方法诊断循环依赖。
六、深度解析:三级缓存的源码视角
1. 关键源码路径(AbstractBeanFactory.doGetBean
):
// 从一级缓存获取Bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && !isSingletonCurrentlyInCreation(beanName)) {
return getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
// 标记Bean为“正在创建”
beforeSingletonCreation(beanName);
try {
// 从二级缓存获取早期实例
sharedInstance = getSingleton(beanName, false);
if (sharedInstance != null) {
// 处理早期实例
return getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
// 创建Bean实例(未初始化)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 将早期实例放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 填充依赖(可能触发循环依赖)
populateBean(beanName, mbd, instanceWrapper);
// 初始化Bean(调用后置处理器等)
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 将完全初始化的Bean放入一级缓存
addSingleton(beanName, exposedObject);
} finally {
afterSingletonCreation(beanName);
}
2. 核心方法解析:
- getSingleton(beanName, true):尝试从一级缓存获取 Bean,若不存在则创建。
- addSingletonFactory:将 Bean 的工厂对象存入三级缓存,用于生成早期实例。
- getEarlyBeanReference:处理 AOP 代理等后置操作,返回早期实例。
七、总结:循环依赖的本质与设计哲学
Spring 通过三级缓存解决循环依赖的核心,是利用 “早期暴露” 机制打破初始化死锁:将未完全初始化的 Bean 提前暴露到二级缓存,允许其他 Bean 先获取其引用,后续再完成初始化。
但构造器循环依赖无法解决,这体现了 Spring 的设计原则:构造器依赖应代表 “强依赖”,而强依赖不应形成循环。在实际开发中,合理的依赖设计(如模块化、单向依赖)比依赖 Spring 的循环依赖处理机制更重要。
理解循环依赖的原理与解决方案,不仅能帮助开发者快速定位问题,还能加深对 Spring Bean 生命周期和依赖注入机制的理解,从而写出更健壮的代码。