文章目录
- 前言
- 一、关于循环依赖
- 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种情况。
-
都是singleton。我们知道Spring在对singleton初始化时,对象要在三级缓存之间移动,分别是singletonFactories,earlyInCreationObject和singletonObjects。其中earlyInCreationObject保存了处于早期阶段的对象。该阶段在构造函数完成之后,属性填充之前。此外,两个singleton之前需要set的具体对象框架层面是可感知的。因此,可以使用早期对象完成关联。整个逻辑下来,无视了循环依赖,也就不存在所谓的识别;
-
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在容器层面没有任何问题,实际工程中需要结合场景判断是否有问题。
-
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,
其中的动图非常生动形象,在此供参考
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)