什么是循环依赖?
循环依赖指的是两个或多个 Bean 之间相互依赖的情况。例如:
// A依赖B
@Component
public class A {
@Autowired
private B b;
}
// B依赖A
@Component
public class B {
@Autowired
private A a;
}
在这个例子中,A 的创建需要 B,而 B 的创建又需要 A,形成了一个闭环。如果没有特殊处理,这将导致无限循环或初始化失败。
循环依赖的类型
Spring 中存在三种主要的循环依赖场景:
- 构造器注入循环依赖:通过构造方法相互依赖
- setter 注入循环依赖:通过 setter 方法相互依赖
- 字段注入循环依赖:通过字段直接注入相互依赖
其中,只有构造器注入的循环依赖是 Spring 无法解决的,而后两种可以通过 Spring 的缓存机制完美解决。
Spring 解决循环依赖的核心:三级缓存
Spring 通过三级缓存机制解决了单例 Bean 的循环依赖问题,这三个缓存实际上是三个 Map:
- 一级缓存(singletonObjects):存储完全初始化完成的单例 Bean
- 二级缓存(earlySingletonObjects):存储提前暴露的、尚未完全初始化的单例 Bean 实例
- 三级缓存(singletonFactories):存储 Bean 的工厂对象(ObjectFactory),用于创建 Bean 的早期引用
这三级缓存的定义可以在DefaultSingletonBeanRegistry
类中找到:
// 一级缓存:存放完全初始化好的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放早期暴露的Bean实例(未完全初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:存放Bean工厂,用于生成早期Bean引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
解决循环依赖的详细流程
让我们以 A 依赖 B,B 依赖 A 的场景为例,详细解析 Spring 如何处理循环依赖:
-
初始化 A
- 检查三级缓存,发现 A 不存在
- 实例化 A(调用构造方法,此时 A 的属性尚未设置)
- 将 A 的工厂对象添加到三级缓存:
singletonFactories.put("a", () -> getEarlyBeanReference("a", mbd, a))
- 开始为 A 填充属性,发现依赖 B
-
初始化 B
- 检查三级缓存,发现 B 不存在
- 实例化 B
- 将 B 的工厂对象添加到三级缓存
- 开始为 B 填充属性,发现依赖 A
-
处理 B 对 A 的依赖
- 检查一级缓存,A 不存在(未完全初始化)
- 检查二级缓存,A 不存在
- 检查三级缓存,找到 A 的工厂对象
- 通过工厂对象获取 A 的早期引用
- 将 A 的早期引用从三级缓存移到二级缓存
- 将 A 的早期引用注入到 B 中
- B 完成初始化,放入一级缓存
-
完成 A 的初始化
- 将 B 从一级缓存中取出,注入到 A 中
- A 完成初始化,从二级缓存移到一级缓存
通过这个流程,Spring 成功打破了 "A 依赖 B,B 依赖 A" 的循环,两个 Bean 都能被正确初始化。
三级缓存的精妙之处
为什么需要三级缓存而不是两级?这与 Spring 的 AOP 特性密切相关。
三级缓存中存储的是ObjectFactory
,它的作用是在必要时生成 Bean 的代理对象。当一个 Bean 需要被 AOP 增强时,这个工厂会返回代理对象而不是原始对象。
如果没有三级缓存,直接在二级缓存中存储原始对象,那么当需要代理时,注入的就会是原始对象而不是代理对象,这显然不符合 AOP 的预期。
通过三级缓存,Spring 实现了延迟创建代理对象的功能,只有在发生循环依赖时才会提前创建代理,否则会在 Bean 初始化完成后再创建代理,这符合 Spring 的设计理念。
无法解决的循环依赖情况
Spring 的循环依赖解决方案并非万能,以下情况无法解决:
-
构造器注入的循环依赖
@Component
public class A {
private B b;@Autowired public A(B b) { this.b = b; }
}
@Component
public class B {
private A a;@Autowired public B(A a) { this.a = a; }
}
这种情况下,Spring 会抛出BeanCurrentlyInCreationException
异常,因为构造器注入需要在实例化阶段就获取依赖,而此时依赖对象还未创建。
原型 Bean 的循环依赖
原型 Bean(@Scope("prototype")
)每次请求都会创建新实例,Spring 不会缓存原型 Bean,因此无法解决其循环依赖。
非单例 Bean 的循环依赖
除了单例 Bean 外,其他作用域的 Bean(如 request、session)的循环依赖也无法被 Spring 解决。
如何避免循环依赖?
虽然 Spring 能解决单例 Bean 的循环依赖,但在设计上应尽量避免循环依赖,这通常意味着代码结构可以优化:
- 重构代码:将相互依赖的部分提取到第三个类中
- 使用 @Lazy 注解:延迟初始化其中一个 Bean
- 使用 setter 注入代替构造器注入:构造器注入更适合强制依赖,但可能导致循环依赖