学过这么一遍,Spring循环依赖问题难不倒我
“我正在参加「掘金·启航计划」”
简单介绍
碰上问题了
今天原本愉快的在 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<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
// 二级缓存。完成实例化,但是还未进行属性注入及初始化的对象,也就是半成品对象
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
// 三级缓存。提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
-
一级缓存:
Map singletonObjects
用于存储单例模式下创建的 Bean 实例(已经创建完毕)。
该缓存是对外使用的,指的就是使用 Spring 框架的程序员。
K:bean
的名称V:bean
的实例对象(有代理对象则指的是代理对象,已经创建完毕) -
二级缓存:
Map earlySingletonObjects
用于存储单例模式下创建的 Bean 实例(该 Bean 被提前暴露的引用,该 Bean 还在创建中)。 该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。
K:bean
的名称V:bean
的实例对象(有代理对象则指的是代理对象,已经创建完毕) -
三级缓存:
Map<String, ObjectFactory<?>> 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> {
T getObject() throws BeansException;
}
下面的这个匿名内部类实现了上面的接口:
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance
(创建实例)以后,此时单例对象已经被建立。
此时对象已经被生产出来了,虽然还不完美,可是已经能被人认出来了(根据对象引用能定位到堆中的对象),因此Spring此时将这个对象提早曝光出来用来供认识和使用。
小结
本篇文章我们解决了突发的循环依赖问题,并且较为详细的解释了循环依赖究竟是什么样的问题,Spring 是如何解决循环依赖问题的。当然要彻底搞清楚这个知识点的内容还需要深入研究 Spring 源码。
本文参考:
转载自:https://juejin.cn/post/7202108826974978106