likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 19

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 的流程。

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

重点来了,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 从二级缓存转移到一级缓存,并取消创建中的标记

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

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 出现在三级缓存和二级缓存中时,被标记为创建中。当添加到一级缓存之后,创建中的标记才被取消。

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

注:三级缓存中存放的实际上是 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 (+)

注:+号表示新增、*表示修改

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


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请分享一下。

转载自:https://juejin.cn/post/7378318507833688118
评论
请登录