张三闪现开大居然把Spring循环依赖的骨灰都给扬了?

782 阅读13分钟

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 代理。

欢迎大家关注我的微信公众号:码外狂徒,记录张三与后端的爱恨情仇,每周更新两篇后端知识和至少三篇LeetCode解题思路

参考链接

mp.weixin.qq.com/s/Y4xCFTpht…

mp.weixin.qq.com/s/pQaX2-BqF…

jishuin.proginn.com/p/763bfbd2c…