再读Spring源码之一 Spring如何对付循环引用

  1. Bean循环依赖
  2. Spring处理策略
  3. 源码实现
    1. 调用时序图
    2. 检查缓存的单例
    3. 检查prototype循环依赖
    4. 构建不同类型的实例
    5. 单例对象的早期引用
  4. 总结

关于Spring如何解决循环依赖这个问题,以前写过一篇文章Spring源码分析3-对Bean循环依赖的处理。但是如今回头看,写得还是略粗糙,而且细节也忘得差不多了。所以决定再写一写,重新理一遍思路。

Bean循环依赖

spring框架的bean可以互相依赖,因此,可能存在循环依赖的情况,例如:

    <!--构造器循环依赖(单例)-->
    <bean id = "testA" class="org.demo.circle.TestA">
        <constructor-arg index = "0" ref="testB"/>
    </bean>

    <bean id = "testB" class="org.demo.circle.TestB">
        <constructor-arg index = "0" ref="testC"/>
    </bean>

    <bean id = "testC" class="org.demo.circle.TestC">
        <constructor-arg index = "0" ref="testA"/>
    </bean>

    <!--构造器循环依赖(prototype)-->
    <bean id = "testA0" class="org.demo.circle.TestA" scope="prototype">
        <constructor-arg index = "0" ref="testB0"/>
    </bean>

    <bean id = "testB0" class="org.demo.circle.TestB" scope="prototype">
        <constructor-arg index = "0" ref="testC0"/>
    </bean>

    <bean id = "testC0" class="org.demo.circle.TestC" scope="prototype">
        <constructor-arg index = "0" ref="testA0"/>
    </bean>

    <!--属性注入循环依赖(单例)-->
    <bean id = "testA1" class="org.demo.circle.TestA">
        <property name="testB" ref = "testB1"></property>
    </bean>

    <bean id = "testB1" class="org.demo.circle.TestB">
        <property name="testC" ref = "testC1"></property>
    </bean>

    <bean id = "testC1" class="org.demo.circle.TestC">
        <property name="testA" ref = "testA1"></property>
    </bean>

    <!--属性注入循环依赖(prototype)-->
    <bean id = "testA2" class="org.demo.circle.TestA" scope="prototype">
        <property name="testB" ref = "testB2"></property>
    </bean>

    <bean id = "testB2" class="org.demo.circle.TestB" scope="prototype">
        <property name="testC" ref = "testC2"></property>
    </bean>

    <bean id = "testC2" class="org.demo.circle.TestC" scope="prototype">
        <property name="testA" ref = "testA2"></property>
    </bean>

如上配置文件,包含常见的几种循环依赖,可以归类为以下两大类:

  • 通过构造器注入形成循环依赖:

    • testA –> testB –> testC –> testA –> …
    • testA0 –> testB0 –> testC0 –> testA0 –> …
  • 通过属性注入形成循环依赖:

    • testA1 –> testB1 –> testC1 –> testA1 –> …
    • testA2 –> testB2 –> testC2 –> testA2 –> …

按照bean的类型又可以细分为单例和prototype两种(其他scope类型不常用暂不讨论)

本文主要探讨spring对这几种循环依赖的处理策略和原理。

Spring处理策略

先来说结论,通过测试用例我们可以简单验证以上几种循环依赖的处理策略:


  /**
     * 测试构造器注入循环依赖(单例)
     */
    @Test(expected = BeanCreationException.class)
    public void testCircleRefByConstructor() {
        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("circle.xml"));
        TestA testA = (TestA) beanFactory.getBean("testA");
    }

    /**
     * 测试构造器注入循环依赖(prototype类型)
     */
    @Test(expected = BeanCreationException.class)
    public void testCircleRefByConstructorForPrototype() {
        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("circle.xml"));
        TestA testA = (TestA) beanFactory.getBean("testA0");
    }

    /**
     * 测试属性注入循环依赖(单例)
     */
    @Test
    public void testCircleRefBySetter() {
        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("circle.xml"));
        TestA testA = (TestA) beanFactory.getBean("testA1");
    }


    /**
     * 测试的属性注入循环依赖(prototype类型)
     */
    @Test(expected = BeanCreationException.class)
    public void testPrototypeRefBySetter() {
        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("circle.xml"));
        TestA testA = (TestA) beanFactory.getBean("testA2");
    }

本文的测试用例代码可以移步spring-learn仓库

测试结果表明:只有单例模式下通过属性注入才能够实现循环依赖,其他情况下都将抛出以下异常BeanCreationException。

源码实现

调用时序图

我们从上边的测试用例出发分析源码实现,主要涉及的类包括XmlBeanFactory及其父类AbstractBeanFactory,和DefaultSingletonBeanRegistry以及它的父类AbstractAutowireCapableBeanFactory。核心的方法如下时序图:

spring-bean-加载时序图.png

读者可以沿着这个时序图的思路去阅读下bean初始化构建的源码。

检查缓存的单例

我们直接从AbstractBeanFactory#doGetBean开始深入。这个方法一开始先调用getSingleton(beanName)检查缓存里是否有名为name的单例Bean缓存。

protected <T> T doGetBean( String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
            throws BeansException {
      
  String beanName = transformedBeanName(name);
  Object bean;

  // 获取已经注册的单例bean实例
  Object sharedInstance = getSingleton(beanName);

  // ...

}

注意getSingleton(beanName)默认传入的allowEarlyReference参数为true。EarlyReference(早期引用)其实就是spring将还没有初始化完毕的单例bean放进缓存的bean引用,当发生循环依赖的时候,可以直接引用这个早期引用,而不会发生死循环不断地进行bean初始化。

@Override
@Nullable
public Object getSingleton(String beanName) {
  return getSingleton(beanName, true);
}

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 从singletonObjects获取已经初始化完毕的单例对象
  Object singletonObject = this.singletonObjects.get(beanName);
  // 判断该bean是不是正在初始化的单例bean
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    synchronized (this.singletonObjects) {
      // 从earlySingletonObjects中获取单例bean
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
        // 从singletonFactories缓存里获取单例工厂对象
        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
          // 通过单例工厂对象中获取早期单例bean对象,并缓存到earlySingletonObjects中
          singletonObject = singletonFactory.getObject();
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}

public boolean isSingletonCurrentlyInCreation(String beanName) {
  return this.singletonsCurrentlyInCreation.contains(beanName);
}

总结下,getSingleton的流程就是:

  • 如果该单例已经初始化完毕,直接返回;
  • 如果是在建的单例,则尝试返回单例bean早期引用(也就是尚未构建完的半成品bean引用);
  • 如果有单例工厂对象,则使用该对象获取单例bean早期引用,加进缓存并返回;

这里先记住这个几个缓存很重要,尤其关注singletonFactories,下文会用到它。

检查prototype循环依赖

上述检查单例如果返回null,则会进入Prototype的循环依赖检测:

Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
  // ...
} else {
  // prototype循环依赖检测
  if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
  }
  // ...
}         

这里检查到该bean是prototype,而且处于构建中状态,则意味着形成了循环依赖,直接抛异常终止初始化过程。

protected boolean isPrototypeCurrentlyInCreation(String beanName) {
  Object curVal = this.prototypesCurrentlyInCreation.get();
  return (curVal != null &&
      (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}

检查方法很简单,通过名字相同来判断是否为同一个bean。注意因为是prototype,所以缓存字段prototypesCurrentlyInCreation可能为Set,包含多个bean实例。

因此我们知道,prototype的bean无论是构造器或者属性注入,都不允许实现循环依赖

构建不同类型的实例

根据bean类型,分别有各自的构建类型,分为单例、prototype和其他类型三种情况:

//单例
if (mbd.isSingleton()) {
  sharedInstance = getSingleton(beanName, () -> {
    try {
      return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
      // Explicitly remove instance from singleton cache: It might have been put there
      // eagerly by the creation process, to allow for circular reference resolution.
      // Also remove any beans that received a temporary reference to the bean.
      destroySingleton(beanName);
      throw ex;
    }
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// prototype类型
else if (mbd.isPrototype()) {
  // It's a prototype -> create a new instance.
  Object prototypeInstance = null;
  try {
    beforePrototypeCreation(beanName);
    prototypeInstance = createBean(beanName, mbd, args);
  }
  finally {
    afterPrototypeCreation(beanName);
  }
  bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// 其他类型
else {
  String scopeName = mbd.getScope();
  if (!StringUtils.hasLength(scopeName)) {
    throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
  }
  Scope scope = this.scopes.get(scopeName);
  if (scope == null) {
    throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
  }
  try {
    Object scopedInstance = scope.get(beanName, () -> {
      beforePrototypeCreation(beanName);
      try {
        return createBean(beanName, mbd, args);
      }
      finally {
        afterPrototypeCreation(beanName);
      }
    });
    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
  }
  catch (IllegalStateException ex) {
    throw new BeanCreationException(beanName,
        "Scope '" + scopeName + "' is not active for the current thread; consider " +
        "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
        ex);
  }
}

值得注意的是构建实例前,spring都会在缓存里标记该bean正常构建中。

比如单例类型在构建前会调用beforeSingletonCreation(beanName),该方法会把该beanName加入缓存中。在前一个章节 《检查缓存的单例》 中会使用这里的缓存来判断这个bean是否处于单例构建状态。注意这个方法,如果缓存里边已存在该bean,会抛出BeanCurrentlyInCreationException异常。这个异常正是在构造器循环依赖(单例)的时候抛出。

protected void beforeSingletonCreation(String beanName) {
  if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
  }
}

而prototype以及其他类型调用的是beforePrototypeCreation(beanName),同样也是将beanName加入缓存:

protected void beforePrototypeCreation(String beanName) {
  Object curVal = this.prototypesCurrentlyInCreation.get();
  if (curVal == null) {
    this.prototypesCurrentlyInCreation.set(beanName);
  }
  else if (curVal instanceof String) {
    Set<String> beanNameSet = new HashSet<>(2);
    beanNameSet.add((String) curVal);
    beanNameSet.add(beanName);
    this.prototypesCurrentlyInCreation.set(beanNameSet);
  }
  else {
    Set<String> beanNameSet = (Set<String>) curVal;
    beanNameSet.add(beanName);
  }
}

这里用到的缓存prototypesCurrentlyInCreation,也正好呼应了是前一章 《检查prototype循环依赖》

其他类型的bean和prototype类型相同。

因此,spring在构建bean之前都会在缓存里标记该bean处于构建中,所以当发生循环依赖的时候可以快速感知到。

单例对象的早期引用

接上文,如果是单例对象,会调用匿名函数新建createBean进行单例bean构建:

if (mbd.isSingleton()) {
  sharedInstance = getSingleton(beanName, () -> {
    try {
      return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
      // Explicitly remove instance from singleton cache: It might have been put there
      // eagerly by the creation process, to allow for circular reference resolution.
      // Also remove any beans that received a temporary reference to the bean.
      destroySingleton(beanName);
      throw ex;
    }
  });
}

createBean只是做一些构造前的准备,实际的构建过程在doCreateBean中。这很符合spring一贯的命名风格:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
  /// ...
  if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  }
  Object bean = instanceWrapper.getWrappedInstance();
  /// ...

  // Eagerly cache singletons to be able to resolve circular references
  // even when triggered by lifecycle interfaces like BeanFactoryAware.
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
      isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
      logger.trace("Eagerly caching bean '" + beanName +
          "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }

  // Initialize the bean instance.
  Object exposedObject = bean;
  try {
    populateBean(beanName, mbd, instanceWrapper);
    exposedObject = initializeBean(beanName, exposedObject, mbd);
  }

  /// ...
}

doCreateBean比较长,上边只保留核心代码。可以看到主要包含三个步骤:

  • 1.调用createBeanInstance初始化一个半成品的bean,该方法中会寻找满足条件的构造函数进行bean的初始化;
  • 2.如果是构建中的单例bean,并且允许循环依赖(默认允许),那么就会尝试调用addSingletonFactory方法提前曝光一个早期引用(也就是一个半成品bean)到缓存中;
  • 3.调用populateBean、initializeBean,将半成品的bean进一步初始化,主要是处理属性注入等等;

扼要地讲,步骤一处理了构造器注入;步骤二进行了半成品bean的早期引用曝光;步骤三进行了属性注入!

我们来看看早期引用曝光的逻辑:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
  Assert.notNull(singletonFactory, "Singleton factory must not be null");
  synchronized (this.singletonObjects) {
    if (!this.singletonObjects.containsKey(beanName)) {
      this.singletonFactories.put(beanName, singletonFactory);
      this.earlySingletonObjects.remove(beanName);
      this.registeredSingletons.add(beanName);
    }
  }
}

我们看到singletonFactories中会缓存一个工厂对象ObjectFactory,而这个ObjectFactory的getObject方法正是可以返回一个处于构建中半成品bean对象引用。

我们在前边已经讲到,单例对象构建中如果发现出现循环依赖,会尝试从单例的工厂对象中获得一个早期bean对象,并返回,从而避免循环依赖出现死循环。我们再来回忆下:

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 从singletonObjects获取已经初始化完毕的单例对象
  Object singletonObject = this.singletonObjects.get(beanName);
  // 判断该bean是不是正在初始化的单例bean
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    synchronized (this.singletonObjects) {
      // 从earlySingletonObjects中获取单例bean
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
        // 从singletonFactories缓存里获取单例工厂对象
        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
          // 通过单例工厂对象中获取早期单例bean对象,并缓存到earlySingletonObjects中
          singletonObject = singletonFactory.getObject();
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}

因此看到这里你就应该明白,单例对象的构造器注入没有进行早期引用曝光,所以是不能实现循环依赖的!但是单例对象的属性注入是先进行了早期引用曝光,在进行属性注入,所以是支持循环依赖的!

总结

本文主要是探讨spring对循环依赖的处理策略和实现原理。我们认识到spring只支持单例对象的属性注入循环依赖,而其他类型的循环依赖都不支持。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com

×

喜欢就点赞,疼爱就打赏