美文网首页开发技巧大后端
Spring面试题之循环依赖的理解

Spring面试题之循环依赖的理解

作者: 凯凯雄雄 | 来源:发表于2021-03-29 11:23 被阅读0次

    最近面试的时候发现很多人会问Spring是如何解决循环依赖的,虽然知道是通过三级缓存去解决的,但是也仅仅只是知其然,不知其所以然,抱着学习的心态还是好好捋一捋:

    • 三级缓存是如何解决循环依赖的?
    • 为什么是三级缓存?二级缓存行不行?
    • 有什么好的方式可以避免构建IOC的时候产生循环依赖?

    循环依赖的场景

    这个场景其实分为很多种:
    简单一点场景: A -> B -> A
    复杂一点的场景:

    • A 依赖 B,C
    • B依赖A
    • C依赖A

    在我们业务逻辑越来越复杂的时候,难免因为层级过深导致这种场景出现,但是在没有运行的时候发现不了。

    另外Spring是能够解决set属性赋值的循环依赖,但是构造器注入的是会有问题的,构造器在实例化的时候会出现死结,而set可以预先实例化后赋值所以好解决。

    三级依赖是如何解决的?

    首先我们要了解三级缓存的用处:

    • 一级缓存 singletonObjects : 用于保存实例化、注入、初始化完成的bean实例。

    这里就是生命周期已经加载完成了的对象

    • 二级缓存 earlySingletonObjects : 用于保存实例化完成的bean实例.

    其实也就是new完了的对象,但是没有进行set(依赖注入)、以及初始化的对象,就是简单的实例化对象。

    • 三级缓存 singletonFactories : 用于保存bean创建ObjectFactory工厂,方便后续可以创建代理对象。

    这里很重要,这个工厂里面会包含bean的创建,可能是普通对象,可能是代理过后的对象。可以理解为先把new完之后的实例引用先获取到。

    循环依赖场景: A -> B -> A

    了解大概流程,先不纠结于细节。

    image.png

    缓存获取顺序的代码:
    org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton

    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 先从一级缓存中获取
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
        synchronized(this.singletonObjects) {
            // 然后从二级缓存里面获取对象
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 最后从三级缓存中获取对象
                ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 从工厂里面获取对应的值,可能是普通实例,可能是代理对象
                    singletonObject = singletonFactory.getObject();
                    // 放入二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 删除三级缓存
                    this.singletonFactories.remove(beanName);
                }
            }
        }
      }
        return singletonObject;
    }
    
    

    这里有个很重要的点:就是当B要获取A的时候,从三级缓存里面查找,这时候已经能够找到了,就会从ObjectFactory工厂中返回一个对象,这个对象可能是普通实例也可能是代理对象。 这个时候会加入到二级缓存中,下一次查找就能从一级,然后到二级直接找到对象了,不会在走到三级封装成ObjectFactory对象了(每次从工厂里面拿可能会不是同一个实例)。

    也就是说,发生循环的时候,会从工厂中将对象提前实例化出来,然后这个引用会被会注入到发生循环依赖的Bean作为属性填充。

    另外下面的代码是创建bean中,会预先将bean封装成ObjectFactory对象的代码

    // org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
    // addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
                throws BeanCreationException {
    // .. 省略
    // 加入三级缓存中
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    
    Object exposedObject = bean;
    
    try {
        // 然后开始对该bean进行属性赋值
        populateBean(beanName, mbd, instanceWrapper);
        // 执行init方法.初始化bean
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
    }
    
    return exposedObject;
    // .. 省略
    }
    // 封装之后,加入三级缓存.
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(singletonFactory, "Singleton factory must not be null");
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                // 先加入三级缓存
                this.singletonFactories.put(beanName, singletonFactory);
                // 删除二级缓存
                this.earlySingletonObjects.remove(beanName);
                this.registeredSingletons.add(beanName);
            }
        }
    }
    // org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getEarlyBeanReference
    // 这里是ObjectFactory对象构建并获取的逻辑
    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
        Object exposedObject = bean;
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            // 这里会提前执行后置处理器,有可能会返回的是一个代理对象
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                // 通过这个SmartInstantiationAwareBeanPostProcessor类型的执行器,来获取提前暴露对象的逻辑
                if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                }
            }
        }
        return exposedObject;
    }
    
    // 当实例创建完成之后,会加入到单例工厂,从二级缓存升级到一级缓存中
    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
    

    这个时候B初始化好了直接加入到一级缓存中,而A在三级缓存被查找到后,放入二级缓存中,下一次查找会直接从二级缓存升级到一级缓存。流程结束。

    我估计你现在还是会有点疑问:
    ObjectFactory中获取的对象确实是提前暴露实例化的对象,但是它是咋进行属性填充的?

    还是看下图吧:

    引用逻辑

    我不知道注释的代码你能否理解...

    总的来说就是引用逻辑,先将引用传递到工厂中,让工厂构建早期bean的时候是基于这个实例的引用去做的,这个时候依赖注入给其他Bean的时候也是基于该引用,所以等到该bean初始化完成,其他被其依赖注入的bean的引用就是初始化完成的。

    为什么要三级缓存? 二级缓存行不行?

    从上面的流程上来看二级缓存只是为了将工厂得到的实例对象预先存储在二级缓存中,作用也不是特别明显。

    但是首先要思考一个问题:假设干掉二级缓存,三级变成两级。

    假设循环依赖的场景是: A->B->C->B->A

    流程图

    TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。
    说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的,比如代理对象,每次获取的都是一个新的代理对象。

    为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。

    所以二级缓存还是有必要的。区分工厂获取的对象和具体实例的引用对象。

    image.png

    还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?

    答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

    直接反射实例化的话,没办法经过SpringBean的后置处理器参考getEarlyBeanReference方法生成增强对象。

    有什么好的方式避免循环依赖吗?

    比如我明显知道A 依赖 B 了,这个时候B也需要A。

    我们可以采用懒加载的方式以及容器先加载完的方式再获取.

    1. @Layz注解,延迟加载
    2. B实现ApplicationContextAware的接口获取到上下文,然后从上下文中获取A,总的来说也是懒加载的思路.

    好了,以上仅仅是本人在遇到这个问题的一些延伸及参考加思考所写,感谢你这么忙还能观看我的文章。

    如果有问题欢迎留言交流,我会及时回复,希望共同进步。

    参考文章

    相关文章

      网友评论

        本文标题:Spring面试题之循环依赖的理解

        本文链接:https://www.haomeiwen.com/subject/ifpuhltx.html