一、循环依赖的定义与本质
在Spring框架中,循环依赖指的是两个或多个Bean之间存在直接或间接的相互引用关系,从而形成一个闭合的依赖环。简而言之,当BeanA依赖BeanB,同时BeanB也依赖BeanA时,便构成了典型的循环依赖。
代码示例:
```java
@Component
publicclassBeanA{
@Autowired
privateBeanBbeanB;
}
@Component
publicclassBeanB{
@Autowired
privateBeanAbeanA;
}
```
这种场景类似于“先有鸡还是先有蛋”的哲学难题:创建BeanA需要先实例化BeanB,而创建BeanB又反过来需要BeanA。Spring通过一套精巧的机制解决了特定情况下的循环依赖,但并非所有场景都能处理。
二、Spring对循环依赖的支持与限制
Spring处理循环依赖的能力存在明确边界,主要取决于Bean的作用域和注入方式。
无法解决的场景一:构造器注入循环依赖
```java
@Component
publicclassBeanA{
publicBeanA(BeanBbeanB){...}
}
@Component
publicclassBeanB{
publicBeanB(BeanAbeanA){...}
}
```
原因:构造器注入要求在实例化Bean时必须完成所有依赖的注入。由于A和B相互依赖,导致双方都无法完成实例化,形成死锁。
结果:Spring直接抛出`BeanCurrentlyInCreationException`异常。
建议:在可能存在循环依赖的场景中,优先使用Setter注入而非构造器注入。
无法解决的场景二:原型(Prototype)作用域的循环依赖
```java
@Scope("prototype")
@Component
publicclassBeanA{...}
@Scope("prototype")
@Component
publicclassBeanB{...}
```
原因:原型Bean每次请求都会创建新实例,且Spring不会缓存这些实例。因此,无法通过“提前暴露半成品”的方式打破循环。
建议:从设计上避免原型Bean参与复杂的依赖网络。
可以解决的场景:单例作用域+Setter/字段注入
这是最常见的场景,也是Spring通过三级缓存机制能够妥善处理的场景。
三、SpringBean的生命周期与循环依赖的突破口
要理解循环依赖的解决机制,首先需要明晰SpringBean的完整生命周期,其核心分为四大阶段:
1.Bean定义扫描与注册:Spring通过反射扫描`@Component`等注解,为每个Bean创建`BeanDefinition`对象并注册。
2.Bean实例创建与初始化:这是解决循环依赖的关键阶段,包含以下核心步骤:
实例化:通过反射调用构造方法创建原始对象。
提前暴露引用(关键步骤):将原始对象包装为一个`ObjectFactory`并存入三级缓存。
属性填充:为对象注入其依赖的其他Bean。若依赖的Bean尚未创建,则会触发其创建流程。
初始化:执行`Aware`接口回调、`BeanPostProcessor`的前后置方法及`InitializingBean`的`afterPropertiesSet`方法。
3.Bean生存期:Bean完全初始化,驻留在应用上下文中供使用。
4.Bean销毁:容器关闭时,执行相关的销毁回调。
循环依赖的突破口就在于在属性填充阶段,允许引用一个尚未完成初始化的“早期”对象。
四、三级缓存机制详解
Spring通过三个层级的缓存来管理单例Bean的不同状态,以支持循环依赖的解决:
| 缓存级别 | 名称 | 存储内容 | Bean状态 |
| 一级缓存 | `singletonObjects` | 完全初始化好的Bean | 成品 |
| 二级缓存 | `earlySingletonObjects` | 早期暴露的Bean(已实例化,未完成属性注入和初始化) | 半成品 |
| 三级缓存 | `singletonFactories` | `ObjectFactory`工厂对象,用于生成早期Bean | 工厂 |
注意:只有单例Bean才会被纳入此三级缓存体系。
五、循环依赖解决流程全解析(以BeanA↔BeanB为例)
Step1:开始创建BeanA
1.容器开始创建`BeanA`,标记其为“创建中”。
2.实例化`BeanA`,得到一个原始对象。
3.将`BeanA`的`ObjectFactory`放入三级缓存。
4.准备为`BeanA`注入属性`beanB`,发现`BeanB`不存在。
Step2:转去创建BeanB
1.开始创建`BeanB`,标记其为“创建中”。
2.实例化`BeanB`,得到原始对象。
3.将`BeanB`的`ObjectFactory`放入三级缓存。
4.准备为`BeanB`注入属性`beanA`,发现需要`BeanA`。
Step3:解决僵局(核心步骤)
1.容器发现`BeanA`处于“创建中”状态。
2.从一级缓存未找到`BeanA`。
3.从二级缓存也未找到。
4.从三级缓存中获取到`BeanA`的`ObjectFactory`,并调用其`getObject()`方法。
5.此步骤可能生成`BeanA`的早期代理对象(若需要AOP),并将该对象放入二级缓存,同时从三级缓存移除工厂。
6.将这个`BeanA`的早期引用注入给`BeanB`。至此,僵局被打破。
Step4:BeanB完成创建
1.`BeanB`成功完成属性注入和后续初始化。
2.将完整的`BeanB`放入一级缓存。
Step5:BeanA完成创建
1.流程回到`BeanA`的属性注入阶段,此时它能从一级缓存获取到完整的`BeanB`。
2.`BeanA`完成属性注入和后续初始化。
3.将完整的`BeanA`放入一级缓存。
六、关键细节与设计思考
1.为何需要三级缓存?二级缓存是否足够?
三级缓存的核心价值在于延迟代理对象的创建并确保代理的唯一性。如果Bean需要被AOP代理(例如使用了`@Transactional`),提前暴露的必须是最终的代理对象。`ObjectFactory`提供了灵活性,确保只在真正发生循环依赖且需要注入时,才生成代理对象,避免了不必要的早期代理创建和潜在的代理对象不一致问题。
2.早期暴露的对象是否安全?
理论上,早期对象(半成品)的状态是不完整的(属性未注入)。但Spring通过严谨的生命周期管理确保:仅在解决循环依赖的注入环节使用它,且一旦目标Bean完成初始化,所有引用最终都会指向完全初始化后的成品Bean。
3.最佳实践是什么?
尽量避免循环依赖。尽管Spring提供了解决方案,但循环依赖意味着较高的代码耦合度,会降低代码的可读性、可测试性和模块化程度。它应被视为一种在特定情况下的“妥协”方案,而非推荐的设计模式。
七、总结
Spring解决(单例Setter注入)循环依赖的本质,是一种空间换时间和状态分离的策略。通过三级缓存提前暴露尚未完成初始化的对象引用,打断了循环依赖的致命等待链。这一机制深刻体现了SpringIoC容器在Bean生命周期管理上的精细与复杂,是理解Spring框架底层原理的重要一环。开发者应在掌握此机制的同时,铭记良好的软件设计是预防复杂依赖问题的根本。
来源:小程序app开发|ui设计|软件外包|IT技术服务公司-木风未来科技-成都木风未来科技有限公司