Spring Boot 扩展:Spring 如何处理循环依赖

01-前言:什么是循环依赖?

首先,我们先明确下依赖的定义。 如果一个 Bean bar 的属性,引用了容器中的另外一个 Bean foo,那么称 foo 为 bar 的依赖,或称 bar 依赖 foo。 如果用代码表示,可以表示为:

@Component("foo")
public Class Foo {
    @Autowired
    private Bar bar;  // 称 foo 依赖 bar
}

@Component("bar")
public Class Bar {
    @Autowired
    private Baz baz;  // bar 依赖 baz
}
复制代码

其次,循环引用的概念是指多个 Bean 之间的依赖关系形成了环。 接着上面的例子,如果 baz 依赖 foo 或 bar 都将形成循环依赖。

@Component("baz")
public class Baz {
    @Autowired
    private Foo foo; // 形成了循环依赖,baz -> (依赖) foo -> bar -> baz ...
}
复制代码

02-Spring 如何处理循环依赖?

我跟大家一块学习了 Spring 创建 Bean 过程的源码。 我们知道:在 createBeanInstance 阶段,需要解决构造器、工厂方法参数的依赖; 在 populateBean 阶段,需要解决类属性中对其他 Bean 的依赖。 这其实对应了 Spring 中支持的两种依赖注入方式,基于构造器的依赖注入和基于 setter 方法的依赖注入,分别对应前面的两种情况。

接下来,我会通过上节介绍的示例,来分情况讨论产生循环依赖的场景。 为了使讨论过程更清楚、更简洁,我会让 foo 依赖 bar,而 bar 依赖 foo。 在接下来的描述中,我假设 Spring 会先创建 Bar 的对象,再创建 Foo 的对象。 针对不同的依赖情况,可以分为四种场景:

  • 第一种场景,Bar 在构造器参数中依赖 Foo,Foo 在构造器参数中依赖 Bar。这种场景下,依赖的注入发生在 Bar 和 Foo 的实例化阶段。
  • 第二种场景,Bar 在构造器参数中依赖 Foo,Foo 通过 setter 函数依赖 Bar。这种场景下,Bar 中注入 Foo 发生在实例化阶段,Foo 中注入 Bar 发生在属性填充阶段。
  • 第三种场景,Bar 通过 setter 函数依赖 Foo,Foo 在构造器参数中依赖 Bar。这种场景下,Bar 中输入 Foo 发生在属性填充阶段,而 Foo 中注入 Bar 发生在实例化阶段。
  • 第四种场景,Bar 通过 setter 函数依赖 Foo,Foo 通过 setter 函数依赖 Bar。 这种场景下,依赖注入均发生在属性填充阶段。

在具体分析上述四种场景之前,先说下结论: Spring 可以解决场景三、四中出现循环依赖的情况,而第一、二种场景,Spring 无法解决,需要重构依赖或者延迟延迟依赖注入的时机(例如使用 @Lazy 等)。 细心的读者可能会问第二种、第三种场景有什么不同呢? 其实第二、第三种场景本质上是同一种情况,唯一的不同是实例化的先后顺序。 结合这个信息,可以得出,先创建的类以构造器参数方式依赖其他 Bean,则会发生循环依赖异常。 反过来,如果先创建的类以 setter 方式依赖其他 Bean,则不会发生循环依赖异常。

接下来,我会详细分析每一种场景,并指出抛循环依赖异常的时机。 (注:接下来的分析需要对 Spring 创建 Bean 的过程有基本的了解

首先,所有的单例 Bean 会在容器启动后被创建 ConfigurableListableBeanFactory#preInstantiateSingletons,即所谓的 “eager registration of singletons” 过程。

第一种场景,会先触发 Bar 类的实例 bar 的创建。在 createBeanInstance 阶段,会通过 ConstructorResolver#autowireConstructor 来创建实例。 ConstructorResolver#createArgumentArray 会解析构造器中的参数,并处理对其他 Bean 依赖的引用 ConstructorResolver#resolveAutowiredArgument。 处理依赖的方式就是通过 DefaultListableBeanFactory#resolveDependency 来查找符合条件的 Bean,最终还是通过 AbstractBeanFactory#getBean 来从容器中取。 当通过 getBean("bar") 来触发 Spring 创建 bar 时,在实例化阶段,根据构造器参数来 getBean("foo") 并触发 foo 的创建。 在 foo 的实例化过程与 bar 的是完全一样的,最终 getBean("bar")。这是容器中的 bar 还没有创建好,所以会再次触发创建过程。 在真正创建过程之前,在 DefaultSingletonBeanRegistry#getSingleton 中会有一次检查,DefaultSingletonBeanRegistry#beforeSingletonCreation 如果发现要创建的 bean 正在创建过程中,则抛出异常。

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'bar': Requested bean is currently in creation: Is there an unresolvable circular reference?
复制代码

第二种场景,同样先构建 bar 实例。与第一种场景不同之处在于 foo 创建时,它的 createBeanInstance 阶段能够执行完毕。 原因是 foo 只有一个无参构造器(即默认构造器),不需要注入其他依赖。 foo 的 createBeanInstance 阶段执行完毕后,会进入 populateBean 阶段。 在这个阶段中,AutowiredAnnotationBeanPostProcessor#postProcessProperties 会处理 setter 函数依赖的 Bean。 大致处理过程为:AutowiredAnnotationBeanPostProcessor 识别到 foo 中包含需要注入依赖的 setter 函数,将其映射为 AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement 对象。 然后调用 AutowiredMethodElement#inject 方法注入依赖。 在 inject 方法中,会调用 DefaultListableBeanFactory#resolveDependency 来查找对应的依赖。 到这里为止,后续的过程与第一种场景完全一致了。 从容器中尝试获取 bar,发现不存在,会出发 bar 的再次创建,最终在 DefaultSingletonBeanRegistry#beforeSingletonCreation 中抛出异常。

第三种场景,同样先构建 bar 实例。由于它只包含一个默认构造器,所以它的 createBeanInstance 阶段会顺利完成,然后进入 populateBean 阶段。 当你仔细回看一下 Spring 创建 Bean 过程的源码,你会发现下面这段代码:

if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
复制代码

这段逻辑发生在 createBeanInstance 之后,尚未进入 populateBean 之前。 这里其实就是 Spring 解决循环依赖机制的核心点之一,这里我暂且不深入介绍,后面会有详细的分析。

继续前面的分析。bar 实例创建进入到 populateBean 阶段后,会检查其自身依赖情况,然后注入对应的依赖 Bean。 这里的处理逻辑依然是 AutowiredAnnotationBeanPostProcessor#postProcessProperties 来处理。 当尝试注入 foo 时,会出发 foo 实例的创建过程。foo 通过构造器依赖 bar,因此在其 createBeanInstance 阶段,会通过 ConstructorResolver#autowireConstructor 完成依赖注入。 此时通过 getBean("bar") 从容器中尝试获取 bar 时,能够“获取到”。 注:这里为什么不会出发 bar 的创建,反而能够直接得到 bar 对象呢?上面的获取到我加了引号,它其实获得的并不是一个完整、可用的 bar。 它获得的是通过 earlySingletonExposure 提前暴露出的对象。 这个过程在后面介绍三级缓存时会详细介绍。

篇幅原因,第四种场景我不在这里继续分析,感兴趣的读者可以自己尝试分析下。 简单提示下,它的过程有点像第三种场景前半段、第二种场景的后半段结合起来。

在上述四种场景下,第一种情况,依赖双方都是通过构造器依赖对方,这种情况下 Spring 是无法处理的。 而且,我认为出现这一情况,属于是设计上的缺陷,应当通过重新设计依赖关系来解决,例如可以将基于构造器的注入修改为基于 setter 的注入,或者通过 @Lazy 将依赖的初始化延迟到使用时。 通过 Foo、Bar 类来举例说明。

@Component
public class Foo {
    private Bar bar;
    @Autowired
    public Foo(@Lazy Bar bar) {
        this.bar = bar;
    }
}

@Component
public class Bar {
    private Foo foo;
    @Autowired
    public Bar(Foo foo) {  // 或者将对 foo 的依赖,注解为 @Lazy 表示使用时才初始化
        this.foo = foo;
    }
}
复制代码

另一种修改方式就是,将第一种情况,修改为第二种情况,即:

@Component
public class Bar {
    private Foo foo;
    @Autowired
    public void setFoo(Foo foo) {
        this.foo = foo;
    }
}
复制代码

03-Spring 中解决循环依赖的三级缓存

Spring 中设计了一个三级缓存用来解决前面介绍的循环依赖问题的处理。三级缓存包括:

  • singletonObjects,为一级缓存,保存了 beanName -> bean instance 的映射关系。存放的是完全可用的单例 Bean 对象。
  • earlySingletonObjects,为二级缓存,保存了 beanName -> bean instance 的映射关系。 在一级、二级缓存都没有发现目标对象,但三级缓存中存在 ObjectFactory 对象时,调用 ObjectFactory#getObject 创建实例,放入二级缓存,删除三级缓存中的 ObjectFactory 对象。
  • singletonFactories,为三级缓存,保存了 beanName -> ObjectFactory 的映射关系。 在 doCreateBean 时,会向这个 map 中添加 beanName: () -> getEarlyBeanReference(beanName, mbd, bean) 的映射关系,value 是一个函数式接口 ObjectFactory。
protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}
复制代码

大概了解了 Spring 中的三级缓存后,我们再回过头来看一下 AbstractBeanFactory#getBean 过程。 它的实际工作是在 AbstractBeanFactory#doGetBean 中完成的。 doGetBean 方法的具体实现可以简化、抽象为:

protected  T doGetBean(
			String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // 缓存中存在
        /**
        * 如果 beanName 是一个 FactoryBean,则获取对应的 Bean
        * 如果 beanName 是一个普通的 Bean,则返回这个 Bean 本身
        */ 
        beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // 缓存中不存在
        // 创建对象
        
        if (mbd.isSingleton()) {
            /**
            * 这里的 getSingleton 是 getSingleton(beanName) 的重载版本
            * 它接受一个 beanName 和 一个 ObjectFactory 作为参数
            * 调用 ObjectFactory#getObject 产生一个实例
            * 并通过 addSingleton(beanName, singletonObject); 将实例添加到 singletonObjects 中
            * 这里 createBean 的代码就是前面提到的 Spring 创建 Bean 实例的过程 doCreateBean
            */
            sharedInstance = getSingleton(beanName, () -> {
                try {
                    return createBean(beanName, mbd, args);
                }
            });
            /**
            * 同前面分支中的作用一样
            */
            beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }
    }
    /**
    * 判断 Bean 类型与 requiredType 类型是否一直,一致则直接返回,不一致则需要进行转换
    */
    return adaptBeanInstance(name, beanInstance, requiredType);
}
复制代码

从上面的源码可以知道,当 DefaultSingletonBeanRegistry#getSingleton(beanName) 时,会先从多级缓存中取对象(可能是 bean instance,也可能是对应的 ObjectFactory)。 从多级缓存中取对象的源码如下:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    Object singletonObject = this.singletonObjects.get(beanName);
    /**
    * 判断第一级缓存中是否有完全可以可用的 Bean 实例,若有则返回;
    * 若没有,则根据情况判断
    * isSingletonCurrentlyInCreation(beanName) 检查的是在 `Set singletonsCurrentlyInCreation` 集合中是否包含要获取的 Bean 实例
    * beanName 只在调用 beforeSingletonCreation(String beanName)  时被添加到 singletonsCurrentlyInCreation 集合中
    * beforeSingletonCreation 在创建 bean,即 doCreateBean 之前调用,在创建过程完成以后,调用 afterSingletonCreation 从集合中移除 beanName
    */
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { 
        /**
        * 执行到这个分支,其实说明 Bean 已经在创建过程中了,只不过是尚未完全可用(即一级缓存中没有)
        * 检查二级缓存,是否包含指定的 Bean
        * 
        * 二级缓存里的内容何时被添加或设置进来的呢?
        * 我们可以检查下 earlySingletonObjects.put 方法都在哪里调用。
        * 检查后发现,其实 earlySingletonObjects 就是在当前方法中设置的,我们接着往下看。
        */
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            /**
            * 这里的 allowEarlyReference 的意思就是指是否允许在二级缓存中创建一个对象,即是否允许暴露未完全可用的对象
            * 如果 allowEarlyReference 为假,则不会操作二级、三级缓存,而仅检查一级缓存中是否有完全可用的 Bean 实例
            * 这也意味着,不允许返回未完全可用状态的 Bean
            * 
            * 当发现二级缓存中没有对象,同时又允许提前引用(即 allowEarlyReference 值为真)
            * 则检查三级缓存中是否有对应的 ObjectFactory 对象,若有,则调用它的 getObject 方法产生对象,然后将其放置到二级缓存中,同时删除三级缓存中的对象工厂实例
            * 若三级缓存中也没有对象工厂实例,则说面 bean 还未创建
            */
            synchronized (this.singletonObjects) {
                /**
                * 这里会进行一个 double-check,避免多线程间的线程安全问题
                */
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            /**
                            * 三级缓存中存在对象工厂实例,则通过它产生一个 Bean 实例
                            * 加入到二级缓存中,同时删除三级缓存中的对象工厂实例
                            */
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}
复制代码

注:

  • isSingletonCurrentlyInCreation(beanName) 意味着什么呢?意味着对应的 Bean 在 doCreateBean 过程中,可能在 createBeanInstance \ populateBean \ initializeBean 阶段中。
  • 在前面提到,createBeanInstance 后,Bean 会被添加到上述多级缓存中的第三级缓存中,存入对象是 beanName -> objectFactory 映射关系。 当其他的 Bean 依赖当前 Bean 时,而且允许引用提前暴露的 Bean(即未完全可用的 Bean),会检查第二级缓存,如果没有还会检查第三级缓存,并在得到对应 objectFactory 时,获得对象并将其从第三级移动到第二级。

有些读者看到这里可能会有个疑问,那二级缓存中的对象什么时候删除呢? 我们再来回头看下 doGetBean 中的代码片段:

sharedInstance = getSingleton(beanName, () -> {
    try {
        return createBean(beanName, mbd, args);
    }
});
复制代码

这里的 singleton

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            /**
            * 检查 Bean 是否在创建过程中,避免重复创建,
            * 不能解决的循环依赖也是在这里抛出异常
            */
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            try {
                /**
                * 这里调用的其实就是  AbstractAutowireCapableBeanFactory#createBean
                * 然后会执行 doCreateBean(三个阶段)
                */
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            catch (IllegalStateException ex) {
                // Has the singleton object implicitly appeared in the meantime ->
                // if yes, proceed with it since the exception indicates that state.
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    throw ex;
                }
            }
            catch (BeanCreationException ex) {
                throw ex;
            }
            finally {
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                /**
                * 这里说明一个 bean 创建过程的三个阶段都执行完毕了
                */
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}
/**
* 将 Bean 实例添加到第一级缓存
* 将第二级、第三级缓存中的对象删除
*/
protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
			this.singletonObjects.put(beanName, singletonObject);
			this.singletonFactories.remove(beanName);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}
复制代码

到这里为止,相信你对 Spring 创建 Bean 的过程以及处理循环依赖的机制能够有个大致的了解。 如果想了解的更深或跟多细节,可以自己单步调试下。 希望今天的内容能够对你有所帮助。

你可能感兴趣的:(spring,spring,boot,java)