参考地址:
Spring循环依赖:https://zhuanlan.zhihu.com/p/700890658
Spring三级缓存解决循环依赖的问题:https://blog.csdn.net/Trong_/article/details/134063622
==================================================================
1.什么是循环依赖?
1>说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。 2>循环依赖,发生在属性赋值阶段。 3>循环依赖发生在被显式依赖的情况下,即 ①属性注入依赖②构造函数注入依赖③Setter方法注入依赖 注意: @DependsOn注解的使用,只是指明Bean的初始化顺序,业务上存在先后装载顺序,但并不是显式的依赖关系,因此@DependsOn注解并不造成循环依赖。
第一种情况:自己依赖自己的直接依赖
第二种情况:两个对象之间的直接依赖
第三种情况:多个对象之间的间接依赖
前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。
2.Spring支持解决哪些循环依赖
2.1 Spring 初始化Bean的底层过程
Spring创建Bean的过程以及Bean的声明周期,详细可见 【面试 Spring】基础普及 https://www.cnblogs.com/sxdcgaq8080/diary/2024/12/17/18612056
我们可知,Spring生命周期分为四个: 实例化 ->属性赋值 ->初始化 ->销毁。
而Spring容器要完成Bean的加载,也基本按照这个步骤进行:
1.Spring容器启动 ->
2.从XML或Configuration加载Bean的定义 ->
3.实例化Bean ->
4.装配Bean的属性 (即依赖注入环节)->
5.对Bean做BeanPostProcess前置动作(AOP before) ->
6.对Bean做 @PostConstruct 、Initialization接口afterPropertiesSet() 、XML的init-method等初始化方法的执行 ->
7.对Bean做BeanPostProcess后置动作 (AOP after)->
8.完成对Bean的最终初始化动作。
......
好,那循环依赖在什么环节发生?
就是发生在依赖注入的环节 即 装配Bean的属性(属性赋值)阶段。
2.2 形成循环依赖的成因
构造函数循环依赖
在使用构造函数注入Bean时,如果两个Bean之间相互依赖,就可能会形成构造函数循环依赖,例如:
@Component
public class A { private B b; public A(B b) { this.b = b; }}
==============
@Component
public class B { private A a; public B(A a) { this.a = a; }}
上述代码,A、B的构造函数分别需要创建对方,A依赖B,B依赖A,它们之间形成了一个循环依赖。
当Spring容器启动时,它会尝试先实例化A,但是在实例化A的时候需要先实例化B,而实例化B的时候需要先实例化A,这样就形成了一个循环依赖的死循环,从而导致应用程序无法正常启动。
属性循环依赖
在使用属性注入Bean时,如果两个Bean之间相互依赖,就可能会形成属性循环依赖。例如:
@Component
public class A { @Autowired private B b;
}
=============
@Component
public class B { @Autowired private A a;
}
类似的,同样Spring在实例化A时会注入B,而注入B时又需要注入A,形成循环依赖
2.3 Spring可以解决那些注入的循环依赖
如上面的过程中,可以确认的是1>Spring支持解决的循环依赖,只针对单例(Singleton)的Bean。原型(Prototype 即多例业务域)的Bean出现循环依赖,Spring会直接抛出异常。2>Spring不支持 A\B初始化顺序上,A依赖B需要构造函数注入才能完成实例化的方式。而Spring支持 A\B 循环依赖通过Setter/属性 注入的方式。
1>Spring仅支持解决单例的Bean的循环依赖,不支持原型/多例作用域的Bean的循环依赖
①Spring可以通过 三级缓存 设计,解决 单例模式Bean的循环依赖问题 ②Spring无法解决多例/原型(Propotype)作用域Bean的循环依赖问题 原因在于: 多例模式的Bean的创建,是区别于 单例Bean的创建过程。三级缓存针对单例Bean解决循环依赖,主要这样做: 1>A实例化阶段,将 A_BeanFactory放入三级缓存 2>A属性注入阶段,发现依赖B,此时B未创建,所以去实例化B 3>B实例化阶段,将B_BeanFactory放入三级缓存 4>B属性注入阶段,三级B删除,放入二级B,发现依赖A,从三级缓存 一级\二级\三级依次找下去,发现三级中的A 5>使用A_BeanFactory对B完成属性注入,同时A也从三级中删掉,放入了二级缓存 6>B完成属性注入后,删除二级B,将完成初始化的B放入一级缓存 7>接着A继续进行属性赋值,从 一级\二级\三级依次找B,从一级中找到B,完成属性注入 8>删除二级A,放入一级A,AB至此都完成了初始化。可以看出,单例的Bean是在 其生命周期 不同阶段,加入三级缓存,才有了可操作的空间。 而多例的Bean,是每次请求容器getBean()都会去创建新的Bean,那这个过程中多例的Bean又依赖了其他多例的Bean,就会导致被依赖的多例的Bean也加入创建过程,这样就形成了 无限递归的 多例创建过程,最终导致栈溢出(StockOverflowError) 或 内存溢出(OutOfMemoryError).
2>Spring支持的单例Bean下循环依赖时的注入方式
从上面一节,我们可以知道,Spring使用三级缓存解决 单例Bean的依赖注入,是利用了Spring管理Bean的生命周期过程,才有了可操作的空间。1.实例化阶段 只创建Bean 2.属性赋值解决 才解决依赖注入 3.初始化前后 AOP切面增强逻辑因此,以 A和B循环依赖为例, 先实例化A,在实例化B的先后顺序来讲,如果AB均采用构造器注入,就无法给三级缓存操作空间,即第一步A实例化就无法成功。 如果A对B采用构造器注入,B对A采用Setter注入,那同理第一步A实例化就无法成功。因为实例化一个对象,需要通过执行它的构造函数。而其他依赖注入方式,无论属性注入还是 setter注入 都是可以在 通过实例化阶段后的第二阶段再调用,因此给了三级缓存可操作的空间。
3.Spring解决循环依赖的方案--三级缓存
3.1 三级缓存示意图
// 一级缓存 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 三级缓存 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
3.2 三级缓存解决Spring中单例Bean的循环依赖问题
三级缓存,从一级、二级、三级依次查找的使用顺序。1>实例化A,将A_ObjectFactory放入三级缓存,表示A已经开始了实例化。objectFactory.getObject()内部最终调用getEarlyBeanReference()方法2>实例化完A,开始对A进行属性赋值阶段(即依赖注入),发现A依赖B 3>从三级缓存依次查找,未找到B 4>实例化B,将B_ObjectFactory放入三级缓存,表示B开始了实例化。 5>实例化完B,开始对B进行属性赋值阶段,发现B依赖A 6>从三级缓存依次查找,在三级缓存找到A 7>将A_ObjectFactory从三级缓存取出,判断A是否被AOP切面代理,如果A被代理,则objectFactory.getObject()内部最终调用getEarlyBeanReference()方法,返回A的代理对象A_proxy,放入二级缓存如果A未被代理,则直接返回真实A对象,放入二级缓存虽然此时A还未完全完成属性赋值和初始化,但真实A 或代理对象A_proxy 已经足以完成对B的依赖注入了 8>B完成属性赋值后,完成初始化动作,放入一级缓存,清理二级、三级缓存中的B 9>从二级缓存拿出A或A_proxy,从三级缓存依次查找B,找到B完成对A的属性赋值,完成A的属性赋值和初始化,A放入一级缓存,二级、三级缓存清理 10>完成单例的A和B循环依赖的初始化。
4. 答疑解惑