Spring IOC 源码简单分析 03 - 循环引用

### 准备

## 目标

了解 Spring 如何处理循环引用

##测试代码

gordon.study.spring.ioc.IOC03_CircularReference.java

Spring IOC 源码简单分析 03 - 循环引用

 
ioc03.xml
<beans ...>
    <bean id="chairman" class="gordon.study.spring.common.Employee">
        <property name="name" value="Cheque Wicket" />
        <property name="company" ref="macrohard" />
    </bean>
    <bean id="macrohard" class="gordon.study.spring.common.Company">
        <property name="name" value="macrohard" />
        <property name="employees">
            <list>
                <ref bean="chairman" />
            </list>
        </property>
    </bean>

</beans>

### 分析

## 问题何在

按照前一篇 getBean 方法流程分析的结论,当第20行代码通过 getBean 方法获取 chairman bean 实例时,会先实例化 gordon.study.spring.common.Employee 实例,然后装配 name 和 company 属性,由于 company 属性值是另一个 bean macrohard,因此会调用 getBean 方法获取 macrohardbean 实例,巧合的是,macrohard bean 的 employees 属性又依赖 chairman bean 实例,形成循环引用。

## 解决思路

以前自己写过简单的 IOC 容器,处理循环引用的方法就是将已实例化但还未装配完成的 bean 直接当作已完全构造完成的 bean 使用,只要保证这些 bean 最终能够完成装配与初始化工作即可。
对于本例,chairman bean 在装配 company 属性时依赖 macrohard bean,于是递归构建 macrohard bean,在装配 macrohard bean 的 employees 属性时又依赖回 chairman bean。此时我们把未装配完成的 chairman bean 交给 macrohard bean 用于装配属性,这样macrohard bean 的构建工作就可以继续,直到其完成装配与初始化工作后,递归调用返回(此时macrohard bean 不算完成构建,因为其 employees 中的元素此时还没有被装配与初始化)。现在,chairman bean 就可以用 macrohard bean 完成自己的装配工作了。当chairman bean 装配完成并初始化后,macrohard bean 也自动构建完成了。

## 源码分析

DefaultListableBeanFactory 类型层次如下图所示:

Spring IOC 源码简单分析 03 - 循环引用

其父类 DefaultSingletonBeanRegistry 专门用于处理 singleton bean 的注册(包括创建),该类的两个 getSingleton 方法是本次分析的重点。
 
DefaultSingletonBeanRegistry 包含以下属性与本次分析息息相关:
Spring IOC 源码简单分析 03 - 循环引用
 
先看一下 getBean 方法的基本逻辑:
  1. 调用 getSingleton(String beanName) 方法尝试获取已构建完成的 bean 实例,或者在发现循环引用时获取 early singleton bean 实例。
  2. 如果前一步 getSingleton 方法没有找到合适的 bean 实例,则通过 getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法创建 bean 实例。该方法调用过程中会引起递归(创建 bean)。
 
getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法主要流程(方法二):
  1. 尝试直接从 singletonObjects 中获取已构建完成的 bean 实例。singletonObjects 用于缓存已完全构建的 bean 实例。
  2. 在 beforeSingletonCreation 方法中将 bean name 放入集合 singletonsCurrentlyInCreation 中。singletonsCurrentlyInCreation 记录已经在创建中的所有单例 bean,当相同的 bean 妄图进入该集合时,表示发现循环引用且无法解决,将抛出 BeanCurrentlyInCreationException 异常。
  3. 调用参数 ObjectFactory 的 getObject 方法创建 bean 实例。
    getObject 方法直接调用 AbstractAutowireCapableBeanFactory 的 createBean 方法创建 bean 实例,包括实例化、属性装配与初始化工作,调用栈如下图所示(截图来自上一篇文章):
    Spring IOC 源码简单分析 03 - 循环引用
    在本例中,因为使用的都是无参构造函数,所以实例化工作是一定可以完成的。

    当实例化完成后,有以下与循环引用相关的代码:
    Spring IOC 源码简单分析 03 - 循环引用
    默认情况下,全局参数允许循环引用(allowCircularReference)值为 true,考虑到当前 bean 在上一步一定已经放入 singletonsCurrentlyInCreation 集合,因此会调用 addSingletonFactory 方法为该 bean 创建对应的 ObjectFactory。【(以下描述不准确)addSingletonFactory方法的实现有点闭包的味道:实际上是建立了 beanName 与 getEarlyBeanReference 方法在当时的上下文环境(可以简单理解为局部变量值环境)下的联系。之后需要获取 beanName 的 early bean reference 时,通过调用 ObjectFactory 的 getObject 方法,相当于调用当时上下文环境下的 getEarlyBeanReference方法(参数值保留了创建 ObjectFactory 实例时上下文环境中的变量值)。可以想象,如果不用闭包,需要将 mbd 与 bean 用其它的方式与 beanName 建立对应关系并缓存起来。实际上对于本例,getEarlyBeanReference方法返回的就是其参数 bean,也就是此段代码前面刚刚创建的 bean 实例。】
    Spring IOC 源码简单分析 03 - 循环引用
    如上,bean name 与相应的 ObjectFactory 被放入 singletonFactories 中。同时可以发现,当 beanName 存在于 singletonFactories中时,一定不存在于 earlySingletonObjects 中。
    之后,在装配属性时可能会依赖其它 bean 实例,通过 getBean 方法获取相应的 bean 实例。如果没有循环引用,则形成典型的递归调用(getBean -> 方法二 -> getBean);如果发现循环引用,则方法一介入,见下文分析。

  4. 在 afterSingletonCreation 方法中将 bean name 从 singletonsCurrentlyInCreation 中移除。
  5. 在 addSingleton 方法中将创建出的实例放入 singletonObjects 中。
    Spring IOC 源码简单分析 03 - 循环引用
    第147行将 bean name 从 earlySingletonObjects 中移除。目前看来纯粹是为了释放内存,并无其它深意。
 
getSingleton(String beanName) 方法主要流程(方法一):
  1. 尝试直接从 singletonObjects 中获取已装配完成的 bean 实例
  2. 如果 bean name 在集合 singletonsCurrentlyInCreation 中,表示发现循环引用。此时,本方法会通过 earlySingletonObjects 要么返回已经创建好的 early singleton bean 实例,要么通过 singletonFactories 找到对应的 ObjectFactory 创建 early singleton bean 实例并放入 earlySingletonObjects 中。
    Spring IOC 源码简单分析 03 - 循环引用

## 本例分析

  • 对于本例,首先通过 getBean 方法获取 chairman bean,getSington 方法一返回 null(因为 singletonsCurrentlyInCreation 当前为空,不包含 chairman),getSington 方法二实例化 chairman bean,在装配 company 属性时,需要构造 macrohard bean。此时 singletonObjects 为空,earlySingletonObjects 为空,singletonsCurrentlyInCreation 包含 chairman,singletonFactories 包含 chairman。
  • 递归调用 getBean 方法获取 macrohard bean,getSington 方法一返回 null(因为singletonsCurrentlyInCreation当前不包含 macrohard),getSington 方法二实例化
    macrohard bean,在装配 employees 属性时,需要构造 chairman bean。此时 singletonObjects 为空,earlySingletonObjects 为空,singletonsCurrentlyInCreation 包含 chairman 与 macrohard,singletonFactories 包含 chairman 与 macrohard。
  • 再次递归进入 getBean 方法获取 chairman bean,方法一创建 early singleton bean chairman 并放入 earlySingletonObjects。此时 singletonObjects 为空,earlySingletonObjects 包含 chairman,singletonsCurrentlyInCreation 包含 chairman 与 macrohard,singletonFactories 包含 macrohard。
  • 返回 macrohard bean 构建过程,完成构建。此时 singletonObjects 包含 marcohard,earlySingletonObjects 包含 chairman,singletonsCurrentlyInCreation 包含 chairman,singletonFactories 为空。
  • 返回 chairman bean 构建过程,完成构建。此时 singletonObjects 包含 marcohard 与 chairman,earlySingletonObjects 为空,singletonsCurrentlyInCreation 为空,singletonFactories 为空。

## 其它循环引用情况分析

通过 setter 注入方式产生的循环引用是可以通过以上方案解决的。

构造器注入方式产生的循环引用无法解决,因为无法实例化出 early singleton bean 实例。
非单例模式的循环引用也无法解决,因为 Spring 框架不会缓存非单例的 bean 实例。
 
 
 
 
 
 
 
上一篇:LightDB 21.3 release note


下一篇:Block的使用及循环引用的解决