为什么每个Spring开发者都必须掌握循环依赖的解决方案?
在开发岗位面试中,Spring框架的循环依赖问题几乎是必考题。但更重要的是,在实际企业级开发中,理解循环依赖的解决机制能够帮助你:
- 快速定位和解决诡异的Bean创建异常
- 设计出更优雅、解耦的系统架构
- 避免陷入"明明代码没问题,为什么启动失败"的困境
- 提升对Spring核心机制的理解深度
本期咱们一起探讨剖析这个既经典又实用的问题,让你在面试和实际开发中都能游刃有余。
什么是循环依赖?
简单来说,循环依赖就是两个或多个Bean之间相互依赖,形成一个"死循环"。例如Bean A依赖于Bean B,而Bean B又依赖于Bean A。这种情况下,Spring在初始化这些Bean时会陷入两难境地:先创建A需要B,但创建B又需要A。
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}Bean的创建过程:理解关键前提
要理解Spring如何解决循环依赖,首先需要了解Bean的完整创建过程:
- 实例化:通过构造函数创建Bean的原始对象(相当于new操作)
- 属性填充:为对象注入依赖的其他Bean
- 初始化:执行各种回调方法(如@PostConstruct)
循环依赖问题的核心在于:我们需要在A的属性填充阶段注入B,但B此时可能还未创建完成。
三级缓存:Spring的解决方案
Spring通过三级缓存机制巧妙解决了这个问题:
第一级缓存:singletonObjects
存放已经完全初始化好的单例Bean(成品),一旦需要获取Bean,首先从这里查找。
第二级缓存:earlySingletonObjects
存放提前暴露的原始Bean实例(半成品),用于解决循环依赖。
第三级缓存:singletonFactories
存放Bean工厂对象,用于生成原始Bean的提前引用。
解决循环依赖的完整流程
让我们通过一个具体例子来看Spring如何处理A和B的循环依赖:
- 开始创建A
- 调用A的构造函数实例化A对象
- 将A对象的工厂放入三级缓存(singletonFactories)
- 开始为A填充属性,发现需要B
- 转而创建B
- 调用B的构造函数实例化B对象
- 将B对象的工厂放入三级缓存
- 开始为B填充属性,发现需要A
- 获取A的引用
- 从一级缓存查找A(未找到)
- 从二级缓存查找A(未找到)
- 从三级缓存找到A的工厂,生成A的早期引用
- 将A的早期引用放入二级缓存,并从三级缓存移除
- 将A的早期引用注入到B中
- 完成B的创建
- B完成属性填充和初始化
- 将B放入一级缓存
- 从二级和三级缓存中移除B的相关引用
- 完成A的创建
- 将B注入到A中(此时B已在一级缓存中)
- A完成属性填充和初始化
- 将A放入一级缓存
- 从二级缓存中移除A的引用
这个过程中,关键点在于Spring在实例化后就将对象工厂放入三级缓存,使得其他Bean可以获取到它的早期引用,即使它还没有完成初始化。
AOP代理与循环依赖
当涉及AOP代理时,情况变得更加复杂。Spring需要确保循环依赖的双方注入的是代理对象而不是原始对象。三级缓存中的工厂正好解决了这个问题——它不仅可以返回原始对象,还可以返回代理对象。
// 三级缓存中的工厂逻辑
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
// 如果需要代理,这里会返回代理对象而不是原始对象
return mbd.getEarlyBeanReference(beanName, bean);
}无法解决的循环依赖场景
需要注意的是,Spring并非能解决所有循环依赖:
- 构造函数循环依赖:无法解决,因为实例化都无法完成
@Component
public class A {
public A(B b) { ... }
}
@Component
public class B {
public B(A a) { ... }
}- 原型(prototype)作用域的循环依赖:无法解决
- @Async注解方法的循环依赖:可能无法正确解决
实际开发中的建议
虽然Spring提供了循环依赖的解决方案,但在实际项目中,咱们仍应该:
- 尽量避免循环依赖:良好的设计应该避免这种紧耦合
- 使用setter注入而非构造器注入:setter注入允许Spring解决循环依赖
- 使用@Lazy注解:延迟加载可以打破循环依赖
@Component
public class A {
@Lazy
@Autowired
private B b;
}Spring通过三级缓存机制巧妙解决了循环依赖问题,其核心思想是提前暴露原始对象的引用,使得Bean在尚未完成初始化时就能被其他Bean引用。这种设计既体现了Spring框架的精巧,也展示了解决复杂问题的创新思维。
小编认为理解这个机制不仅有助于应对面试,更能让我们在实际开发中更好地使用Spring框架,写出更健壮、可维护的代码。