[关闭]
@Catyee 2021-06-29T09:54:09.000000Z 字数 15759 阅读 757

源码解析Spring循环依赖

Spring


声明:下文出现的Spring源码都基于Spring 5.3.7版本

一、什么是循环依赖

所谓循环依赖是指两个或两个以上的bean互相引用对方,最终形成了一个依赖的闭环

说到循环依赖好像就会有一种先入为主的印象,总感觉是自己在代码实现上出现了问题,其实循环依赖是与实际业务相关的,某些业务在实现的时候可能就是需要bean A依赖bean B,同时bean B又依赖bean A,这是正常的情况,开发者在编程的时候可以尽量避免循环依赖,但如果遇到某个业务实在无法避免也没关系,Spring本身可以自动解决一部分循环依赖的情况,只要正确的使用Spring,即使有循环依赖也完全可以正常运行,关键在于我们需要弄清楚Spring中有哪些循环依赖的情况,以及Spring无法解决哪些情况。

二、Spring如何检测循环依赖

Spring检测循环依赖的方式也比较简单,在创建bean的时候可以给一个标签,表明当前bean正在创建,然后递归的创建依赖的bean,递归创建的过程中如果发现bean已经处于创建中的状态,就说明存在循环依赖了,存在循环依赖并不会直接报错,只有Spring无法自动解决的时候才会报错。

三、Spring能处理哪些循环依赖的情况

先说结论:如果存在一个依赖闭环,我们称这个闭环中最先被Spring创建的Bean为闭环中的第一个Bean(Spring按照字典序创建Bean),如果这个Bean是构造器注入,其它Bean不管是构造器注入还是Set方法注入,spring都无法自动解决,并且会在启动的时候报错提醒。简单来说就是只要存在依赖闭环,并且闭环中第一个bean是构造器注入,其它bean不管是构造器注入还是Set方法注入,这些情况Spring都无法自动解决,其余情况Spring都能自动解决

在分析原因之前先来说说Spring处理循环依赖的主要理论依据,我们知道Spring Bean说到底也是java对象,bean之间的依赖关系说到底也就是对象之间的引用关系,比如Bean A依赖Bean B,实际上就是A对象中的一个属性保持了对B对象的引用,从这个角度来说Spring创建bean并处理依赖关系的过程可以简单的认为就是实例化一个java对象并填充java对象属性的过程。那java对象的属性可以在什么时候填充呢?最为常见的有两种,第一种是使用有参构造器,在实例化的时候就填充上;第二种是先通过无参构造器把对象实例化出来,然后通过属性的set方法填充,在Spring中这两种方式分别对应构造器注入和Set方法注入。Set方法注入相比构造器注入的不同就在于使用Set方法进行注入的时候bean已经实例化出来了,只是属性暂时还没填充完整,这种bean被称为提前暴露的bean。bean之间建立依赖关系只要有一个对象的引用就可以了,至于这个对象的属性是否已经填充完整并不关心,这些属性可以延后填充, 所以提前暴露出来的bean虽然不完整但已经可以注入到其它bean了。

这就是Spring自动解决循环依赖的理论依据:当出现循环依赖的时候可以先把依赖闭环中第一个bean实例化并提前暴露出来,这样闭环上依赖这个bean的bean就可以创建完成,这个bean创建出来之后,依赖这个bean的下一个bean也就可以创建出来,从而整个递归能够顺利进行下去,当跳出最后一层递归之后第一个bean依赖的bean也已经创建出来了,只要将这个bean再注入到第一个bean中整个闭环上所有bean就都创建完成了。文字描述比较抽象,举个例子,假如A依赖B,B依赖C,C又依赖A,先把A实例化并暴露出来,这个样A的引用已经有了并且能被其它bean发现。由于A依赖B,所以接下来尝试去创建B,发现B又依赖C,所以尝试去创建C,发现C又依赖A,这个时候发现A的引用已经有了,所以直接注入到C,C能顺利创建,C创建完成之后B也就能顺利创建,B创建完之后,将B注入A,A也就创建完成了,整个依赖闭环上的Bean都创建成功了。关键就在于依赖闭环的第一个Bean能不能提前暴露出来,为什么一定是第一个呢?因为最后一个bean总会依赖第一个Bean,如果第一个bean不能提前暴露出来,最后一个bean就无法创建完成,从而导致整个递归过程失败。哪种情况第一个bean无法提前暴露出来呢?没错,就是第一个bean使用构造器注入的时候。

4.3 Spring的三级缓存

我们称Spring为容器,容器从字面意义上就是存放东西的器皿,那Spring作为容器存放了什么呢?没错,就是Bean,最终所有的单例bean都会存放到Spring的单例池中。但是在Spring启动的时候单例bean并不是一开始就存放到了单例池的,而是使用了三级缓存,创建的过程中会按照需要放入到不同的缓存,但最终都会移入到单例池中,单例池实际上就是三级缓存中的第一级缓存,看DefaultSingletonBeanRegistry类的源码:

  1. // DefaultSingletonBeanRegistry类源码
  2. /** Cache of singleton objects: bean name to bean instance. */
  3. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  4. /** Cache of singleton factories: bean name to ObjectFactory. */
  5. private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  6. /** Cache of early singleton objects: bean name to bean instance. */
  7. private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

所谓三级缓存就是三个Map,Spring官方并没有直接指明层级,不过根据代码,singletonObjects总是最先被查询,我们姑且称为一级缓存,以此类推,earlySingletonObjects为二级缓存,singletonFactories为三级缓存。它们各自的含义如下:

4.3.1 三级缓存的使用过程

在没有AOP的情况下我们来看三级缓存的实际使用过程,这里描述四种情况下缓存的使用涵盖了所有的场景:第一种情况是Bean A和Bean B没有依赖关系;第二种情况是Bean A依赖Bean B,Bean B不依赖Bean A;第三种情况Bean A不依赖Bean B,Bean B依赖Bean A;第四种情况为Bean A和Bean B互相依赖,也就是循环依赖。

在往下阅读之前先声明一下,Spring创建一个bean可以简化的看成两个步骤,第一步是实例化bean;第二步是填充属性,也就是处理依赖关系,本文中将第二步填充属性称之为"装配bean",所以后文出现"装配bean"的描述,含义就是处理bean的依赖关系。同时当读者在下文中看到"创建bean"字样的时候,要理解这个"创建"实际是包含了两个步骤的,两步都完成才算创建完成。
在下文中还会出现"移入"的字样,比如"把bean从三级缓存中移入到一级缓存"这样的描述,这里的"移入"指的是DefaultSingletonBeanRegistry类中addSingleton方法的逻辑:

  1. // DefaultSingletonBeanRegistry类中的addSingleton方法
  2. protected void addSingleton(String beanName, Object singletonObject) {
  3. synchronized (this.singletonObjects) {
  4. this.singletonObjects.put(beanName, singletonObject);
  5. this.singletonFactories.remove(beanName);
  6. this.earlySingletonObjects.remove(beanName);
  7. this.registeredSingletons.add(beanName);
  8. }
  9. }

这段代码实际上是把bean放入到一级缓存,同时从二级缓存和三级缓存中移除,整体来看相当于将bean从二级缓存或三级缓存移入到一级缓存,所以使用"移入"这样一个词来描述这个过程。

声明结束之后来看四种情况下三级缓存的具体使用情况:

以上过程都是通过调试代码得出的,我们知道Spring容器的启动是从refresh()方法开始的,在refresh()方法中会调用finishBeanFactoryInitialization()方法,该方法会创建出所有非懒加载的bean,将这个方法作为入口进行调试,我制作了一张长图来展示完整的调用过程,如果感兴趣可以用同样的方式进行调试,由于图是在太长,所以请自行点开查看:spring创建bean流程

4.3.2 三级缓存使用过程中重要的三个方法

整个调用过程还是很复杂的,但是其中最重要的就只有三个方法,这三个方法中主要步骤我都进行了注释,由于三个方法都比较长,所以后面在贴源码的时候省略掉了中间那些对理解无关紧要的代码,比如异常处理等,如果想看完整的代码,建议直接看源码。一定要跟着源码调试一遍,调试是学习源码最好的方法。

第一个重要的方法是DefaultSingletonBeanRegistry类的getSingleton方法,这个方法是创建bean的入口,这个方法最终会调用到AbstractAutowireCapableBeanFactory类的doCreateBean方法来真正创建和装配bean,也就是第二个重要的方法。注意DefaultSingletonBeanRegistry类还有一个重载的getSingleton方法,这是第三个重要的方法(后面有展示),这两个方法的参数不同,功能也不一样,后一个getSingleton方法用来从缓存中查找bean。

  1. /**
  2. * DefaultSingletonBeanRegistry类的getSingleton方法,这个方法是创建bean的入口,后面还有一个getSingleton方法,两个方法的参数不同,功能也不一样,后一个getSingleton方法用来从缓存中查找bean
  3. */
  4. public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
  5. Object singletonObject = this.singletonObjects.get(beanName);
  6. if (singletonObject == null) {
  7. // 1、此处getObject方法最终会调用到AbstractAutowireCapableBeanFactory类的doCreateBean方法,也就是真正去创建一个bean的地方,也就是第二个重要的方法
  8. singletonObject = singletonFactory.getObject();
  9. newSingleton = true;
  10. if (newSingleton) {
  11. // 2、当bean创建完成之后将bean从三级缓存或二级缓存移入到一级缓存,整个创建过程就结束了
  12. addSingleton(beanName, singletonObject);
  13. }
  14. }
  15. return singletonObject;
  16. }

第二个重要的方法是AbstractAutowireCapableBeanFactory类的doCreateBean方法,这个方法用来真正实例化bean、处理依赖关系、增强bean的功能。处理依赖关系的时候会去查找它依赖的bean,所以最终又会调用到DefaultSingletonBeanRegistry类中用于查找bean的getSingleton方法,也就是第三个重要的方法。

  1. /**
  2. * AbstractAutowireCapableBeanFactory类的doCreateBean方法,这个方法用来真正实例化和装配bean
  3. */
  4. protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
  5. throws BeanCreationException {
  6. BeanWrapper instanceWrapper = null;
  7. ...
  8. if (instanceWrapper == null) {
  9. // 1、实例化一个bean
  10. instanceWrapper = createBeanInstance(beanName, mbd, args);
  11. }
  12. Object bean = instanceWrapper.getWrappedInstance();
  13. ...
  14. boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
  15. isSingletonCurrentlyInCreation(beanName));
  16. if (earlySingletonExposure) {
  17. // 2、将实例化的bean包装为ObjectFactory对象,放入到三级缓存singletonFactories中
  18. // 注意第二个参数是一个lambda表达式,实际上是ObjectFactory函数式接口,这个接口只有getObject()一个方法,用来返回bean,如果没有AOP,返回的就是上面实例化的bean对象,但如果有AOP,返回的将是代理对象
  19. // 由于是一个lambda表达式,所以getEarlyBeanReference()方法并不会立刻执行
  20. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  21. }
  22. Object exposedObject = bean;
  23. // 3、populateBean方法用来装配bean,也就是依赖注入,当发现依赖关系之后会去容器中查找依赖的bean,查找的过程最终会调用到DefaultSingletonBeanRegistry类中重载的的getSingleton方法,也就是第三个重要的方法
  24. populateBean(beanName, mbd, instanceWrapper);
  25. // 4、bean功能的增强(aware接口、bean后置处理器、初始化方法),AOP的代理对象在这里生成
  26. exposedObject = initializeBean(beanName, exposedObject, mbd);
  27. if (earlySingletonExposure) {
  28. // 从缓存中获取对象,注意第二参数是false,意味着只会从一级缓存和二级缓存获取,由于此时当前bean还没有创建完成,所以一级缓存必然没有当前bean,那么这次调用其实含义是从二级缓存中获取bean
  29. // 所以如果此时bean还在三级缓存中,这里返回值earlySingletonReference将是null
  30. // 但如果返回值不为null,说明bean从三级缓存移到了二级缓存,必然是循环依赖的场景,如果bean有AOP,这里earlySingletonReference将是代理后的bean;如果没有AOP,earlySingletonReference依然是原本的bean
  31. Object earlySingletonReference = getSingleton(beanName, false);
  32. if (earlySingletonReference != null) {
  33. if (exposedObject == bean) {
  34. exposedObject = earlySingletonReference;
  35. }
  36. ...
  37. }
  38. }
  39. ... // 省略了部分代码
  40. return exposedObject;
  41. }

第三个重要的方法是DefaultSingletonBeanRegistry类中重载的getSingleton方法,这个方法用来从缓存中查找bean。

  1. /**
  2. * DefaultSingletonBeanRegistry类中重载的getSingleton方法,这个方法用来从缓存中查找bean
  3. */
  4. // 注意第二个参数,如果第二个参数为false,意味着只会从前两级缓存中查找,不会去第三级缓存查找
  5. protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  6. // 从第一级缓存中查找
  7. Object singletonObject = this.singletonObjects.get(beanName);
  8. if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  9. // 从第二级缓存中查找
  10. singletonObject = this.earlySingletonObjects.get(beanName);
  11. if (singletonObject == null && allowEarlyReference) {
  12. synchronized (this.singletonObjects) {
  13. singletonObject = this.singletonObjects.get(beanName);
  14. if (singletonObject == null) {
  15. singletonObject = this.earlySingletonObjects.get(beanName);
  16. if (singletonObject == null) {
  17. // 从第三级缓存中查找,注意:从三级缓存中找到之后会移入到二级缓存
  18. ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
  19. if (singletonFactory != null) {
  20. singletonObject = singletonFactory.getObject();
  21. this.earlySingletonObjects.put(beanName, singletonObject);
  22. this.singletonFactories.remove(beanName);
  23. }
  24. }
  25. }
  26. }
  27. }
  28. }
  29. return singletonObject;
  30. }

4.3.3 为什么一定要三个层级的缓存

从上面使用三级缓存的过程中可以看到如果没有AOP,二级缓存earlySingletonObjects只有在有循环依赖的情况下才会使用,但是不知道大家有没有一个疑问,为什么一定要有这个二级缓存earlySingletonObjects呢?比如A依赖B,B依赖A的循环依赖场景,假如A和B都是set方法注入并且Spring只有一级缓存和三级缓存,Spring先实例化A,A实例化结束之后包装为BeanFactory对象放入三级缓存,然后装配A的时候发现A依赖B,就去尝试创建B,先实例化B,B实例化结束之后包装为BeanFactory对象放入到三级缓存中,然后装配B,发现B依赖A,A已经在三级缓存中了,直接将A从三级缓存中获取到完成B的装配,B创建完成,将B从三级缓存移入到一级缓存中;然后回过头来继续装配A,从一级缓存中获取到B完成A的装配,A创建完成,将A从三级缓存移入到一级缓存,A和B都创建完成了,这样一分析是不是发现似乎只有一级缓存和三级缓存也可以解决循环依赖的问题?

确实,如果仅仅只是为了处理循环依赖的情况,两个层级的缓存已经完全足够,根本不需要三个层级的缓存,使用三个层级的缓存也不是为了提升效率之类想当然的理由。
其实前面的描述中已经提示了答案,我在前面描述的时候都加了一个前提条件:"如果没有AOP",Spring一定要使用三级缓存的目的其实是为了处理有AOP情况下的循环依赖问题。注意,如果仅仅只有AOP而没有循环依赖,也不会用到二级缓存,一定是有循环依赖才需要用到二级缓存。

我们知道AOP的底层原理是动态代理,通过代理来对功能进行增强,所以如果有AOP最终使用的必然是代理对象,比如我们对A进行了AOP,那么最终注入到B的必然是A的代理对象。那么A的代理对象是什么时候生成的呢?如果没有循环依赖,AOP的代理对象是在Bean后置处理器的后方法中生成的,Spring执行Bean后置处理的后方法是在initializeBean方法中,而initializeBean方法是在populateBean方法的后面,populateBean方法用来处理依赖关系,所以没有循环依赖的情况下Spring是先进行依赖注入,然后再生成代理对象。

  1. if (instanceWrapper == null) {
  2. // 1、实例化一个bean
  3. instanceWrapper = createBeanInstance(beanName, mbd, args);
  4. }
  5. Object bean = instanceWrapper.getWrappedInstance();
  6. ...
  7. boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
  8. isSingletonCurrentlyInCreation(beanName));
  9. if (earlySingletonExposure) {
  10. // 2、将实例化的bean放入到三级缓存singletonFactories中,注意并不是直接把bean放入一个容器,第二个参数是一个lambda表达式,getEarlyBeanReference()方法并不会立刻执行
  11. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  12. }
  13. try {
  14. // 3、populateBean方法用来装配bean,也就是依赖注入
  15. populateBean(beanName, mbd, instanceWrapper);
  16. // 4、bean功能的增强(aware接口、bean后置处理器的前后方法、初始化方法)
  17. exposedObject = initializeBean(beanName, exposedObject, mbd);
  18. } catch (Throwable ex) {
  19. ...
  20. }

但假如有循环依赖的情况,比如A和B循环依赖,且A有AOP,A先实例化出来,执行到注释3的位置,发现A依赖B,就去尝试递归创建B,B先实例化,实例化之后执行到注释3的位置,发现B依赖A,这个时候B需要的是A的代理对象,但是A的代理对象正常情况下要在在注释4的位置才生成,A还没执行到注释4的位置呢,那怎么办呢,只能是B在注释3的位置也就是处理依赖关系的时候就提前把A的代理对象给生成出来,要怎么提前生成出来呢?B是从三级缓存中获取A,还记得第三级缓存是什么吗?对,是ObjectFactory接口,所以B从三级缓存中获取A是先获取到ObjectFactory对象,然后调用ObjectFactory对象的getObject方法来获取A,getObject方法的具体实现是getEarlyBeanReference()方法:

  1. // 将实例化的bean包装为ObjectFactory对象放入三级缓存,这里是一个lambda表达式,表达式的内容就是getObject方法的具体实现
  2. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

我们来看getEarlyBeanReference()方法的逻辑:

  1. /**
  2. * AbstractAutowireCapableBeanFactory中的getEarlyBeanReference方法
  3. */
  4. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  5. Object exposedObject = bean;
  6. // Spring使用AnnotationAwareAspectJAutoProxyCreator这个Bean后置处理来处理AOP,AnnotationAwareAspectJAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor接口,所以这里if条件就是判断当前bean是否需要进行AOP处理
  7. if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
  8. for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
  9. // 最终会调用到AbstractAutoProxyCreator类中的getEarlyBeanReference方法,生成代理对象
  10. exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
  11. }
  12. }
  13. return exposedObject;
  14. }
  15. /**
  16. * AbstractAutoProxyCreator类中的getEarlyBeanReference方法
  17. */
  18. @Override
  19. public Object getEarlyBeanReference(Object bean, String beanName) {
  20. Object cacheKey = getCacheKey(bean.getClass(), beanName);
  21. // 注意earlyProxyReferences存放的是原来的bean,而不是代理过后的bean
  22. this.earlyProxyReferences.put(cacheKey, bean);
  23. // 生成代理对象
  24. return wrapIfNecessary(bean, beanName, cacheKey);
  25. }

可以看到在getEarlyBeanReference()方法中判断如果当前bean有AOP,那么就会生成代理对象,否则就直接返回原对象。回到之前的场景,B从三级缓存中先获取到ObjectFactory对象,然后调用ObjectFactory对象的getObject()方法,由于A有AOP,所以getObject()方法返回的是代理对象,正常执行下去b会用代理对象完成依赖注入,然后B的创建过程完成,跳出递归后会继续执行A的流程,将B注入A,完成依赖关系的装配,然后执行到注释4的位置,也就是initializeBean()方法,这个方法正常情况下会在bean后置处理器的后方法中生成代理对象,但是代理对象已经提前生成过了,如果再生成一个代理对象就不对了,所以这里不会再生成代理对象,而是返回原对象,看源码:

  1. /**
  2. * AbstractAutoProxyCreator类中的bean后置处理器中的后方法
  3. */
  4. @Override
  5. public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
  6. if (bean != null) {
  7. Object cacheKey = getCacheKey(bean.getClass(), beanName);
  8. // 有AOP的情况下会调用getEarlyBeanReference方法,在getEarlyBeanReference方法中会将原本的bean放入到earlyProxyReferences中,所以remove出来的bean和参数中的bean是同一个,条件不成立,所以不会再生成一个新的代理对象
  9. // 如果没有aop,就不会调用getEarlyBeanReference方法,earlyProxyReferences是空的,remove出来是null,条件成立,就会生成一个代理对象
  10. if (this.earlyProxyReferences.remove(cacheKey) != bean) {
  11. return wrapIfNecessary(bean, beanName, cacheKey);
  12. }
  13. }
  14. return bean;
  15. }

也就是说调用initializeBean()方法返回的仍然是原bean,但是最终需要的又是代理过的对象,那怎么获取代理过的对象呢?还得从缓存中获取,此时一级缓存中还没有,三级缓存中存储的仍然是原本的bean,如果总共只有两级缓存,是没办法获取到代理后的对象的,所以就需要一个另外的缓存来存储代理过后的对象,也就是二级缓存。实际上B从三级缓存中获取ObjectFactory对象,然后调用getObject方法获取到A的代理对象之后,就将代理对象放入到了二级缓存,所以之后只需要从二级缓存中来获取到代理后的bean就可以了:

  1. // 4、如果有AOP,initializeBean不会再生成代理对象,所以exposedObject仍然是原本的bean
  2. exposedObject = initializeBean(beanName, exposedObject, mbd);
  3. if (earlySingletonExposure) {
  4. // 从缓存中获取bean,注意这里第二个参数是false,意味着只会从一级和二级缓存中获取bean,前面处理依赖关系的时候已经将代理对象放入到了二级缓存,所以这里实际上就是从二级缓存中把代理过后的对象给取出来
  5. Object earlySingletonReference = getSingleton(beanName, false);
  6. if (earlySingletonReference != null) {
  7. if (exposedObject == bean) {
  8. // 将代理过后的对象赋给exposedObject
  9. exposedObject = earlySingletonReference;
  10. }
  11. ...
  12. }
  13. }
  14. ...
  15. return exposedObject;

最后将这个返回exposedObject对象放入到一级缓存(单例池)中,bean A就创建结束了。

总结一下,在有循环依赖并且第一个bean有AOP的情况下,需要将第一个bean提前暴露出来,并且提前暴露出来的还要是代理过后的bean,这个暴露出来的bean必须有一个地方存储,由于第三级缓存中存放的是原始的bean,而且bean没有装配完成的情况下也不能放入到一级缓存中,所以必须要要有二级缓存来存放提前暴露出来的代理过后的bean,这才是必须使用三个缓存的原因。

所以Spring使用三级缓存不是为了什么提升效率之类想当然的理由,而是有明确目的的。但其实看了源码就知道完成同样的功能用两级缓存也不是绝对不可以,不过那就是另一套实现方式了,任何时候实现相同的功能都可以有不同的方式,没有什么是绝对的,最终经受考验的是哪种方式效率更高、更容易让人理解。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注