从源码层面解读spring中bean的循环依赖解决方案

210 阅读4分钟

写在前面的话

正逢立冬,北风潜入悄无声,未品浓秋已立冬。

寒潮来袭,气温骤降,当下室外温度为7°,成都这一轮的新冠也扰人不得安宁,致敬在抗疫一线工作的英雄们。

什么是循环依赖?

循环依赖就是循环引用,就是两个或者多个bean相互之间的持有对方,

比如A引用B,B引用C,而C又引用A,这样它们就最终反映为一个环。

注意这里不是循环调用,循环调用是方法之间的环调用,

循环调用是无法解决的,除非有终结条件,否则就是死循环,最终会导致内存溢出错误。

spring如何解决依赖循环

先说分类,spring的循环依赖分为:构造器循环依赖和setter循环依赖。

结论是:构造器循环依赖没有办法解决,spring会直接抛错。

setter中的bean循环依赖解决必须使单例模式的,否则也是无法解决的

下面详细说一下各种的特点和具体表现

1.构造器循环依赖

表示通过构造器注入构成的循环依赖,无法解决,抛出BeanCurrentlyInCreationException异常来表示发生了循环依赖。

<bean id="A" class="com.bean.A">
    <constructor-arg indes="0" ref="B"/>
</bean>

<bean id="B" class="com.bean.B">
    <constructor-arg indes="0" ref="C"/>
</bean>

<bean id="C" class="com.bean.C">
    <constructor-arg indes="0" ref="B"/>
</bean>

举个例子:

假设通过要创建A类的时候,构造器需要B类,这是就需要去创建B类,但是在创建B的时候发现B的构造器需要C类,

这时就需要去创建C,但是不巧的是C的构造器也需要A类,那么就形成了一个环,没有办法创建。

Spring有个机制是,会将每一个正在创建的bean标识符放在“当前创建bean池”中,创建过程中会一直保持,创建完毕就会将bean标识符从池子中清除掉。

如果创建过程中发现自己已经在该池中时就会抛出BeanCurrentlyInCreationException异常表示依赖循环。

2.setter循环依赖

定义为通过setter注入方式构成的循环依赖。

解决的原理是:

通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的bean来解决setter循环依赖

而且只能解决单例作用域的bean依赖循环。

通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bena

源代码如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, 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);
      }
   }
}
/**
 * 获取用于早期访问指定bean的引用,
 * 通常用于解析循环引用。
 */
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
   Object exposedObject = bean;
   if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
         }
      }
   }
   return exposedObject;
}

我们举一个列子:

<bean id="A" class="com.bean.A">
    <property name="B" ref="B"/>
</bean>

<bean id="B" class="com.bean.B">
    <property name="C" ref="C"/>
</bean>

<bean id="C" class="com.bean.C">
    <property name="A" ref="A"/>
</bean>

默认是“singleton”作用域。

需求还是创建A,B,C三个类

1.Spring容器创建单例“A”bean,首先会根据无参构造器创建bean,并暴露一个“ObjectFactory”用于返回一个提前暴露创建中的bean,并将“A”标识符放到“当前创建bean池”,然后进行setter方式注入“B”。

2.Spring容器创建单例“B”bean,首先会根据无参构造器创建bean,并暴露一个“ObjectFactory”用于返回一个提前暴露创建中的bean,并将“B”标识符放到“当前创建bean池”,然后进行setter方式注入“C”。

3.Spring容器创建单例“C”bean,首先会根据无参构造器创建bean,并暴露一个“ObjectFactory”用于返回一个提前暴露创建中的bean,并将“C”标识符放到“当前创建bean池”,然后进行setter方式注入“C”。

这里在注入A的时候由于提前暴露了“ObjectFactory”工厂,从而可以使用工厂返回提前暴露一个创建中的bean,完成“C”的注入。

4.最后依赖注入“B”和“A”,完成setter注入。

3.prototype范围的依赖处理

结论:对于“prototype”作用域的bean,Spring容器无法完成依赖注入,

因为Spring容器规定不会进行缓存“prototype”作用域的bean,因此无法提前暴露一个创建中的bean。

配置示例:

<bean id="A" class="com.bean.A" scope="prototype">
    <property name="B" ref="B"/>
</bean>

<bean id="B" class="com.bean.B" scope="prototype">
    <property name="C" ref="C"/>
</bean>

<bean id="C" class="com.bean.C" scope="prototype">
    <property name="A" ref="A"/>
</bean>

注意: 问题: 那么如果可以控制bean之前能否循环引用吗?

回答:可以的。

对“singleton”作用域的bean,可以通过“setAllowCircularReferences(false);”来禁用循环引用。

该属性表示是否允许bean之间的循环引用并自动尝试解析它们,默认是true。

设置为false时可在遇到循环引用时引发异常,完全不允许循环引用。

写在后面的话

我们初步介绍了一下Spring循环依赖的分类和解决原理,也引出了提前暴露bean的工厂,接下来我们会从源码层面详细介绍bean工厂和3级缓存。