在Spring框架中,循环依赖指的是两个或多个Bean之间相互依赖,形成闭环。例如,Bean A依赖于Bean B,而Bean B又依赖于Bean A。这种情况如果处理不当,会导致应用程序无法正常启动。
形成原因
-
构造函数注入:当使用构造函数注入时,若存在循环依赖,则会在创建Bean的过程中导致无限递归调用,最终抛出
StackOverflowError
异常。这是因为Spring无法在一个Bean完全创建之前将其传递给另一个Bean。(这种情况通常无法自动解决,对于这种情形,Spring建议使用setter注入代替构造器注入,或者考虑重新设计你的类结构以避免循环依赖。) -
属性依赖(Setter方法注入):对于属性依赖或setter方法注入的情况,尽管Spring能够处理单例模式下的循环依赖问题,但这也可能导致系统复杂性增加和难以维护。
为什么在java中对象中属性依赖不会出现循环依赖的现象而在spring就会出现循环依赖?
普通Java对象:
在普通的Java编程中,当你使用new
关键字创建一个对象时,该对象立即就可以被使用了。这意味着,即使两个对象相互依赖(比如A依赖B,同时B也依赖A),只要你在初始化这些对象时能够合理安排它们的构造顺序,并通过setter方法或者构造函数参数等方式设置依赖关系,就能避免循环依赖带来的问题。
Spring Bean:
然而,在Spring框架中,Bean的创建和管理涉及到更复杂的生命周期过程。Spring默认是按照单例模式来管理Bean的,这意呸着每个Bean在IoC容器中只有一个实例。Spring Bean的生命周期包括实例化、属性填充(设置依赖)、初始化等阶段。如果两个或多个Bean之间存在循环依赖,特别是在属性填充阶段需要完全解析所有依赖的情况下,就会导致问题。
Spring解决循环依赖的方式
-
三级缓存机制:Spring使用了三个主要的缓存来处理和解决循环依赖的问题:
- singletonObjects(一级缓存):存储已经完全初始化好的单例Bean。
- earlySingletonObjects(二级缓存):存储早期暴露的单例对象,这些对象可能还未完成属性填充。
- singletonFactories(三级缓存):存储Bean工厂对象,用于创建早期暴露的单例对象。
-
具体解决流程:
- 当Spring尝试创建一个单例Bean A时,它首先会检查一级缓存
singletonObjects
是否已经有这个Bean。如果没有,则开始实例化Bean A,并将半成品的A放入三级缓存singletonFactories
中。 - 如果在创建A的过程中发现需要另一个Bean B,Spring会尝试去获取B。如果B也存在循环依赖,即B也需要A才能完成初始化,那么此时Spring会从三级缓存中取出A的工厂,利用它创建一个不完全的A实例并将其放入二级缓存
earlySingletonObjects
中。 - 接下来,当Spring继续创建B并尝试注入A时,它会在一级缓存中找不到A,然后检查二级缓存
earlySingletonObjects
。由于前面步骤中已经把A放到了二级缓存里,所以现在可以从这里拿到A的引用并完成B的创建。 - 最后,当B创建完成后,返回到A的创建过程中,这时A也能顺利完成了,因为它所需的B已经被正确创建并注入。
- 当Spring尝试创建一个单例Bean A时,它首先会检查一级缓存
注意事项
-
构造器注入不支持循环依赖:Spring不能解决通过构造函数进行注入时产生的循环依赖问题,这是因为构造函数注入要求在创建Bean之前就必须满足所有依赖条件,而循环依赖违反了这一原则。
-
非单例作用域不支持循环依赖:Spring只对单例作用域内的Bean提供了循环依赖的支持,对于原型(Prototype)或其他非单例作用域的Bean,Spring不会尝试解决它们之间的循环依赖问题。
补充:
@DependsOn
与循环依赖
假设你有两个Bean A和B,并且A依赖于B,同时B也依赖于A。如果你尝试使用@DependsOn
来管理它们的初始化顺序,这将导致问题,因为这种配置会创建一个无法解决的循环依赖关系:
@Component
@DependsOn("beanB")
public class BeanA {public BeanA(BeanB beanB) {// Constructor injection of BeanB into BeanA}
}@Component
@DependsOn("beanA")
public class BeanB {public BeanB(BeanA beanA) {// Constructor injection of BeanA into BeanB}
}
在这种情况下,Spring容器在尝试初始化这些Bean时将会陷入困境,因为它无法决定先初始化哪一个Bean。
解决方案
-
重构代码以避免直接循环依赖:最直接的方法是重构你的代码,消除直接的循环依赖。可以通过提取共同依赖到一个新的Bean中,或者使用事件、回调等机制间接地建立依赖关系。
-
使用Setter注入而非构造器注入:如果设计允许的话,可以考虑使用Setter注入而不是构造器注入。Spring能够更好地处理通过setter方法建立的循环依赖,因为它可以在对象实例化后设置属性。
-
应用延迟加载(Lazy Initialization):对于某些场景,你可以声明一个Bean为懒加载(
@Lazy
),这样只有在实际需要该Bean的时候才会进行初始化。这种方法有时可以帮助缓解循环依赖的问题。 -
谨慎使用
@DependsOn
:尽量只在确实需要控制初始化顺序的情况下使用@DependsOn
,并且确保这样做不会引入循环依赖。在多数情况下,Spring的自动装配机制已经足够智能,不需要额外指定依赖顺序。
记住,虽然Spring提供了一些工具和技术来处理循环依赖,但最好的做法还是尽量设计你的应用程序以避免出现循环依赖的情况。这样不仅可以简化配置,还可以提高代码的可维护性和清晰度。