【重写SpringFramework】第一章beans模块:循环依赖(chapter 1-10)

1.前言

Spring 的依赖注入使得一个类可以很方便地获取需要的对象,但同时也产生了新的问题。我们来看一个例子,假设有两个类 RefARefB,彼此互相依赖。如果不做任何处理,Spring 处理 RefA 时会寻找 refB 字段的依赖项,然后开始解析 RefB。接着发现需要寻找 refA 的依赖项,又转过头来处理 RefA。如此便陷入了无限循环之中,直到堆栈溢出报错。虽然我们可以通过代码规范尽量避免循环依赖,但当对象的依赖关系变得复杂时,隐式的循环依赖是难以避免的,因此必须从机制层面解决问题。

//示例代码
public class RefA {
    @Autowired
    private RefB refB;
}

public class RefB {
    @Autowired
    private RefA refA;
}

2. 原理分析

2.1 循环依赖产生的原因

整个流程分为两个阶段,一是获取 RefA 的流程,二是依赖解析 RefB 的流程。第一个阶段,在创建实例之后,进入填充对象的流程,发现需要注入 refB 字段,此时尝试解析 RefB 对象。然后进入第二阶段,前边的流程和第一阶段相同,然后依赖解析时发现需要注入 refA 字段,于是又进入获取 RefA 的流程。

在这里插入图片描述

重点来了,RefA 的创建流程是包括 initializeBean 方法的,但此时尚处于 resolveDependency 方法的嵌套之中,Spring 容器的缓存中是没有 RefA 对象的,只能再次执行创建流程。这样一来,陷入了无限循环之中,再也无法达到 initializeBean 方法,整个流程也结束不了。仔细分析整个流程,创建 RefA 的流程是必须的,解析 RefB 的流程也是必要的,关键在于第二次尝试获取 RefA 时,应该想办法从缓存中获取实例,避免再次执行创建流程

2.2 引入三级缓存

首先可以确定的是,如果对象已经创建完毕,是不会出现循环依赖的问题。因此需要通过某种方式,将创建中的 RefA 对象临时存储起来,当第二次获取 RefA 时返回临时存储的对象。这样一来,不会继续执行创建 RefA 的流程,循环的链条就此被打断了。鉴于此,Spring 使用三级缓存机制来解决循环依赖的问题。前面提到,DefaultSingletonBeanRegistry 负责存储单例,相关字段介绍如下:

  • singletonObjects 字段表示一级缓存,存储已创建的单例

  • earlySingletonObjects 字段表示二级缓存,存储创建中的单例

  • singletonFactories 字段表示三级缓存,作用是解决代理对象的循环依赖问题

  • singletonsCurrentlyInCreation 字段负责对创建中的对象进行标记

public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);	//一级缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);		//二级缓存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);	//三级缓存

    //临时标记一个创建中的Bean
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}

其中二级缓存和三级缓存负责临时存储创建中的对象,最终对象将保存在一级缓存中。三级缓存之间必须满足两个原则:

  • 一级缓存与二、三级缓存互斥,即 Bean 存储在一级缓存,则必定不在二、三级缓存中
  • 二级缓存与三级缓存互斥,前提是一级缓存中不存在

2.3 解决思路

前边分析到,我们需要对创建中的对象进行标记,并依次加入到三级缓存中。因此,在解决循环依赖的时候,一共出现了四处改动。结合流程图进行分析,如下所示:

  • 第一次获取 RefA 的流程时,在创建实例之前先标记为创建中

  • 创建 RefA 实例之后,将未完成的对象加入到三级缓存

  • 第二次进入获取 RefA 的流程,获取缓存时,从三级缓存获取 RefA 的实例,然后加入到二级缓存

  • 等到 RefA 的创建流程执行完毕,将 RefA 从二级缓存转移到一级缓存,并取消创建中的标记

在这里插入图片描述

3. 代码实现

3.1 标记为创建中

本节比较特殊,是在已有的代码上进行调整,因此需要顺着循环依赖发生的过程来介绍。首先是获取 RefA 的流程,我们来看简化的 doGetBean 方法,第一步是从缓存中获取单例,第二步是创建单例并加入缓存。这里有两个重载的 getSingleton 方法,前者只是查询缓存,后者包括实际的创建流程。一开始,缓存中不存在 RefA 对象,我们来看第二步的 getSingleton 方法。

//所属类[cn.stimd.spring.beans.factory.support.AbstractBeanFactory]
//获取Bean的入口方法
protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args) {
    //1. 从缓存中获取单例
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance == null) {
        //2. 创建单例,并加入缓存
        sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
           @Override
            public Object getObject() throws BeansException {
                return createBean(beanName, mbd, args);
            }
        });
    }
    ......
}

在之前的简单实现中,只出现了第二步和第四步。现在增加了两个改动,一是在创建对象前将当前 Bean 标记为创建中(第一步),二是在创建结束后取消创建中的标记(第三步)。按照执行的流程,仅执行了第一步,也就是将 RefA 标记为「创建中」。到了第二步,就进入了创建对象的流程。

//所属类[cn.stimd.spring.beans.factory.support.DefaultSingletonBeanRegistry]
//获取已缓存的单例,如不存在则创建单例
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if(singletonObject == null){
        //1) 标记为创建中的Bean
        beforeSingletonCreation(beanName);
        //2) 回调执行创建对象的流程
        singletonObject = singletonFactory.getObject();
        //3) 取消创建中的标记
        afterSingletonCreation(beanName);
        //4) 将对象添加到一级缓存,同时从二级缓存中移除
        addSingleton(beanName, singletonObject);
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

3.2 三级缓存的处理

RefA 标记为创建中之后,接下来触发回调,进入 AbstractAutowireCapableBeanFactorydoCreateBean 方法。第一步创建实例,第三步是填充对象,紧接着就是依赖解析,因此必须在此之前做点什么。我们来看第二步的实现,首先检查当前 Bean 是否处于创建中,此时 RefA 已经被标记为创建中,因此调用 addSingletonFactory 方法,将一个 ObjectFactory 匿名对象加入到三级缓存中。既然是 ObjectFactory 对象,说明不会立即被创建,而是在后续流程中的某一时刻触发回调。

//所属类[cn.stimd.spring.beans.factory.support.AbstractAutowireCapableBeanFactory]
//创建Bean的流程
protected Object doCreateBean(String beanName, RootBeanDefinition mbd) {
    //1. Bean的实例化(略)

    //2. 提前暴露Bean以解决循环依赖
    boolean earlySingletonExposure = isSingletonCurrentlyInCreation(beanName);
    if(earlySingletonExposure) {
        //加入三级缓存
        addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
                //对于普通对象,简单认为返回bean
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
    }
    Object exposedObject = bean;

    //3. 填充对象(略)
    //4. 初始化(略)

    //如果Bean在创建中,取出二级缓存中的引用(可能是一个代理)
    //填充对象和初始化的操作都是针对目标对象,但是最后需要返回的是一个代理
    if(earlySingletonExposure){
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null && exposedObject == bean) {
            exposedObject = earlySingletonReference;
        }
    }
    return exposedObject;
}

现在的问题是,为什么要通过回调的方式执行 getEarlyBeanReference 方法?假设正在创建的对象是一个代理,而此时的变量 bean 还只是一个原始对象,如果直接加入三级缓存,原本应该注入代理对象,结果注入了原始对象。这是非常严重的错误,Spring 的补救措施是将创建代理的过程提前,这也是方法名 early 的含义所在。以下是两种创建代理的时机:

  • BeanPostProcessor 接口的 postProcessAfterInitialization 方法在初始化后创建代理
  • InstantiationAwareBeanPostProcessor 接口的 getEarlyBeanReference 方法则提前暴露代理(在必要的情况下)
//所属类[cn.stimd.spring.beans.factory.support.AbstractAutowireCapableBeanFactory]
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (bean != null && !mbd.isSynthetic()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                //提前创建代理对象
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                if (exposedObject == null) {
                    return null;
                }
            }
        }
    }
    return exposedObject;
}

3.2 二级缓存的处理

接下来是填充对象,在此期间对 RefB 进行依赖解析,转入获取 RefB 的流程。三级缓存的处理是一样的,然后在依赖注入的过程中,发现 RefA 需要依赖解析。现在第二次进入 RefA 的获取流程,我们尤其关心如何拿到 RefA 的缓存。

//所属类[cn.stimd.spring.beans.factory.support.AbstractBeanFactory]
//获取Bean的入口方法
protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args) {
    //1. 从缓存中获取单例
    Object sharedInstance = getSingleton(beanName);
    ......
}

DefaultSingletonBeanRegistry 实现了 getSingleton 方法(重载一)。在之前的简单实现中,仅从一级缓存中查询,现在则分为三步:

  1. 尝试从一级缓存中查找,此时 RefA 尚未创建完毕,一级缓存中不存在 RefA 的实例。
  2. 尝试从二级缓存中查询,此时二级缓存中也没有 RefA 的实例。
  3. 从三级缓存中取出 ObjectFactory 对象,然后执行 getObject 方法获取实例(可能是代理对象),最后将 RefA 的实例添加到二级缓存,并从三级缓存中移除。

需要注意的是,第三步只会执行一次。假如还有其他对象比如 RefC 也依赖 RefA 对象,由于二级缓存中已存在 RefA 的实例,直接使用就可以了。由于我们从缓存中拿到了 RefA 的实例,不会继续执行创建流程,循环的链条就此被截断,循环依赖的死结被解开了。

//所属类[cn.stimd.spring.beans.factory.support.DefaultSingletonBeanRegistry]
//重载方法一,不涉及创建对象
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //1. 一级缓存,已经创建完毕的Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        //2. 二级缓存,说明Bean尚在创建中(如果存在,说明三级缓存已经调用过了)
        singletonObject = this.earlySingletonObjects.get(beanName);

        if (singletonObject == null && allowEarlyReference) {
            //3. 三级缓存,主要用于解决代理对象的循环依赖(二级缓存保证了三级缓存只会调用一次)
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                //二、三级缓存互斥,添加到二级缓存的同时,从三级缓存中移除
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
}

3.3 一级缓存的处理

最后来看 RefA 的收尾工作,回到 DefaultSingletonBeanRegistrygetSingleton 方法(重载二),上述三级缓存和二级缓存的处理都在第二步的执行流程中。由于依赖循环的链条断裂了,RefA 对象最终得以创建。第三步,取消 RefA 的创建中的标记。关键是第四步,将 RefA 的实例添加到一级缓存,并从二级缓存中移除,这标志着对象最终创建完毕。至此,循环依赖已得到彻底解决。在这之后,如果还有其他对象依赖 RefA,直接从一级缓存中获取对象就行了。

//所属类[cn.stimd.spring.beans.factory.support.DefaultSingletonBeanRegistry]
//重载方法二,涉及创建对象
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if(singletonObject == null){
        //1) 标记为创建中的Bean
        beforeSingletonCreation(beanName);
        //2) 回调执行创建对象的流程
        singletonObject = singletonFactory.getObject();
        //3) 取消创建中的标记
        afterSingletonCreation(beanName);
        //4) 将对象添加到一级缓存,同时从二级缓存中移除
        addSingleton(beanName, singletonObject);
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

4. 再论三级缓存

4.1 三级缓存的迁移

看完了依赖循环流程上的变化,我们再来看一下三级缓存是如何互相作用的。首先,创建 RefA 实例之后,添加到三级缓存中。其次,第一次解析依赖 RefA,从 Spring 容器中查询缓存,取出三级缓存中的实例,然后转移到二级缓存。最后,当 RefA 的创建流程全部走完,将二级缓存中的 RefA 转移到一级缓存中。此外,当 RefA 出现在三级缓存和二级缓存中时,被标记为创建中。当添加到一级缓存之后,创建中的标记才被取消。

在这里插入图片描述

注:三级缓存中存放的实际上是 ObjectFactory 对象,但是对于普通对象来说,可以简单地认为就是 RefA 本身。

4.2 三级缓存的作用

通过对流程的梳理,我们发现整个流程就是从三级缓存到二级缓存,再到一级缓存,不断提升的过程。因此,我们从下到上再来看一下它们的作用。

  • 三级缓存:存放 ObjectFactory 匿名对象,主要是为了解决代理对象的循环依赖问题
  • 二级缓存:存放从三级缓存中得到的实例,可能是一个代理对象。在一个循环依赖的过程中,多次依赖解析同一个对象,实际都是从二级缓存中获取的
  • 一级缓存:当一个对象完成创建的全部流程,存放到一级缓存。在此之后,查询缓存指的都是一级缓存

我们发现,二级缓存的作用是确保三级缓存中的 ObjectFactory 对象只回调一次,因为对于代理对象来说,代理对象的创建过程只能有一次。如果是普通对象,只保留一级和三级缓存就够了,因为不管回调多少次,获得的都是同一个对象。

4.3 三级缓存的不足

需要注意的是,Spring 框架并未完全解决循环依赖的问题。这是因为循环依赖是由 resolveDependency 方法触发的,但是 resolveDependency 方法并非只有填充对象的流程会触发。在配置类的构造方法中,即使参数没有声明 @Autowired 注解,也会调用 resolveDependency 方法。配置类的构造方法是在实例化的过程中完成的,三级缓存针对的则是填充对象的流程。针对这种特殊情况,可以采取其他方式来解决。比如使用 @Lazy 注解,或者取消构造方法,改成 @Autowired 注解声明字段的方式。

//示例代码:配置类的构造方法会自动触发依赖解析
@Configuration
public class XxxConfig {
    private Foo foo;

    //触发依赖解析
    public XxxConfig(Foo foo) {
        this.foo = foo;
    }
}

5. 测试

5.1 未解决的循环依赖

测试方法没有使用原生的 DefaultListableBeanFactory,而是新增了一个子类 UnresolvedCircleReferenceBeanFactory,作用是重写父类 DefaultSingletonBeanRegistry 获取单例的方法,实际上复制了原有的简单实现(详见代码)。接下来我们要解决循环依赖的问题,如果仍沿用 DefaultListableBeanFactory,测试用例就失去意义,不能模拟出循环依赖未解决的场景了。

//测试方法
@Test
public void testUnresolvedCircleReference(){
    DefaultListableBeanFactory factory = new UnresolvedCircleReferenceBeanFactory();
    AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
    factory.addBeanPostProcessor(processor);
    processor.setBeanFactory(factory);

    factory.registerBeanDefinition("refA", new RootBeanDefinition(RefA.class));
    factory.registerBeanDefinition("refB", new RootBeanDefinition(RefB.class));
    factory.getBean("refA", RefA.class);
}

从测试结果可以看到,Spring 容器在解析 RefARefB 时,陷入了无限循环之中。

[Beans] [依赖注入] --> 目标对象: refA, 字段名: refB
[Beans] [依赖注入] --> 目标对象: refB, 字段名: refA
[Beans] [依赖注入] --> 目标对象: refA, 字段名: refB
[Beans] [依赖注入] --> 目标对象: refB, 字段名: refA
[Beans] [依赖注入] --> 目标对象: refA, 字段名: refB
......

5.2 已解决的循环依赖

在测试方法中,Spring 容器的实现类是 DefaultListableBeanFactory,其余代码与上一个测试方法相同。

//测试方法
@Test
public void testResolvedCircleReference() {
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
    factory.addBeanPostProcessor(processor);
    processor.setBeanFactory(factory);

    factory.registerBeanDefinition("refA", new RootBeanDefinition(RefA.class));
    factory.registerBeanDefinition("refB", new RootBeanDefinition(RefB.class));
    factory.getBean("refA", RefA.class);
}

从测试结果可以看到,Spring 容器分别对 RefARefB 解析后,就走完了整个测试流程,说明循环依赖得到了解决。

[Beans] [依赖注入] --> 目标对象: refA, 字段名: refB
[Beans] [依赖注入] --> 目标对象: refB, 字段名: refA

5.3 代理对象的循环依赖

准备两个测试类,TargetATargetB 都实现了 ITarget 接口,互相以对方作为依赖项,从而形成代理对象的循环依赖的场景。

//测试类:模拟代理对象的循环依赖
public class TargetA implements ITarget {
    @Autowired
    private ITarget targetB;

    @Override
    public void handle() {
        System.out.println("targetB的实际类型:" + targetB.getClass().getName());
    }
}

public class TargetB implements ITarget {
    @Autowired
    private ITarget targetA;

    @Override
    public void handle() {
        System.out.println("targetA的实际类型:" + targetA.getClass().getName());
    }
}

测试方法比较简单,唯一需要注意的是注册 SimpleProxyPostProcessor 组件来创建代理对象。

//测试方法
@Test
public void testProxyCircleReference() {
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
    processor.setBeanFactory(factory);
    factory.addBeanPostProcessor(processor);
    factory.addBeanPostProcessor(new SimpleProxyPostProcessor());   //添加创建代理的处理器

    factory.registerBeanDefinition("targetA", new RootBeanDefinition(TargetA.class));
    factory.registerBeanDefinition("targetB", new RootBeanDefinition(TargetB.class));
    ITarget targetA = factory.getBean("targetA", ITarget.class);
    ITarget targetB = factory.getBean("targetB", ITarget.class);
    targetA.handle();
    targetB.handle();
}

从测试结果可以看到,由于 TargetATargetB 都是代理对象,因此在创建实例之后提前暴露代理对象,1~4 行日志可以印证这一点。然后调用 handle 方法,TargetATargetB 的类型都是 JDK 代理。

[Beans] [依赖注入] --> 目标对象: targetA, 字段名: targetB
提前暴露代理对象[targetA]
[Beans] [依赖注入] --> 目标对象: targetB, 字段名: targetA
提前暴露代理对象[targetB]
代理拦截:目标对象 [beans.autowire.TargetA], 方法名 [handle]
targetB的实际类型:com.sun.proxy.$Proxy7
代理拦截:目标对象 [beans.autowire.TargetB], 方法名 [handle]
targetA的实际类型:com.sun.proxy.$Proxy7

6. 总结

本节我们介绍了依赖注入可能产生的副作用,即循环依赖的问题。简单地说,当两个对象互相依赖对方,如果不加以干涉,那么在依赖解析的过程中将陷入无限循环,最终导致堆栈溢出的异常。为了解决依赖循环的问题,Spring 引入了三级缓存。对于普通对象来说,其实二级缓存就足够了,三级缓存的作用是解决代理对象的循环依赖问题。关于代理对象这种特殊的单例,我们将在第二章 aop 模块进行介绍。

此外,Spring 框架并未完全解决循环依赖的问题。本节只介绍了填充对象的过程中可能产生的循环依赖问题,至于实例化的过程也可能产生循环依赖的问题,则必须通过其他方式来解决。这里也可以看到 Spring 框架处理问题的思路,即解决掉最常出现的问题,至于特殊情况另当别论。

7. 项目信息

新增修改一览,新增(5),修改(3)。

beans
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.beans
   │        └─ factory
   │           └─ support
   │              ├─ AbstractAutowireCapableBeanFactory.java (*)
   │              └─ DefaultSingletonBeanRegistry.java (*)
   └─ test
      └─ java
         └─ beans
            └─ autowire
               ├─ AutowireTest.java (*)
               ├─ RefA.java (+)
               ├─ RefB.java (+)
               ├─ TargetA.java (+)
               ├─ TargetB.java (+)
               └─ UnresolvedCircleReferenceBeanFactory.java (+)

注:+号表示新增、*表示修改
  • 项目地址:https://gitee.com/stimd/spring-wheel

  • 本节分支:https://gitee.com/stimd/spring-wheel/tree/chapter1-10

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


关注公众号【Java编程探微】,加群一起讨论。
在这里插入图片描述

  • 29
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值