Spring 循环依赖【1】原理介绍

97

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

前言

这里提前了解一些AOP的相关内容。

AOP是在初始化后做的:(applyBPAfterInitialization)

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
      throws BeansException {

   Object result = existingBean;
   for (BeanPostProcessor processor : getBeanPostProcessors()) {
      Object current = processor.postProcessAfterInitialization(result, beanName);
      if (current == null) {
         return result;
      }
      result = current;
   }
   return result;
}

这个功能需要在启动配置类上加上@EnableAspectJAutoProxy,但SpringBoot中默认就启用了。

加上注解之后,就会把AnnotationAwareAspectJAutoProxyCreator这个类的PP代理,提前注册到beanFactory中。

循环依赖问题,以及如何处理

什么是循环依赖?

  • 简单来说,就是A中有B,B中有A。
  • 简单的循环依赖并不是一个困难的问题,Spring中的循环依赖则并不是一个简单的工作。

Spring中的循环依赖是如何产生的?

  • A中autowired了一个B,B中也autowired了一个A。

  • 假设先是A来执行生命周期:

    • 实例化A,得到一个对象
    • 填充A中的B属性(根据B属性类型,名称去查找),去单例池中获取B(getSingleton()),获取不到就创建B【1】
    • 填充其他属性,初始化前BP,初始化,初始化后,放入单例池
  • 那么B的生命周期:

    • 实例化B,得到一个对象
    • 填充B中的A属性(根据A属性类型,名称去查找),去单例池中获取A(getSingleton()),获取不到就创建A【2】
    • 填充其他属性,初始化前BP,初始化,初始化后,放入单例池
  • 不难发现,由于放入单例池是在填充属性完毕之后才进行的,因此流程1和流程2,如果没有别的特殊处理,那么就会陷入死循环了,因此这篇文章就是来探究Spring中是如何解决这个问题的。

这里的解决方法就是三级缓存:

  • singletonObjects(单例池)

  • earlySingletonObjects(关键的解决方式)

  • singletonFactories

    以及以下两个容器:

  • singletonsCurrentlyInCreation

  • earlyProxyReferences

打破循环的关键,就是新增一个缓存,在步骤1/2之前,先把实例化但并没有填充属性的对象,放到这个缓存里。(我们这里称为缓存A)

那么上述的流程,就变成了如下过程,注意加粗的不同部分:

假设先是A来执行生命周期:

  • 实例化A,得到一个对象,并把这个还没填充属性的A放到缓存A中
  • 填充A中的B属性(根据B属性类型,名称去查找),去单例池中获取B(getSingleton()),获取不到就取缓存A中的,还获取不到就去创建B。
  • 填充其他属性,初始化前BP,初始化,初始化后,放入单例池

接下来进入到B的生命周期:

  • 实例化B,得到一个对象,并把这个还没填充属性的A放到缓存A中
  • 填充B中的A属性(根据A属性类型,名称去查找),去单例池中获取A(getSingleton()),获取不到就取缓存A中的,此时能获取到了,就赋值。【3】
  • 填充其他属性,初始化前BP,初始化,初始化后,放入单例池。

根据这个思路,这样子就解决了上面的循环依赖的问题了。

这里【3】中,A此时的数据是未完全赋值的,因此从【3】中取的对象A,和beanA必须是同一个对象,这样才能让【3】中写入的A,最后属性是完整的。【3:1】

根据这个思路,除了原来已有的单例池,只要多增加一个缓存即可,但是Spring中却有三级缓存多了一个,这个缓存是做什么的呢?

  • 多出来的这级缓存,是和代理相关的。

我们接着看A的生命周期:

  • 实例化A,得到一个对象,并把这个还没填充属性的A放到缓存A中
  • 填充A中的B属性(根据B属性类型,名称去查找),去单例池中获取B(getSingleton()),获取不到就取缓存A中的,还获取不到就去创建B。
  • 填充其他属性
  • 初始化前BP
  • 初始化
  • 初始化后 - > 在这里,我们会进行AOP的处理,如果有就会生成代理对象。
  • 放入单例池 - > 在这里,如果上面AOP处理了,这里就是代理对象了。【4】

看,这里【3:1】和【4】就冲突了:【4】放入单例池的对象,和赋给B的对象就不同了。

如何解决这个问题?有一个思路是这样的:

  • 实例化A,得到一个对象,在这里直接进行AOP的代理,并把这个还没填充属性的A放到缓存A中。
  • 填充A中的B属性(根据B属性类型,名称去查找),去单例池中获取B(getSingleton()),获取不到就取缓存A中的,还获取不到就去创建B。
  • 填充其他属性
  • 初始化前BP
  • 初始化
  • 初始化后 - >这里不用再来了。
  • 放入单例池 - > 在这里,如果上面AOP处理了,这里就是代理对象了。【4】

如果这种思路,其实也是不需要多一级缓存的。但这里不另加一层缓存,和设计上有冲突:

  • AOP实际上是填充属性后才进行的,需要在填充属性之前就做AOP?

到这里我们知道了:

  • 如果要解决循环依赖的问题,那么也必须要提前进行AOP,否则会出现注入对象不一致的情况。

  • 因此,Spring采取了这种设计:A出现循环依赖的情况下,进行提前AOP(实例化之后就进行AOP)

那么,如何在实例化后,就能判断A出现了循环依赖?

  • 如果需要判断循环依赖,在B的生命周期【3】中,是比较合适的判断点,而一开始我们死循环的进入点,也是这里。 并且,我们在生命周期一开始的时候,就把当前正在创建的bean的名称,存到singletonsCurrentlyInCreation这个容器里。现在流程就变成了:

A的:

  • beanNameAsingletonsCurrentlyInCreation这个容器里。

  • 实例化A,得到一个对象,并把这个还没填充属性的A放到缓存A中。

  • 填充A中的B属性(根据B属性类型,名称去查找),去单例池中获取B(getSingleton()),获取不到就取缓存A中的,还获取不到就去创建B。

接下来进入到B的生命周期:

  • beanNameBsingletonsCurrentlyInCreation这个容器里。

  • 实例化B,得到一个对象,并把这个还没填充属性的B放到缓存A中

  • 填充B中的A属性(根据A属性类型,名称去查找),

    • 这里就能发现循环依赖了:singletonsCurrentlyInCreation里有A!这里A就出现了循环依赖,而且比较好判断;根据上面的分析,这里我们就会进行对A的判断:
      • 如果A需要进行AOP,则进行AOP获取代理对象;
      • 如果不需要AOP:去单例池中获取A(getSingleton()),获取不到就取缓存A中的,此时能获取到了,就赋值。
  • 填充其他属性,初始化前BP,初始化,初始化后,放入单例池。

回到A生命周期

  • 填充其他属性---【5】,初始化前BP,初始化,初始化后,放入单例池。

不妨把问题考虑得更复杂一些,【5】之中:

此时A中有个C,C中也有个A,此时A需要填充C属性,就进到C的bean生命周期:

  • .......

  • .......

  • 填充C中的A属性:(与上面相同)

    • 此时我们就能了解到为什么需要多一个缓存了:

      • 如果我们没有地方来记录生成的A的AOP代理对象,那么这里就违反了单例的思想了:

        B和C中的A的代理对象不是同一个!

        因此,B生成的A的AOP对象,就需要存到一个用来存储生成的代理对象的池里,这样在后面同样发生依赖注入的点上,才能使得这些AOP的代理对象都是同一个:这个池子就是earlySingletonObjects

        但是,AOP需要的是代理,代理中需要原始对象作为target,这也就意味着这里其实也需要原始对象A

        现在问题就变成了:我们需要从缓存A中,尝试:

        • 获取原始bean,或者获取代理bean。
        • 如果为了获取原始bean或者代理bean,把原始bean作为参数一直往下传递只为了在这个地方判断是否需要代理,会比较麻烦而且不必要,因此我们希望:
        • 在缓存A中取出来的,可以智能一点,因此缓存A我们就用来存这个相关的内容:返回原始对象或者代理对象,这里就是Spring中的singletonFactories:<BeanNameA,工厂方法>
        • 这里的工厂方法,代表的就是生成原始对象,或生成代理对象的方法。