Spring需要三个级别缓存解决循环依赖原理解析

Java
322
0
0
2023-03-23
标签   Spring
目录
  • 导学
  • getBean
  • getSingleton
  • 三个级别缓存作用
  • 循环依赖示例
  • 循环依赖执行流程
  • 思考:为什么需要三个级别的缓存来解决循环依赖
  • 总结

导学


Spring的三级缓存是绕不过去的一个坎儿。面试也经常被问到。而网文大多都在讲Spring三级缓存的用途,而分析的很好的很少。

接下来整篇文章分析下:Spring为什么要使用三级缓存解决循环依赖,而不是二级缓存或是一级缓存

先来明白一下Spring实例化一个Bean的过程中几个重要概念

getBean

通过getBean方法获取单例Bean,每个bean的创建都是从该方法开始的。

getSingleton

Spring中最最重要的核心逻辑,没有之一。该方法依次从一级缓存、二级缓存、三级缓存中获取单例bean对象。

注意:如果从三级缓存中获取到对象之后,就会被立即移动到二级缓存。下面是源码

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 // Quick check for existing instance without full singleton lock
 Object singletonObject = this.singletonObjects.get(beanName);
 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  singletonObject = this.earlySingletonObjects.get(beanName);
  if (singletonObject == null && allowEarlyReference) {
   synchronized (this.singletonObjects) {
    // Consistent creation of early reference within full singleton lock
    singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
     singletonObject = this.earlySingletonObjects.get(beanName);
     if (singletonObject == null) {
      ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
      if (singletonFactory != null) {
       singletonObject = singletonFactory.getObject();
       this.earlySingletonObjects.put(beanName, singletonObject);
       this.singletonFactories.remove(beanName);
      }
     }
    }
   }
  }
 }
 return singletonObject;
}

三个级别缓存作用

一级缓存:存放最终的单例Bean,里面所有的Bean都是直接能使用的。(这个大家一定都明白就不多说了)

三级缓存:存放一个工厂对象,这个工厂对象有一个getObject方法。工厂一般由lambda表达式组成,在工厂中主要完成了对Aop的代理。执行一些Bean的扩展逻辑。

二级缓存:没错,先介绍三级缓存是有目的的。二级缓存只有在getSingleton(前文提到)方法中,才会把三级缓存获得的对象存入二级缓存。并且删除三级缓存中的工厂对象。至于为什么总结里会说。

循环依赖示例

废话不多说,先给两个类AooBoo,这两个类形成循环依赖,代码简单如下。

@Component
public class Aoo {
  @Autowired
  private Boo boo;
}
@Component
public class Boo {
  @Autowired
  private Aoo aoo;
}

循环依赖执行流程

首先Spring会扫描指定的包,把所有标注@Component注解的类顺序的实例化。Spring启动时,容器中没有任何bean(当然是有一些内部bean的,但是这样说便于我们理解)

下面来理解下Spring实例化Bean的顺序,

  • 启动时
  • 检测到Aoo开始实例化Aoo对象
  • 调用doGetBean方法实例化Aoo
  • 调用getSingleton方法,试图从三级缓存中依次获取bean。但是第一次肯定都为空
Object sharedInstance = getSingleton(beanName);
  • 因为缓存为空,所以程序继续往下走
sharedInstance = getSingleton(beanName, () -> {
  return createBean(beanName, mbd, args);
});

程序调用getSingleton方法创建Aoo对象,并且该方法传入一个lambda表达式,这个表达式后面是会被放入三级缓存的。

  • 我们来看下getSingleton方法
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    try {
      singletonObject = singletonFactory.getObject();
      newSingleton = true;
    }
    if (newSingleton) {
      addSingleton(beanName, singletonObject);
    }
    return singletonObject;
}

可以看到该方法中试图通过第二个参数,也就是上一步的lambda表达式创建Aoo对象,并且创建完成后把Aoo对象存入一级缓存。 此时一级缓存何时加入的我们清楚了。

  • 接下来我们来看下这个lambda表达式,也即是createBean方法。而它又调用了doCreateBean方法。
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
   // 省略无关代码
   return doCreateBean(beanName, mbdToUse, args);
}
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
 // 创建一个Aoo的包装对象。此时Aoo是一个空壳对象其中的所有属性均为空
 BeanWrapper instanceWrapper = null;
 Object bean = instanceWrapper.getWrappedInstance();
 // 判断是否提前暴露(其实就是循环依赖了)
 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
 if (earlySingletonExposure) {
  // 把Aoo对象加入三级缓存!!!
  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
 }
 Object exposedObject = bean;
 try {
  // 给Aoo对象注入属性
  populateBean(beanName, mbd, instanceWrapper);
  // 初始化Aoo(一些扩展点,与循环依赖关系不大)
  exposedObject = initializeBean(beanName, exposedObject, mbd);
 }
 if (earlySingletonExposure) {
   // 如果提前暴露对象,则尝试从缓存中获取。
  Object earlySingletonReference = getSingleton(beanName, false);
  if (earlySingletonReference != null) {
    exposedObject = earlySingletonReference;
  }
 }
 return exposedObject;
}

第一步:创建一个Aoo的空壳对象,此时Aoo其中的属性还没有值。

第二步:把工厂存入三级缓存,缓存的key就是对象的名称aoo,而value是一个工厂对象的lambda表达式,这个工厂对象会返回Aoo对象。这个工厂会执行getEarlyBeanReference方法,该方法中会完成AOP动态代理。需要重点说明一下,从三级缓存中取出的对象每次都是不一样的。因为它是每次代理生成的。

第三步:执行populateBean方法。为Aoo注入属性,Aoo只有一个属性Boo。Spring在注入Boo属性的时候发现容器没有Boo对象。

第四步:从这一步就循环到文章的最开始了,接下来我用小写数字表示的步骤表示上文提到的步骤。回到3调用doGetBean方法实例化Boo。

第五步:执行4调用getSingleton方法,试图从三级缓存中依次获取bean。前面说过第一次肯定都为空,这次是第一次获取Boo对象肯定也还是空的。

第六步:依次执行567,在7中会把Boo对象存入三级缓存中。 没错,任何一个Bean都会先放到三级缓存中。此时三级缓存中有两个Bean了,分别是Aoo和Boo

第七步:在7中的populateBean方法开始给Boo注入属性了,Boo只有一个属性Aoo,Spring在注入Aoo属性是会从容器中获取,也就是调用getBean方法,此时发现三级缓存中有Aoo。就会从三级缓存中获取Aoo对象并给Boo的这个属性赋值。同时也会把Aoo对象从三级缓存移动到二级缓存中。此时一级缓存为空、二级缓存中有Aoo对象、三级缓存中有Boo对象。此时Aoo、Boo的状态还都是创建的过程中。

第八步:Boo的属性已经完成,回到上面的6它会把Boo对象添加到一级缓存,并从三级缓存中移除(这儿没二级缓存啥事儿嘿嘿嘿)。 此时一级缓存中有一个对象Boo、二级缓存中有Aoo对象、三级缓存为空。

第九步:既然Boo都已经在一级缓存当中了,那么接着第三步来说,此时Aoo的属性Boo也完成了赋值。此时Aoo也是一个完整对象了。但它此刻还在二级缓存当中。

第十步:在7执行完毕之后,回归到6的代码,执行addSingleton方法把Aoo从二级缓存移动到一级缓存当中。至此,依赖注入完毕。一级缓存中有Aoo对象和Boo对象。二级、三级缓存为空。

思考:为什么需要三个级别的缓存来解决循环依赖

现在来思考一下为什么一定要是三个级别的缓存呢?我们来删除二级缓存后看这个问题。下面我们就使用只有一级缓存和三级缓存这2个缓存来看下循环依赖的问题能不能解决。

还是Aoo和Boo两个类循环依赖

  • Spring启动
  • 从一级缓存和三级缓存中获取Aoo,缓存中没有,则创建
  • 创建Aoo的空壳对象,并把它和工厂对象放入三级缓存中。
  • 对Aoo进行属性注入,发现Boo即不在一级缓存,也不在三级缓存。只能创建了
  • 创建Boo对象
  • 对Boo进行属性注入,发现三级缓存中有Aoo对象,直接从三级缓存中获取。
  • Boo对象属性装配完成,把它从三级缓存移到一级缓存。
  • Aoo对象属性装配完成,此时从三级缓存中移到一级缓存。

乍一看没啥问题是不是,其实不是的。问题出在第6步,和第8步。通过前面的讲解,一定要了解到,三级缓存中每次返回的对象都不一样。所以第6步和第8步如果都从三级缓存中获取Aoo对象, 这两步中的Aoo对象不是同一个,Spring中的Aoo对象和Boo对象就会使这个样子

总结

首先不是说非要三级缓存机制才能解决循环依赖,一级缓存同样可以解决,把三级缓存代码平铺化就好了嘛,或者使用JVM指令,字节码等技术完成循环依赖,但你想一下,那样的话代码的可读性必然很低。所以第一个原因就是使用三级缓存解决循环依赖使得代码可读性非常好。

第二个原因是三级缓存中的工厂,每次getObject方法返回的实例不是同一个对象,所以需要二级缓存来缓存一下三级缓存生成的bean,这样就保证了两个类的属性是环形依赖,不会破坏循环依赖。