Spring循环依赖是一道面试时候的高频问题,今天我们一起来看一下Spring是怎么解决循环依赖的问题吧
Spring循环依赖是什么?
循环依赖其实就是存在一些类,他们中的成员变量为其他类的对象,彼此之间相互引用,以上面图为例,就存在这A类对象引用B类对象,B类对象引用A对象,或者C本对象引用他本身,这就是循环依赖。
我们以朴素的思想想象一下,当我们创建A类对象a的时候,需要将B类对象b的引用赋值给a的成员变量,那么我们就需要去创建一个b,但是b创建的时候又会去创建一个a,如此循环往复便形成了死循环。那么Spring是怎么解决这个问题的呢?
Spring循环依赖出现的先决条件
Spring出现循环依赖的先决条件有两个
- 出现循环依赖的Bean必须是**单例(Singleton)**的。如果是原型模式(prototype),每次获取bean的时候都会新创建一个bean,当创建a的时候回去获取一个新的b,而创建b的时候又会去获取a,这个时候程序中singletonsCurrentlyInCreation这个list中已经存在了a,会直接判断报错。
- 先创建的bean采用有参构造注入。在这里我写出了五种情况,分别采用不同的注入顺序和方式,一起来看一下图吧
AB均采用属性自动注入
解决了循环依赖的问题,成功创建
AB均采用setter方法注入
解决了循环依赖的问题,成功创建
AB均采用构造方法方法注入
这里我们可以看到,同时采用有参构造注入出现了循环依赖无法解决的问题
A采用构造方法注入,B采用setter方法注入
这里我们可以看到,先创建的bean采用有参构造注入,后创建的bean采用setter注入,出现了循环依赖无法解决的问题
A采用setter方法注入,B采用构造方法注入
成功创建
为什么先创建的bean采用构造注入会抛出异常?
我们先来大概说一下bean创建的流程,当我们创建a的时候,由于需要先去创建b,此时spring会把a放入singletonsCurrentlyInCreation这个表中,这个表中含有的元素表示仍在创建过程中,还会把添加到inCreationCheckExclusions这个ConcurrentHashMap中,这个map中的含有的元素表示正在创建过程中。然后去创建b,创建b的时候有需要会头获取a,此时a还在singletonsCurrentlyInCreation和inCreationCheckExclusions中,表示a还未创建完成,此时会根据代码抛出效应的异常
//判断这个当前单例是否在创建过程中
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
//对应的抛出异常方法
public BeanCurrentlyInCreationException(String beanName) {
super(beanName, "Requested bean is currently in creation: Is there an unresolvable circular reference?");
}
那么为什么先采用setter方法就可以被spring解决循环依赖问题而不报错呢?我们接下来继续看
Spring如何解决循环依赖的问题
首先来说一点前置知识,我们创建出来的bean都会放在一个单例池中,当我们需要获得一个bean的时候(也就是执行getBean方法),会先去这个单例池里面找,如果有的话则直接返回,没有的话再去创建。当单例池里面没有我们需要的bean的时候整个获取流程包括三步,实例化,填充属性,初始化,这三步执行完之后会将创建好的bean放入单例池中,并将这个bean作为getBean的返回值返回。
我们先来猜想一下,由于出现循环依赖的bean一定是单例的,那么当我们先创建a获取b,再创建b去获取a的时候,我们可以猜测,此时a已经不存在于singletonsCurrentlyInCreation这个表中了,因为如果还存在于这个表中,此时一定会报错,也就是说,spring通过某种方式把没有创建完的半成品a提前暴露出去了(注意此时a并未完成创建过程),因此b才可以获取到a,从而完成b的创建,至于后续怎么处理a,我们后面再看。我们先来画一下我们的猜想
我们继续假设,在创建b获取a的时候,a已经被移动到另外一个专门存放半成品对象的列表(实际上a是在执行实例化的过程之后就被放入了这个半成品池)。那么我们获取a的时候,由于a还是存在于inCreationCheckExclusions这个map中,说明a还在创建过程中,我们在singletonsCurrentlyInCreation找不到这个a,因此我们就去这个半成品表中找到a,那么b这个bean的创建过程就完成了,然后由于b已经成功创建,回到a的创建过程中,a也成功创建。
需要注意的是,我们上面这段只是我们的猜想,有很多地方的细节很不完善,但是大概思路基本可以确定下来。这里还有一个很大的问题,循环依赖的解决就这么结束了嘛?其实并没有,我们上述说的是原始对象的循环依赖解决猜想,在spring中的bean需要通过AOP进行代理,也就是我们真正需要解决的是代理对象的循环依赖解决方案,那么顺着上面的思路,我们继续来看代理对象的循环依赖如何解决。
在前置知识中我们说到,创建bean需要执行三步,实例化,填充属性,初始化。其中初始化阶段就是创建真正的代理对象,那么是不是说只有执行到初始化阶段才能创建代理呢?非也,在spring中有一个getEarlyBeanReference()方法,这个方法的作用是将我们的早期对象暴露出去,那什么又是早期对象呢?有的朋友可能已经猜到了,这个早期对象就是我们假设中提前暴露出去的半成品对象,那么也就是说,我们可以通过提前暴露的方式获得了早期对象,通过这个早期对象创建了代理对象,那么这个早期对象的代理对象会放在哪里呢?首先肯定不是放在存原始对象半成品池里面,那么肯定是有另外一个池子专门用来存放早期代理对象,我们暂且叫他代理池。
代理池中的a,是由于创建b的时候需要对a进行一个提前暴露,但是这时候半成品中的a并不是真正的代理对象,所以我们需要执行创建早期代理对象方法,并将这个早期代理对象放入代理池,并且将半成品池中相应的对象删除。那么当a的早期代理对象创建完成之后,b的赋值过程也就结束了,然后会进行下一步的初始化过程创建b的代理对象,由于b的代理对象没有其他对象进行提前引用,就会执行正常的代理对象创建过程,并把它放入代理池中。此时b的代理对象已经创建完成,我们把b的代理对象放入单例池中,然后回到a对象的赋值过程,赋值过程结束之后,由于此时的a已经是代理对象了,就不需要执行代理对象的创建了,直接执行初始化和后置处理过程。此时a也成功创建,把从a放回到单例池中,然后把代理池中的a删除就可了。
需要注意的是,上面只是我们猜测的一个过程,里面有很多错误和漏洞,后面会给大家总结一个正确的流程,整个猜想过程只是为了引导我们的思路。
源码分析
为了验证我们的猜想,我们一定要回到源码中进行确认,先来找到相关的类DefaultSingletonBeanRegistry,在这个类中有三个非常显眼的成员变量
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
这里需要说明的是singletonObjects,所谓的一级缓存,就是我们上面说的单例池,获取单例对象都是从这里面获取
earlySingletonObjects,存放早期代理对象,二级缓存
singletonFactories三级缓存,就是我们说的半成品池,里面存放早期对象,从这里面取出的时候会立即创建早期对象的代理对象
通过这张图我们可以发现在这个过程中相当于直接返回了一个null,表示我们程序刚启动并不存在这样一个bean,,第一次创建b的过程中执行这个函数也是返回null,由于获取不到我们需要的bean,我们需要去创建对应的bean,来看一下这部分的代码,由于源码过长,这里只保留了最核心的部分
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
String beanName = this.transformedBeanName(name);
//这里的getgetSingleton(String)方法
//实际上就是对上图中的函数进行了一次封装
Object sharedInstance = this.getSingleton(beanName);
Object bean;
//单例池中已经存在
if (sharedInstance != null && args == null) {
bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
}
//单例池中不存在
else {
//单例模式下
if (mbd.isSingleton()) {
//执行
sharedInstance = this.getSingleton(beanName, () -> {
try {
//创建Bean
return this.createBean(beanName, mbd, args);
} catch (BeansException var5) {
this.destroySingleton(beanName);
throw var5;
}
});
bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
return bean;
}
我们先来看createBean方法,这个方法的核心是调用了doCreateBean方法,其余部分我们暂且不看,有兴趣的同学可以自行研究
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
...
try {
beanInstance = this.doCreateBean(beanName, mbdToUse, args);
...
...
return beanInstance;
} catch (...) {
...
}
}
我们深入到doCreateBean方法中去看一下
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
//通过反射进行实例化
instanceWrapper = this.createBeanInstance(beanName, mbd, args);
}
//获去通过反射创建的bean
Object bean = instanceWrapper.getWrappedInstance();
//判断是否允许早期暴露
//三个条件分别是
//isSingleton() 是单例
//allowCircularReferences 允许循环引用
//isSingletonCurrentlyInCreation(beanName) bean正在创建过程中
boolean earlySingletonExposure = mbd.isSingleton() && this.allowCircularReferences && this.isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
//将得到的早期对象放入SingletonFactory
//在这个方法中会把早期对象放入singletonFactories
//并且把earlySingletonObjects中的早期对象删除
this.addSingletonFactory(beanName, () -> {
//getEarlyBeanReference表示对传入的bean进行提前暴露
return this.getEarlyBeanReference(beanName, mbd, bean);
});
}
}
其实看到这里,大概思路就已经比较明确了,通过把不同阶段的对象放入到不同的缓存中,相互配合完成代理对象的循环依赖解决。
还有一点需要看的代码就是我们刚开始图中的那段代码,我们一起来看一下吧
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//从一级缓存中找
Object singletonObject = this.singletonObjects.get(beanName);
//一级缓存中不存在 且 这个bean正在创建过程中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//在二级缓存中找
singletonObject = this.earlySingletonObjects.get(beanName);
//二级缓存中不存在且允许从三级缓存中拿到bean
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//三级缓存返回的是一个工厂,通过工厂来获取创建代理对象
singletonObject = singletonFactory.getObject();
//将创建好的代理对象丢到二级缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
//从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
整个过程我们就分析完了,我们再来简单的分析一下整个流程,真正完整正确详细流程如下
0.开始
1.从容器中获取a
2.从3个缓存级别中找a,找不到
3.进行创建a
4.对a进行实例化,得到a的实例,但此时的a并未填充属性也没有执行初始化操作
5.将早期的a暴露出去,直接放到三级缓存singletonFactories中
6.准备对a进行属性填充,发现需要b,尝试从容器中获取b
7.从三个缓存级别中找b,找不到
8.进行创建b
9.对b进行实例化,得到b的实例,但此时的b并未填充属性也没有执行初始化操作
10.将早期的b暴露出去,放到三级缓存singletonFactories中
11.准备对b进行属性填充,发现需要a,尝试从容器中获取a
12.最终在三级缓存singletonFactories中找到了a,执行singletonFactory.getObject方法创建代理对象,把a从三级缓存singletonFactories中删除,并放入二级缓存earlySingletonObjects中,并返回给b并进行属性填充。
13.b执行初始化方法和后置处理过程,完成创建过程,从三级缓存singletonFactories和二级缓存earlySingletonObjects中删除b并把b放入一级缓存singletonObjects中
14.将b返回给a
15.完成a的属性填充
16.a执行初始化方法和后置处理过程,完成创建过程,从三级缓存singletonFactories和二级缓存earlySingletonObjects中删除a并把a放入一级缓存singletonObjects中
17.整个过程结束
多例下的循环依赖能否解决?
情况一 a,b都是多例,这个情况我们前面说过,由于多例下必定不会进行提前暴露,所有创建过程中的bean都被记录在singletonsCurrentlyInCreation中,当b去获取a的时候发现a已经在创建过程中,会直接报错
情况二 a为多例,b为单例,由于会先去获取a,程序启动的时候a并不存在,需要先创建,并需要创建b进行注入,由于创建b的时候又需要获取a,此时a已经存在于singletonsCurrentlyInCreation中,会直接报错
情况三 a为单例,b为多例,由于需要先创建a,且a是单例,可以进行提前暴露,所以创建b的时候可以拿到a的早期代理对象,完成b的创建过程,进而完成a的创建
能否用二级缓存代替三级缓存?
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。