简单介绍
碰上问题了
今天原本愉快的在 CRUD,结果一次循环依赖的问题打断了我的编码:
以往每次遇到循环依赖问题我都是通过让 Spring 允许循环依赖的方式去解决的。
但是想想看自己根本就不清楚这个 bug 到底该怎样健康解决,不懂 Spring 循环依赖的原理...
一直都不知道的话可还怎么和面试官对线?想到这里泪流了下来(bushi
菜咱就赶紧学起来,顺便记下一篇博客用来回顾。
接下来我们将依次剖析循环依赖的问题本身和原理。
什么是循环依赖?
循环依赖简单定义
就是对象依赖对象,依赖关系形成一条链,最后闭环到自己。
我们新建一个小的 SpringBoot 项目来复现一下循环依赖问题:
在 SpringBoot 项目下我创建一个类 A 并且依赖一个类 B。
@Component
public class A {
@Resource
private B b;
}
复制代码
同理我们创建一个类 B 并且依赖类 A。
@Component
public class B {
@Resource
private A a;
}
复制代码
这里我们能够想象到它的依赖链是一个 A->B->A,这样就是一个简单的循环依赖。
接着我们启动项目,报错如下:
果不其然出现问题。并且顺着依赖链我们同样可以推理出来 a 依赖 b 然后 b 依赖 a 的事实。
解决开始的问题
通过上面的小实验我们已经可以解决最开始我碰到的问题了
这里通过报错信息推理依赖链,是 PSignController 依赖 pSignService(对象),然后 pSignService 依赖自己。
我们看到源代码中的情况,下面是 PSignController 确实依赖一个 pSignService:
然后是 PSignService 接口的实现类,里面依赖了一个 pSignService:
所以由于 pSignService 自己依赖自己,导致出现循环依赖问题...
于是将该依赖删除掉,让 Service 层去依赖 Dao 层,这样循环依赖就解决了!所以说业务层之间还是尽量不要互相依赖为好。
仅仅解决问题是不够的,我们还要顺便将循环依赖问题的原理弄清楚
Spring 解决循环依赖的原理
不考虑 Spring 循环依赖是问题吗?
不考虑 Spring 其实循环依赖并不是问题,因为对象之间相互依赖是很正常的事情。
比如我们改造上面的代码如下:
@Getter
@Setter
class A {
private B b;
}
@Getter
@Setter
class B{
public A a;
}
?
?
@SpringBootTest
public class CircularDependencyTest {
?
@Test
public void testAB(){
A a=new A();
B b=new B();
a.setB(b);
b.setA(a);
}
?
}
复制代码
我们启动测试类,产生了如下图的循环依赖:
但是程序本身是不会有报错的。
为什么在 Spring 中的循环依赖是一个问题?
在 Spring 中,一个对象并不是简单 new 出来了,而是会经过一系列的 Bean 的生命周期,接着注册进 IOC 容器中。
就是因为 Bean 的生命周期所以才会出现循环依赖问题。
在 Spring 中,出现循环依赖的场景很多,有的场景 Spring 自动帮我们解决了,而有的场景则需要程序员来解决。
接着我们就首先来研究下 Spring 下一个 Bean 的创建过程
Bean 生命周期
Spring Bean 的生成是一个很复杂的流程,这里我们不详细展开 Bean 的生命周期,了解就好
- Spring 扫描 class 得到 BeanDefinition
- 根据得到的 BeanDefinition 去根据 name/type 生成 bean
- 首先根据 class 推断构造方法
- 根据推断出来的构造方法,反射,得到一个对象(暂时叫做原始对象)
- 利用依赖注入完成 Bean 中所有属性值的配置注入。
- 如果原始对象中的某个方法被 AOP 了,那么则需要根据原始对象生成一个代理对象
- 把最终生成的代理对象放入单例池( singletonObjects )中,下次 getBean 时就直接从单例池拿即可
Spring Bean 生成过程中的主要执行方法链
- createBeanInstance:实例化,其实也就是调用对象的构造方法或者工厂方法实例化对象
- populateBean:填充属性,这一步主要是对 bean 的依赖属性进行注入(@Autowired)
- initializeBean:回调执行 initMethod、InitializingBean 等方法
这里可以知道循环依赖问题应该是发生在 「populateBean 填充属性」阶段的,这个时候的实例状态属于已经实例化,还未初始化的中间状态。
了解了 Bean 生命周期后我们再重新分析一下为什么会出现循环依赖问题
还是拿上文的 A 类,B 类举例子。
- 首先创建 A 类的 Bean,A 类中存在一个 B 类的 b 属性,所以当A类生成了一个原始对象之后,就会去给 b 属性去赋值,此时就会根据 b 属性的 name/type 去 BeanFactory 中去获取 B 类所对应的单例 bean。
- 如果此时 BeanFactory 中存在 B 类对应的 Bean,那么直接拿来赋值给 b 属性;
- 如果此时 BeanFactory 中不存在 B 类对应的 Bean,则需要生成一个 B 对应的 Bean,然后赋值给 b 属性。
- 问题就出现在第二种情况,如果此时 B 类在 BeanFactory 中还没有生成对应的 Bean,那么就需要去生成,就会经过 B 的 Bean 的生命周期。于是我们的下一步就是创建一个 B 的 Bean。
- 接着创建 B 类的 Bean,如果 B 类中存在一个 A 类的 a 属性,那么在创建 B 的 Bean 的过程中就需要 A 类对应的Bean,但是,触发B类 Bean 的创建的条件是A类 Bean 在创建过程中的依赖注入。
- 所以这里就出现了循环依赖:
- ABean 创建-->依赖了 b 属性-->触发 BBean 创建---> B 依赖了 a 属性--->需要 ABean(但 ABean 还在创建过程中)
由于以上的过程(Bean生命周期),最终导致 ABean 创建不出来,BBean 也创建不出来。
Spring 三级缓存
Spring 能解决什么情况下的循环依赖?
依赖情况 | 依赖注入方式 | 循环依赖是否被解决 |
AB相互依赖(循环依赖) | 均采用field注入 | 否 |
AB相互依赖(循环依赖) | 均采用setter方法注入 | 是 |
AB相互依赖(循环依赖) | 均采用构造器注入 | 否 |
AB相互依赖(循环依赖) | A中注入B的方式为setter方法,B中注入A的方式为构造器 | 是 |
AB相互依赖(循环依赖) | B中注入A的方式为setter方法,A中注入B的方式为构造器 | 否 |
Spring 如何解决循环依赖问题?三级缓存具体是什么?怎么用?
首先我们需要知道 Spring 仅仅解决单例模式下属性依赖的循环问题。
而 Spring 为了解决单例的循环依赖问题,使用了如下「三级缓存」:
// 一级缓存,单例对象缓存池。存储所有创建好了的单例Bean
private final Map singletonObjects = new ConcurrentHashMap(256);
?
// 二级缓存。完成实例化,但是还未进行属性注入及初始化的对象,也就是半成品对象
private final Map earlySingletonObjects = new HashMap(16);
?
// 三级缓存。提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象
private final Map> singletonFactories = new HashMap>(16);
复制代码
- 一级缓存:Map singletonObjects
- 用于存储单例模式下创建的 Bean 实例(已经创建完毕)。
- 该缓存是对外使用的,指的就是使用 Spring 框架的程序员。
- K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)
- 二级缓存:Map earlySingletonObjects
- 用于存储单例模式下创建的 Bean 实例(该 Bean 被提前暴露的引用,该 Bean 还在创建中)。 该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。
- K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)
- 三级缓存:Map
> singletonFactories - 通过 ObjectFactory 对象来存储单例模式下提前暴露的 Bean 实例的引用(正在创建中)。
- 该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。
- 三级缓存是解决循环依赖的核心!这一点将在我们分析完成 Spring 获取单例对象的过程后搞清楚。
- K:bean 的名称 V:ObjectFactory 该对象持有提前暴露的 bean 的引用
Spring 获取单例对象过程,和三级缓存的关系
下面是 Spring 中获取单例的方法 getSingleton:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Spring首先从singletonObjects(一级缓存)中尝试获取
Object singletonObject = this.singletonObjects.get(beanName);
// 若是获取不到而且对象在建立中,则尝试从earlySingletonObjects(二级缓存)中获取
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 尝试从二级缓存中获取
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 获取二级缓存
ObjectFactory> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//调用三级缓存,调用到lambda表达式
//若是仍是获取不到而且容许从singletonFactories经过getObject获取,则经过singletonFactory.getObject()(三级缓存)获取
singletonObject = singletonFactory.getObject();
//若是获取到了则将singletonObject放入到earlySingletonObjects,也就是将三级缓存提高到二级缓存中
//放入到二级缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
//三级缓存中移除beanName的lambda表达式
this.singletonFactories.remove(beanName);
}
}
}
}
// 完整对象或者还未初始化的对象
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
复制代码
分析 getSinglelton方法的过程:
Spring 首先从一级缓存 singletonObjects 中获取。 若是获取不到,而且对象正在建立中,就再从二级缓存 earlySingletonObjects 中获取。若是仍是获取不到且容许 singletonFactories 经过 getObject() 获取,就从三级缓存
singletonFactory.getObject() (三级缓存) 获取,若是获取到了则从三级缓存移动到了二级缓存。最后就是获取到一个半成品对象所依赖的一个完整对象,然后将完整对象注入半成品对象中。
简单来说获取 bean 的顺序就是:从一级缓存中取,若不存在,从二级缓存中取,若还是不存在,则从三级缓存中取。
- setter 注入解决循环依赖问题
- 这一部分内容可信度有限,因为我用 2.6.2 版本的 SpringBoot 实际测试的结果是 setter 注入依旧会导致循环依赖问题。但是网上的大部分言论都是 setter 注入能解决,并且我认为也有一定道理,但是为了知识的完整度也试着汇总分享出来。如果有大佬希望能在评论区解答这个问题
- 我们还是使用 A 和 B 的例子来介绍如上流程如何解决循环依赖问题。不过这次我们的依赖注入方式我们用的是 setter 注入。
- @Component public class A { private B b; @Autowired public void setB(B b){ this.b=b; } } ? @Component public class B { private A a; @Autowired public void setA(A a){ this.a=a; } } 复制代码
- 依赖注入的流程如下:
- 其中没有产生循环依赖问题!
- setter 注入为什么能解决循环依赖?
- setter 方式解决循环依赖的核心就是提前将仅完成实例化的bean暴露出来,提供给其他bean。
第三级缓存 singletonFactories,Spring 解决循环依赖的核心!
经过分析我们清楚,三级缓存最重要的就是这个第三级缓存 singletonFactories 。
它的元素类型是 ObjectFactory 源码如下:
public interface ObjectFactory {
T getObject() throws BeansException;
}
复制代码
下面的这个匿名内部类实现了上面的接口:
addSingletonFactory(beanName, new ObjectFactory
此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance(创建实例)以后,此时单例对象已经被建立。
此时对象已经被生产出来了,虽然还不完美,可是已经能被人认出来了(根据对象引用能定位到堆中的对象),因此Spring此时将这个对象提早曝光出来用来供认识和使用。
小结
本篇文章我们解决了突发的循环依赖问题,并且较为详细的解释了循环依赖究竟是什么样的问题,Spring 是如何解决循环依赖问题的。当然要彻底搞清楚这个知识点的内容还需要深入研究 Spring 源码。
原文链接:
https://juejin.cn/post/7202108826974978106
本文暂时没有评论,来添加一个吧(●'◡'●)