Spring 源码阅读 21:循环依赖和三级缓存

这是我参与「掘金日新计划 · 8 月更文挑战」的第33天,点击查看活动详情

基于 Spring Framework v5.2.6.RELEASE

接上篇:Spring 源码阅读 20:获取 Bean 的规范名称

前情提要

上一篇介绍了获取 Bean 实例对象的第一步,根据给定的name获取 Bean 的规范名称。给定的name可能是 Bean 的别名或者 FactoryBean 的逆向引用名称,获取到的规范名称是 Bean 在容器中的唯一标识符。

本文接着介绍 AbstractBeanFactory 中doGetBean方法的下一行关键代码。

Object sharedInstance = getSingleton(beanName);
复制代码

从缓存中获取 Bean 实例对象

直接进入getSingleton方法的源码。

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)
@Override
@Nullable
public Object getSingleton(String beanName) {
   return getSingleton(beanName, true);
}

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
         singletonObject = this.earlySingletonObjects.get(beanName);
         if (singletonObject == null && allowEarlyReference) {
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
               singletonObject = singletonFactory.getObject();
               this.earlySingletonObjects.put(beanName, singletonObject);
               this.singletonFactories.remove(beanName);
            }
         }
      }
   }
   return singletonObject;
}
复制代码

这个方法的作用是根据beanName从容器缓存中获取对应的 Bean 实例对象。此处有两个需要注意的地方:

  1. 调用方法传入的参数,是通过transformedBeanName方法获取的规范的 Bean 名称。
  2. 通过调用重载方法,给了allowEarlyReference参数一个默认值true

然后查看方法体中的代码,首先,大概浏览一下方法体的整个流程,可以发现,在容器中有三个集合,Spring 会使用beanName依次从这三个集合中查找 Bean 的实例对象,这三个集合,我们可以将之看作缓存(实际上就是缓存),在 DefaultSingletonBeanRegistry 中可以找到创建它们的代码:

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
复制代码

这三个 Map 集合的 Key 都是 Bean 的名称,Value 分别是单例 Bean 的实例、早期单例 Bean 的实例、单例 Bean 工厂。

getSingleton方法的开头,会尝试从singletonObjects集合获取 Bean 的实例,获取到就得到了想要的结果,直接返回。

当没有获取到时,如果isSingletonCurrentlyInCreation(beanName)的结果为true,那么尝试从下一个缓存 Map 中获取。isSingletonCurrentlyInCreation方法的源码如下:

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#isSingletonCurrentlyInCreation
/**
* Return whether the specified singleton bean is currently in creation
* (within the entire factory).
*  @param  beanName the name of the bean
*/
public boolean isSingletonCurrentlyInCreation(String beanName) {
   return this.singletonsCurrentlyInCreation.contains(beanName);
}
复制代码

这里还是一个集合,根据注释可以知道,这个方法的作用是判断一个单例 Bean 是不是正在被创建,也就是说这个集合中保存了所有正在被创建的 Bean 的名称。

返回到getSingleton方法中,如果从singletonObjects中获取不到,并且这个 Bean 正在被创建,那么就尝试从earlySingletonObjects集合中获取。这里存放的是早期单例 Bean 的实例对象,这些 Bean 实例被创建好,但是还没有进行完初始化,就会被放在这个集合当中,可以提前被获取到了,如果这里获取到了目标 Bean 实例,那么也会作为方法的结果返回。

如果还没有获取到,并且allowEarlyReferencetrue,那么会接着尝试从singletonFactories中获取,这里缓存的是创建早期单例的 Bean 工厂,如果从singletonFactories集合中获取到了这个工厂对象,那么就调用它的getObject方法,将早期 Bean 创建出来,并作为结果返回。

另外,如果早期 Bean 是在这里创建的,那么还需要把创建好的早期 Bean 添加到earlySingletonObjects中,并把工厂从singletonFactories移除掉,因为再次之后,它已经不会被用到了。

你可能会问了:何必呢?直接把 Bean 完整地初始化好放在一个缓存中获取,获取不到就创建新的,不就行了吗,为什么还需要早期实例和工厂实例这种中间状态呢?

这就要讲到循环依赖的问题。

循环依赖

假设接下来我们提到的类型,都是用来创建单例 Bean 的类型,思考这样三种情况:

  1. ClassA 中有一个属性的类型是 ClassA
  2. ClassA 中有一个属性的类型是 ClassB,同时,ClassB 中有一个属性的类型是 ClassA
  3. ClassA 中有一个属性的类型是 ClassB,ClassB 中有一个属性的类型是 ClassC,同时,ClassC 中有一个属性的类型是 ClassA

在之前的 Spring 源码阅读 19:如何 get 到一个 Bean? 一文中,曾经介绍过,完成一个 Bean 的初始化之前,必须要先初始化其各个属性值对应的 Bean。当出现以上三种情况或者类似的形成循环依赖关系的情况,就会出现问题。

以第 3 种情况为例,要完成 ClassA 的初始化,就要先初始化 ClassB,那么就要先初始化 ClassC,初始化 ClassC 又需要完成 ClassA 的初始化。这样就变成了「鸡生蛋蛋生鸡」的问题。

这种情况在实际的开发中,难以完全避免,因此,这是 Spring 必须要解决的问题。

三级缓存

Spring 解决这个问题的办法就是:在一个 Bean 还没有完成初始化的时候,就暴露它的引用。这里就需要用到前面介绍过的三个缓存 Map,也叫三级缓存

以刚刚描述的情况为例,当 ClassA 需要需要一个 ClassB 的单例 Bean 作为属性填充的时候,先创建一个早期的 ClassB 的 Bean,此时,ClassB 刚刚被创建出来,还没有进行属性填充等初始化的流程,就将它放在earlySingletonObjects中,这样,ClassA 的 Bean 实例就可以使用这个 ClassB 的早期实例进行属性填充,ClassB 的早期实例,可以再次之后再进行初始化,这样就不会因为循环依赖关系,导致无法初始化这三个 Bean。

暂时无法在飞书文档外展示此内容

上图是 Bean 的初始化流程和从各缓存中获取 Bean 实例的流程,详细的过程,会在之后分析到第一次获取一个没有被创建的 Bean 的流程时,再通过代码做深入分析。

局限

不过,Spring 对这个问题的解决还存在局限性。

在 Spring 中,可以通过 setter 方法和构造器将一个 Bean 实例作为属性值注入给另一个 Bean 实例。根据 Spring 通过三级缓存解决循环依赖问题的原理,在一个 Bean 被放入缓存之前,至少需要先通过反射把对象创建出来。但是,如果一个一个属性只能通过构造器注入,那么就无法在注入之前,完成 Bean 对象的创建,也就无法解决循环依赖的问题。

因此,Spring 只能解决通过 setter 注入的循环依赖问题。

总结

本文分析了 Spring 容器从缓存中获取单例 Bean 的原理,主要涉及到了 Spring 为解决循环依赖问题,而设计的三级缓存机制。通过getSingleton方法从缓存中获取 Bean 实例对象,可能获取到,也可能因为 Bean 还完全没有开始创建而获取不到。下一篇,我们接着分析,如果这里获取到了单例 Bean,接下来还需要做哪些处理。

猜你喜欢

转载自juejin.im/post/7136589270583607309