15.三级缓存解决循环依赖

🥬 15.三级缓存解决循环依赖

经过之前的编码,spring最核心的IOC和AOP的内容已经基本搭建完成,IOC章节中实现了bean的定义和注册,属性填充,读取配置文件,应用上下文,修改,初始化,销毁,aware感知,FactoryBean,事件监听。AOP章节中实现了对普通bean进行增强,自动扫描bean的注册,代理对象注入属性的内容。但是到目前为止,一个关键的问题还没有解决,那就是循环依赖,本节中就来解决这个问题,相关的代码我放到了 仓库

原因

​ 在bean的创建过程中,需要依赖一些属性,将这些属性填充完整才能完成bean的创建,而这些属性不只是普通属性,有可能在bean的创建过程中,还会依赖一些其他的bean,一旦这些被依赖的bean在创建过程中也依赖当前的bean,就会产生循环依赖的问题,如下图所示一共有三种循环依赖的情况:

img

但是三种循环依赖的本质上是一样的,都是在创建过程中依赖一个正在创建的bean,这样相互依赖导致bean无法创建成功,本节中来解决这些问题

思路

​ 为了解决循环依赖的问题,spring引入了三级缓存的概念,其中每一级缓存都各司其职从而解决了循环依赖的问题,下面就以第二种A和B创建过程中互相依赖的情况来描述三级缓存如何解决循环依赖:

  1. 创建A,此时会先创建一个空bean,然后将这个空bean保存到三级缓存中,不管后面是否能用到,之后进行属性填充
  2. 在属性填充的过程中,需要填充B,此时就会去一级缓存中找对应的B,发现B不存在,再去二级缓存中找,也没找到,再去三级缓存中找,还是没找到,此时新建B
  3. 创建B的时候也会先创建一个空bean,然后也将这个空bean保存到三级缓存中,之后进行属性填充
  4. 在属性填充的过程中,需要填充A的属性,此时就会去一级缓存中找对应的A,发现A不存在,再去二级缓存中找,也没找到,再去三级缓存中找,找到之前注册的空A,然后就判断这个A是否需要AOP,需要的话就创建一个代理对象,不需要的话就是普通对象,然后将这个对象放到二级缓存中并填充给B中A属性
  5. B执行其他的属性填充,其他的工作,然后完成B的创建,此时B中的A还是一个空壳子
  6. 完成创建之后,将这个B保存到一级缓存中,然后将其从三级缓存中删除
  7. 之后继续A属性的填充,此时由于三级缓存中提前保存了未实例化完成的A,于是B可以创建完成,A也就可以用这个B进行属性填充,从而完成创建
  8. A创建完成之后,执行正常的生命周期,此时的A还是一个普通对象,属性填充完成之后,由于B中的A与当前属性填充完的A是同一个地址,所以B中的A属性不再是空。后面就需要判断是否需要进行AOP,如果前面进行了AOP,这里就不需要进行AOP,最后调用getSingleton方法得到最终版的结果。在这个方法中,如果提前进行了AOP,直接从二级缓存中就可以得到最终的答案,如果没有提前进行AOP,此时会从三级缓存中拿到最终的对象(普通或者代理)放进一级缓存

相当于如果出现了循环依赖,提前进行了AOP,那么二级缓存中就会存储尝试AOP之后的A,进行正常的生命周期到达正常的AOP步骤时,不用再AOP,最后直接将二级缓存中的A保存到单例池中即可。如果没有出现循环依赖,最终也会调用getSingleton方法,然后重新创建一个AOP对象,关于这里的解释可以看下面的描述:


假设bean需要AOP

如果出现循环依赖,利用三级缓存中保存的信息就可以创建出来一个bean代理对象,然后将其保存到二级缓存中,最后保存到单例池中时,会从二级缓存中拿到这个创建出来的bean代理对象,此时由于AOP提前,所以initializeBean方法中的正常AOP步骤不会执行,这是第一个正常AOP失效的地方 如果没有出现循环依赖,会正常经过bean的生命周期,之后执行AOP的操作,虽然当前bean得到了一个代理对象,但是最后的代码经过了一步:

1
2
3
4
5
6
7
Object exposedObject = bean;
if (beanDefinition.isSingleton()) {
    // 获取代理对象,获取到真正需要的bean
    exposedObject = getSingleton(beanName);
    //将创建完成的bean对象,不管是否出现了循环依赖,都将其保存到最终的单例池中
    registerSingleton(beanName, exposedObject);
} 

这个代码中的getSingleton会尝试从一级二级缓存中拿到bean对象,但是由于没有循环依赖,所以拿不到,于是会从三级缓存中重新创建一个代理bean对象,相当于之前经过正常AOP流程创建出来的bean对象失效不会保存到单例池中,这是第二个正常AOP失效的地方,当没有出现循环依赖时,可以看到正常创建出来的bean代理对象和真正保存到单例池中的代理对象是不一样的: image

所以即使将initializeBean方法中的正常AOP流程去掉也不会对当前的项目有什么影响,因为本项目中的正常AOP流程已经失效了,去掉内部的getSingleton方法可以使得正常的AOP起到效果,但是出现循环依赖后,会提前进行AOP,代理对象被保存到了二级缓存中,如果不调用getSingleton方法,那么当前的bean就是普通的bean,会出现错误,所以这里宁愿正常的AOP失效也不要造成错误,正常的AOP失效不会对外产生任何影响

这里只是这个项目中的bug,并不是真正spring中存在这个问题


核心点就是一个提前暴露的三级缓存,还有一个getSingleton方法内部决定bean的获取逻辑,如果出现循环依赖,先从一级缓存中拿,然后从二级缓存中拿,最后才从三级缓存中拿,保证bean的单例性。提前暴露的目的是为了打破循环

核心点总结如下:

  1. 空bean一创建,三级缓存中就保存了一个ObjectFactory对象,可以根据当前创建的空bean来尝试提前AOP,如果当前对象需要AOP,那么就创建代理对象,否则就创建普通对象,这个bean还没有经历正常的生命周期,为的是出现循环依赖时打破这个循环,提前AOP是为了属性注入时注入的是代理对象的地址而不是普通对象的地址(如果需要AOP的话)
  2. 出现循环依赖时,getSingleton方法中控制了三级缓存的顺序,从而打破这个循环依赖
  3. 后期bean创建完成需要保存到单例池中时,为了保持bean的单例性,会从三级缓存中拿bean对象

下面以流程图的形式描述A和B出现循环依赖时怎么创建:

未命名文件 (4)

出现了循环依赖就会用第三级缓存打破,用第二级缓存保持单例性,然后创建完成之后,将bean对象最终保存到第一级缓存单例池中

三级缓存的作用

一级缓存

一级缓存存放的是已经初始化好的bean,也就是最终可以对外使用的bean

二级缓存

二级缓存存放的是还没有完全被初始化好的中间对象代理,即已经生成了bean但是这个bean还有部分成员对象还未被注入进来,但这个bean后面又被依赖时就可以拿这个没初始化好的bean来打破循环依赖,而不是再次从三级缓存中创建,保持了bean的单例性

三级缓存

三级缓存存放的是还未初始化完的bean,而这些bean只是早期的简单对象,并不是代理对象。三级缓存是用于解决循环依赖问题,当对象A依赖对象B时,A对象就会被先临时存放在三级缓存中,然后初始化B对象。在这个时间点如果有别的注入对象需要依赖A对象就会从三级缓存中查询,并通过三级缓存的getObject方法尝试生成代理对象然后将A对象从三级缓存中删除,放入二级缓存。这一级缓存是为了打破循环依赖

类的变化

为了解决循环依赖,对项目中的类进行了一些修改:

  1. DefaultSingletonBeanRegistry:在这个类中增加三级缓存的容器,并且修改getSingleton方法的逻辑,在其中控制三级缓存之间的读取流程,并且对外提供向第三级缓存中保存空白普通bean的方法,修改向第一级缓存单例池中添加bean的代码:

    image-20231116162258820

    核心就是getSingleton代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public Object getSingleton(String beanName) {
        //从一级缓存中拿
        Object singletonObject = singletonObjects.get(beanName);
        if (null == singletonObject) {
            singletonObject = earlySingletonObjects.get(beanName);
            // 从二级缓存中拿
            if (null == singletonObject) {
                //从三级缓存中拿
                //专门用来打破循环依赖
                ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    // 把三级缓存中的代理对象中的真实对象获取出来,放入二级缓存中
                    earlySingletonObjects.put(beanName, singletonObject);
                    singletonFactories.remove(beanName);
                }
            }
        }
        return singletonObject;
    }
    
  2. InstantiationAwareBeanPostProcessor:这个接口中新增加一个待实现的方法,用来将对象的AOP提前,AOP中进行匹配器的匹配,当前对象不一定创建代理对象: image-20231116164251338

  3. DefaultAdvisorAutoProxyCreator:类中将创建代理对象的方法封装起来,在两个方法中调用,分别是postProcessAfterInitializationgetEarlyBeanReference方法中,第一个方法中调用创建代理对象的方法是因为执行了正常的AOP流程,第二个方法中调用创建代理对象的方法是因为出现了循环依赖,要先将一个空对象创建出来打破循环依赖,第二个方法经过包装最终会在第三级缓存中的getObject方法中调用。并且一旦当前对象提前经过了AOP,那么后面在正常AOP的触发时机到达时就不用再进行AOP了,为了实现这种动态的判断,将提前AOP的对象都在容器中保存一份,这个容器是earlyProxyReferences

    image-20231116164734400

  4. AbstractAutowireCapableBeanFactory:在这个类中修改bean的生命周期过程,当一个空bean创建出来之后,就在三级缓存中保存一个lambda表达式,表达式内部保存的是上面说的getEarlyBeanReference方法逻辑,也就是说当从三级缓存中获取对象时会调用保存的getEarlyBeanReference方法,从而将对象的AOP提前,如果当前对象确实需要AOP,那么就会创建代理对象。否则就是普通对象,然后将这个普通对象保存到二级缓存中并返回,也就是说这一步只是为了尝试给对象进行AOP

    并且在将对象创建完成之后,会将创建完成的代理对象从二级缓存中拿到并保存到单例池中

    image-20231116165043150

  5. ObjectFactory:这是一个函数式接口,当一个空bean创建完成之后,就利用这个接口类型的lambda表达式保存刚创建出来的空bean信息:

    image-20231116192754523

​ 总的来说就是在将单例池一级缓存改成三级缓存,然后在创建对象时就将未创建完成的bean保存到三级缓存中提前暴露,这样提前暴露就可以打循环依赖,并且如果在创建过程中出现循环依赖,就要将针对对象尝试AOP的步骤提前,此时需要记录一下,后期正常AOP的流程就不用执行了,三个缓存都有其单独的作用,单一职责,第三级缓存只是为了打破循环依赖,一旦出现循环依赖,此时就将尝试对bean进行AOP,然后将尝试AOP 之后的对象放入二级缓存并从三级缓存中删除,此时获取到的bean就是尝试AOP但是还没有属性填充完毕的bean,只是一个外壳,有了自己的最终地址,可以临时使用

bean的创建和获取

下面从bean的创建和获取的角度出发讲解如何解决循环依赖的问题,项目中有wifehusband两个bean,剩下的bean都是为了实现spring中基础功能所注册的bean,其中wifehusband互相依赖,而wife需要进行AOP代理:

  1. 读取配置文件,refresh方法中调用preInstantiateSingletons方法开始创建wife的bean对象,创建时会先尝试去缓存中拿:

    image-20231116191307938

  2. 尝试从三级缓存中拿到wife对象,但是初始状态下拿不到wife的bean对象,也就是getSingleton方法会返回null

    image-20231116191405568

  3. 此时需要创建wife的bean对象,也就是调用createBean方法,最终会执行doCreateBean方法,此时只是将xml文件中配置的bean的注册信息扫描完成,但是并没有完成注册:

    image-20231116202532377

  4. 执行doCreateBean方法,内部先创建一个空bean,并且将刚创建的空bean保存到第三级缓存中,后期走到从第三级缓存中获取bean对象时会触发getEarlyBeanReference方法的执行,针对当前bean尝试AOP之后再返回一个搭好的架子

    image-20231116202908603

  5. wife填充属性,这里由于wife内部依赖husband,而husband内部又依赖wife,所以会出现循环依赖,一共经历以下几步来解决循环依赖,从而完成对wife的创建:

    1. 填充hubsband属性,调用getBean方法尝试从缓存中拿到husband

      image-20231116203239661

    2. getBean方法中调用doGetBean方法,内部先调用getSingleton方法,尝试从三级缓存中拿到husband对象,由于husband还没有创建,所以缓存中还没有,所以会调用createBean方法创建husband对象,注意此时wife正在创建中

      image-20231116203546016

    3. 创建husband的bean对象,执行doCreateBean方法,内部先创建一个空bean,并且将创建逻辑保存到第三级缓存中,此时的第三级缓存中保存的有一个没创建完成的wifehusband,两个bean都没有经过AOP:

      image-20231116203819165

    4. husband进行属性填充,husband内部依赖wife,而wife正在创建中,但是这里可以利用第三级缓存中还没有创建完成的提前暴露的wife先完成husband的属性填充:

      image-20231116204101475

    5. getBean方法中调用doGetBean方法,内部先调用getSingleton方法,尝试从三级缓存中拿到wife对象完成husband中对wife的属性填充,此时第三级缓存中保存了wife没有创建完成的对象,于是可以拿到,这一步会触发getEarlyBeanReference方法

      image-20231116204236210

    6. 调用getObject拿到需要的wife对象,内部调用保存的lambda表达式中的getEarlyBeanReference方法,将AOP的操作提前,主要是调用getEarlyBeanReference方法完成AOP的工作,这一步的目的是为了给出现循环依赖的bean填充经历了AOP之后的bean,AOP提前才能使得属性填充中的属性地址正确

      image-20231116204353554

    7. getEarlyBeanReference方法内部,先将这个提前AOP的对象保存到容器中标记一下,然后调用wrapIfNecessary完成对wife的代理对象的创建并返回:

      image-20240312142735401

    8. 从第三级缓存中拿到提前AOP创建出来的wife代理对象之后,将其保存到二级缓存中,并将三级缓存中关于wife的信息删除,此时返回这个提前AOP 的bean,如果创建了代理对象,那么代理对象内部的target真正被代理对象还没有创建完,属性还没有填充完成,但是这个wife代理对象用来给husband做属性填充是足够的,如果没创建代理对象,那么这个普通对象内部的属性还没有填充完成,但是用来做属性填充也是足够的:

      所以这一步只是为了避免循环依赖中被依赖的属性需要进行AOP,所以出现循环依赖之后,从第三级缓存中获取bean对象就会将当前对象的AOP提前,没有出现AOP时,第三级缓存是没有作用的

      image-20231116204759513

    9. husband通过getBean方法得到一个wife的代理对象之后进行属性填充,可以看到husbandwife属性已经填充好了,但是其wife内部的属性还没有填充完成,只是一个架子,但是这足够解决循环依赖了:

      image-20231116205004634

    10. 完成对husband的创建,此时已经打破了循环依赖,完成了对husband的创建,也就是说,wife所依赖的husband创建出来了,此时将husband保存到单例池中,也就是一级缓存中,并且将husband保存到剩下两级缓存中的内容删除,这些操作都是为了保持bean的单例性:

      image-20231116205517886

    11. 至此完成husband的创建,然后返回刚创建的husband

  6. wife调用getBean方法经历上面的10步就拿到了创建成功的husband对象,husband内部依赖的wife此时还没有创建完成,此时就开始wife的属性填充,也就是回到最开始wife属性填充到husband的步骤:

    image-20231116205749789

  7. 属性填充完毕之后,wife的创建也结束了,由于xml文件中配置了切面,标记要对wife进行增强,所以上面husband获取wife对象时获取到的是代理对象,当wife创建成功之后,因为AOP提前了,所以此时的wife是一个普通对象,这个普通对象在提前AOP时保存到了代理对象中的target中,而真正的wife经过了AOP增强,需要将代理对象保存到单例池中,所以下一步就是将真正需要保存的bean拿出来保存到单例池中:

    image-20231116210100456

  8. 在创建husband对象时,创建的wife代理对象保存到了二级缓存中,所以此时就调用getSingleton方法从二级缓存中拿到真实的代理对象,然后将其保存到单例池中:

    image-20231116210304221

  9. 完成对wife的创建后,husband内部的wife也填充完成,此时wifehusband都创建完毕,执行自己的业务逻辑,可以发现出现循环依赖的两个bean都创建成功,并且还执行了AOP的增强:

    image-20231116210429697

​ 经过上面的步骤,循环依赖被完美的解决,但是需要注意的是,上面的wife需要进行AOP,所以创建的是代理对象,如果wife不需要AOP,那么上面的第八步中调用getSingleton拿到的exposedBeanbean是一样的,都是普通的bean。如果wife需要AOP,但是wife没有循环依赖的问题,那么就会执行正常的AOP流程,不会执行提前AOP,最后调用getSingleton尝试拿到AOP对象时拿到的是一个新的代理对象

image

也就是说没有出现循环依赖时,经过正常AOP创建出来的代理对象失效,没有什么作用,最终保存到单例池中供外部使用的代理对象是getSingleton方法中拿到的新代理对象,但是只是正常的AOP流程失效,并不会对spring对外提供的功能产生影响

总结

​ 经历上面的步骤之后,完美的解决了循环依赖的问题,主要是将使用三级缓存将出现循环依赖的bean提前暴露,并且保持其单例性,打破循环依赖之后,bean就可以正常创建,只是将bean所依赖的bean(普通对象或者代理对象)的地址提前拿到,完成一个bean的创建之后,另外一个bean的创建也可以完成,之后由于地址相同,内容同步更新,最后就可以完成所有bean的创建

​ 主要是第三级缓存保存了一个还没有经过属性填充和AOP的普通空bean,后期一旦出现循环依赖,就从第三级缓存中拿到这个普通空bean并尝试AOP,这一步是为了防止属性填充的是普通bean,之后这个普通bean还需要AOP,从而出现属性填充的bean和最终生成的bean不一致的情况,所以将AOP提前,依赖关系填充好后可以打破循环依赖,后期再慢慢的填充属性即可