Spring 循环依赖
见招拆招
在 Spring 容器中,两个或者多个 Bean 实例之间存在直接或者间接的依赖关系,导致在创建和初始化这些 Bean 的时候,形成了闭环,无法正确的创建和初始化这些 Bean,Spring 就会停止工作,抛出异常。
(上面是基本概念)
我在平时的开发过程中有遇到过这样的问题,当时就是找了一些文章看,了解了循环依赖产生的原理和一些常见的解决方案。
(埋钩子)
深入理解
循环依赖主要分为两种:
1、构造器循环依赖
/**
* Bean A
*/
@Component
public class TestA {
public TestA(TestB testB){
}
}
/**
* Bean B
*/
@Component
public class TestB {
public TestB(TestA a) {
}
}
Spring 异常截图:
这种依赖发生在 Bean 之间的构造器相互引用,由于构造器是在 Bean 实例化时调用的,所以 Spring 容器无法先创建其中一个 Bean ,因为这样会导致另一个 Bean 无法实例化,从而形成死循环。
2、setter循环依赖
/**
* Bean A
*/
@Component
public class TestA {
@Resource
private TestB testB;
}
/**
* Bean B
*/
@Component
public class TestB {
@Resource
private TestA testA;
}
Spring 异常截图:
与构造器循环依赖不同,setter注入是在 Bean 实例化完成之后进行的。因此,即使存在循环依赖,Spring容器也可以先实例化每个 Bean,然后通过setter方法注入。但是如果依赖关系比较复杂,或者配置不当,也会造成 Spring 无法初始化的现象。
解决方案
为了解决循环依赖的问题,Spring 容器采用了三级缓存机制来处理setter注入的循环依赖问题,而对于构造器循环依赖的话,无法通过缓存机制来解决,因为它需要在 Bean 实例化之前注入依赖。
(埋一个三级缓存的钩子)
当然,我认为呢,如果出现了循环依赖的问题,其实从侧面也反应了目前的代码结构设计不是那么的合理,最佳的做法是去重构代码,确保 Bean 之间的依赖关系是单向的,并且减少 Bean 之间的直接依赖。当然,也可以使用比如说 @Lazy注解啊,拿到Spring或者一些设计模式(工厂、代理等)去实现。
Spring的三级缓存
概念
一级缓存
存储那些完全初始化好的单例 bean
二级缓存
存储早期暴露的 bean 实例,部分初始化的 bean
三级缓存
用户存储 bean 工厂,主要用于创建bean的代理对象
三级缓存是如何解决循环依赖的?
假设现在我们有 A 、B 两个对象,A 依赖 B,B 依赖 A:
- 创建 A 对象放入 Spring 容器的过程中,先看一级缓存中是否存在,如果存在,则直接获取;如果不存在,则开始创建 A 对象。
- 在创建 A 对象的过程中,发现需要属性 B,也是先查询一级缓存,发现 B 没有在一级缓存当中,于是先将 A 放入三级缓存中,此时的 A 并不完整,没有属性,但是可以引用。
- 接下来就是实例化 B 的过程了。也是先查询以及缓存,没有则进行创建。此时发现需要属性A,查询一级缓存发现没有,然后查询二级缓存、三级缓存,直到找到为止。之间的操作中已经将 A 放到了三级缓存中,所以在三级缓存中可以找到,找到之后将 A 放入二级缓存中,删除三级缓存中的 A。
- B 顺利的创建完毕,将自己放入一级缓存中,此时 B 里面的 A 仍然是创建中的状态。然后接着回去创建 A,此时 B 已经创建结束,可以直接从一级缓存中拿到 B,然后完成 A 的创建,最后放入一级缓存中。
(以上这些,一定要理清楚)
其他问题
这些就不一一列举了,详细参见这篇博客:https://blog.csdn.net/sinat_34814635/article/details/128015738
重点关注一下这个问题吧:
为什么需要三级缓存,二级不够嘛?
Spring 之所以需要三级缓存而不是简单的二级缓存,主要原因在于AOP代理和Bean的早期引用问题。
二级缓存虽然可以解决循环依赖的问题,但在涉及到动态代理(AOP)时,直接使用二级缓存不做任何处理会导致我们拿到Bean 是未代理的原始对象。
如果二级缓存内存放的都是代理对象,则违反了 Bean 的生命周期。
(这里又干出了两个新考点)
总结
关于Spring循环依赖
这个问题,能够了解其原理和解决方案就差不多了,能记得住就记吧,记不住想方设法去绕开这个东西。
2025/01/02
writeBy kaiven