第六篇 再读Spring 之 循环依赖处理

第六篇 再读Spring 之 循环依赖处理,第1张


文章目录
  • 前言
  • 一、关于循环依赖
    • 1. 什么是循环依赖
    • 2. 具体有哪几种
  • 二、文件级别
    • 1、样例场景
    • 2、识别方式
    • 3、处理逻辑
  • 三、 对象级别
    • 1、@DependsOn/depends-on循环依赖
      • a、样例场景
      • b、识别方式
      • c、处理逻辑
    • 2、构造器循环依赖
      • a、样例场景
      • b、识别方式
      • c、处理逻辑
    • 3、setter循环依赖
      • a、样例场景
      • b、识别方式&处理逻辑
  • 总结


前言

本文就Spring中循环依赖的场景和处理方式做汇总和探讨。


一、关于循环依赖 1. 什么是循环依赖

举个栗子,二元关系中A, B之间的循环依赖

使用数据结构中的图来抽象描述下,就是是一个有向有环图。基于有向有环的原则,我们可以轻松构造出4元关系,5元关系,甚至N元关系下的循环依赖。

假设每个节点代表任务,依赖标识任务完成的前后顺序,这张图就是一个任务调度图,大数据处理框架Spark就是用有向无环图来做任务调度的。具体到Spring中,每个节点表示的某个具体对象的初始化任务。所以,按照正常的分析,当出现循环依赖的时候任务永远无法完成。因此,在实际的任务调度中应该避免循环依赖出现。

但是,Spring结合具体的使用场景做了必要的优化。在聊优化之前,有必要看看在Spring循环依赖的场景有哪些?个人也是基于源码阅读汇总出来,如有不妥欢迎拍砖。

2. 具体有哪几种

(不想看文字,看图就OK,一共是2个大类,4个小类)

个人总结下来有配置文件级别,和对象级别的两大类。其中对象级别包含3个小类,分别是构造器依赖,dependsOn依赖和setter依赖。按照细粒度划分一共是4类。到这里,我们聊了循环依赖的种类。接下来,针对每一种聊聊如何解决。

二、文件级别

也就是多个配置文件之间循环依赖。

1、样例场景

通过import标签,或者@import注解触发。下面展示三个文件产生的循环依赖,application_service.xml, application_dao.xml, application_util.xml。最终结果就是这个样子的。

配置内容样例如下

application_service.xml

<beans>
<import resource="application_dao.xml" />
<bean id="A" class="xxxx">bean>
beans>

application_dao.xml

<beans>
<import resource="application_util.xml" />
<bean id="B" class="xxxx">bean>
beans>

application_util.xml

<beans>
<import resource="application_service.xml" />
<bean id="C" class="xxxx">bean>
beans>
2、识别方式

Spring并没有在文件解析层面做特别处理,而是下沉到BeanDefinition解析阶段。因为Spring要求Bean ID在整个BeanFactory中是唯一的。当出现循环依赖时,该唯一性规则被打破。

3、处理逻辑

抛出异常,终止容器初始化过程,由框架使用者解决循环依赖。

三、 对象级别 1、@DependsOn/depends-on循环依赖

产生方式和import类似,此处不再赘述。这里聊下dependsOn的使用场景。

a、样例场景

通常dependson依赖仅仅用于声明对象的初始化顺序,比如A dependson B , 在spring容器中意味着B要先于A 完成初始化。但同时,A和B并不是直接关联,而是在某些条件下产生关联。比如在发布-订阅模式中,publisher和listener在各自的初始化阶段可以独立开始,但是在listener去listen publisher之前,publisher必须初始化完成。从对象构造逻辑来说,两者是独立的–在互相无视的情况下,两个对象都可以正常构造完成。站在业务逻辑角度,两者又有依赖关系的。

b、识别方式

BeanFactory中维护了两个map:
dependentBeanMap 记录了 beanName 和 依赖"我"的 beanName集合
dependencyBeanMap 记录了 beanName 和 “我”依赖的 beanName集合

这里的依赖,都是直接依赖,但是可以解决问题。比如A 依赖B,B依赖C,C依赖A。Spring不能做到开始初始化A的时候发现循环依赖,而是在初始化C的时候发现循环依赖。对于@DependsOn声明,dependentBeanMap中获取依赖"我"集合,结合该集合判断 我依赖的beanName,在依赖我的beanName是否存在。如果存在,则存在循环依赖。

c、处理逻辑

BeanFactory中直接抛出异常,由框架使用者解决。

具体代码在AbstractFactory中,如下

protected  T doGetBean(
			String name, 
			@Nullable Class requiredType, 
			@Nullable Object[] args, 
			boolean typeCheckOnly)
			throws BeansException {
            // 忽略部分代码

               // Guarantee initialization of beans that the current bean depends on.
				String[] dependsOn = mbd.getDependsOn();
				if (dependsOn != null) {
					for (String dep : dependsOn) {
						if (isDependent(beanName, dep)) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
						}
						registerDependentBean(dep, beanName);
						try {
							getBean(dep);
						}
						catch (NoSuchBeanDefinitionException ex) {
							throw new BeanCreationException(mbd.getResourceDescription(), beanName,
									"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
						}
					}
				}


               // 忽略部分代码
} 
2、构造器循环依赖

也就是对象构造函数之间产生循环依赖。

a、样例场景

A.java

public class A {
     private B bInstance;
     public A(B b) {
          this.bInstance = b;
      }
}

B.java

public class B {
     private C cInstance;
     public B(C c) {
          this.cInstance = c;
      }
}

class C

public class C {
     private A aInstance;
     public C(A a) {
          this.aInstance = a;
      }
}
b、识别方式

无论scope是singleton还是prototype,BeanFactory在初始化对象前会记录正在初始化中的beanName。singleton scope对应的是singletonsCurrentlyInCreation,prototype和其他scope对应的都是prototypesCurrentlyInCreation。并在初始化之前判断是否初始化中,如果正在初始化,则表明发现循环依赖。

c、处理逻辑

Spring不处理,解析构造函数的参数过程中直接抛出异常,对应类ConstructorResolver的私有方法createArgumentArray,具体异常类型如下:

org.springframework.beans.factory.UnsatisfiedDependencyException
3、setter循环依赖

对象可以构造出来,但是在属性设置环节存在循环依赖,发生在populateBean阶段。

a、样例场景

A.java

public class A {
     private B bInstance;
     public A() {
      }
      public void setB(B b) {
         this.bInstance = b;
      }
}

B.java

public class B {
     private A a;
     public B() { 
      }
     public void setA(A a) {
     			this.a = a;
     }
}
b、识别方式&处理逻辑

这里需要分3种情况。

  1. 都是singleton。我们知道Spring在对singleton初始化时,对象要在三级缓存之间移动,分别是singletonFactories,earlyInCreationObject和singletonObjects。其中earlyInCreationObject保存了处于早期阶段的对象。该阶段在构造函数完成之后,属性填充之前。此外,两个singleton之前需要set的具体对象框架层面是可感知的。因此,可以使用早期对象完成关联。整个逻辑下来,无视了循环依赖,也就不存在所谓的识别;

  2. A、B中有一个是singleton(假设A是singleton,B是prototype,初始化顺序是A、B)。Spring先创建对象A1,然后在初始化A1时创建B的对象B1,接着对B1做populateBean,此时A的早期对象已经暴露,因此B1可以顺利完成初始化,接下来继续初始化A1,也就是A1和B1关联,以上在容器构造阶段完成的。当应用程序从BeanFactory中获取B类的对象时,由于B的scope是prototype,因此容器建一个对象B2,B2关联的A是A1,但是对象A1由于已经初始化完成,因此仍然关联B1。这个case在容器层面没有任何问题,实际工程中需要结合场景判断是否有问题。

  3. A、B都是prototype。假设先初始化A。A在构造完成后,populateBean阶段需要去构造B,最终会在prototypesCurrentlyInCreation中找到初始化中的A,发现循环依赖,抛出异常:

org.springframework.beans.factory.UnsatisfiedDependencyException

综上,setter循环依赖中,singleton+singleton,singleton+prototype在容器层面都可以正常初始化,但是后者可能在业务逻辑层面可能有问题。prototype+prototype的组合会抛出异常。

总结

以上就是今天要讲的内容,本文总结了Spring中循环依赖的场景,识别方式和处理逻辑,希望能帮助你避免框架使用过程中的坑。

关于singleton的setter循环依赖处理过程参考https://blog.csdn.net/cristianoxm/article/details/113246104,
其中的动图非常生动形象,在此供参考

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/868716.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存