Spring循环依赖的三种⽅式以及解决办法
⼀. 什么是循环依赖?
循环依赖其实就是循环引⽤,也就是两个或者两个以上的bean互相持有对⽅,最终形成闭环。⽐如A依赖于B,B依赖于C,C⼜依赖于A。如下图:
注意,这⾥不是函数的循环调⽤,是对象的相互依赖关系。循环调⽤其实就是⼀个死循环,除⾮有终结条件。
Spring中循环依赖场景有:
(1)构造器的循环依赖
(2)field属性的循环依赖
其中,构造器的循环依赖问题⽆法解决,只能拋出BeanCurrentlyInCreationException异常,在解决属性循环依赖时,spring采⽤的是提前暴露对象的⽅法。
⼆. 怎么检测是否存在循环依赖
检测循环依赖相对⽐较容易,Bean在创建的时候可以给该Bean打标,如果递归调⽤回来发现正在创建中的话,即说明了循环依赖了。三、三种循环依赖
1:构造器的循环依赖。【这个Spring解决不了】
Spring容器会将每⼀个正在创建的Bean 标识符放在⼀个“当前创建Bean池”中,Bean标识符在创建过程中将⼀直保持在这个池中,因此如果在创建Bean过程中发现⾃⼰已经在“当前创建Bean池”⾥时将抛出BeanCurrentlyInCreationException异常表⽰循环依赖;⽽对于创建完毕的Bean将从“当前创建Bean池”中清除掉。
Spring容器先创建单例A,A依赖B,然后将A放在“当前创建Bean池”中,此时创建B,B依赖C ,然后将B
放在“当前创建Bean池”中,此时创建C,C⼜依赖A,但是,此时A已经在池中,所以会报错,因为在池中的Bean都是未初始化完的,所以会依赖错误,(初始化完的Bean会从池中移除)
public class StudentA {
private StudentB studentB ;
public void setStudentB(StudentB studentB) {
this.studentB = studentB;
}
public StudentA() {
}
public StudentA(StudentB studentB) {
this.studentB = studentB;
}
}
public class StudentB {
private StudentC studentC ;
public void setStudentC(StudentC studentC) {
this.studentC = studentC;
}
public StudentB() {
}
public StudentB(StudentC studentC) {
this.studentC = studentC;
}
}
public class StudentC {
private StudentA studentA ;
public void setStudentA(StudentA studentA) {
this.studentA = studentA;
}
public StudentC() {
}
public StudentC(StudentA studentA) {
this.studentA = studentA;
}
}
上⾯是很基本的3个类,,StudentA有参构造是StudentB。StudentB的有参构造是StudentC,StudentC的有参构造是StudentA ,这样就产⽣了⼀个循环依赖的情况,
我们都把这三个Bean交给Spring管理,并⽤有参构造实例化
<bean id="a" class="com.liuqing.student.StudentA">
<constructor-arg index="0" ref="b"></constructor-arg>
</bean>
<bean id="b" class="com.liuqing.student.StudentB">
<constructor-arg index="0" ref="c"></constructor-arg>
</bean>
<bean id="c" class="com.liuqing.student.StudentC">
<constructor-arg index="0" ref="a"></constructor-arg>
</bean>
下⾯是测试类:
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/l");
//System.out.Bean("a", StudentA.class));
}
}
执⾏结果报错信息为:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
2、setter⽅式单例,默认⽅式
Spring中Bean实例化的图
如图中前两步骤得知:Spring是先将Bean对象实例化【依赖⽆参构造函数】--->再设置对象属性的
实例化bean的三种方式修改配置⽂件为set⽅式注⼊:
<!--scope="singleton"(默认就是单例⽅式) -->
<bean id="a" class="com.liuqing.student.StudentA" scope="singleton">
<property name="studentB" ref="b"></property>
</bean>
<bean id="b" class="com.liuqing.student.StudentB" scope="singleton">
<property name="studentC" ref="c"></property>
</bean>
<bean id="c" class="com.liuqing.student.StudentC" scope="singleton">
<property name="studentA" ref="a"></property>
</bean>
下⾯是测试类:
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/l");
System.out.Bean("a", StudentA.class));
}
}
打印结果为:
com.liuqing.student.StudentA@1fbfd6
我们结合上⾯那张图看,Spring先是⽤构造实例化Bean对象,此时Spring会将这个实例化结束的对象放到⼀个Map中,并且Spring提供了获取这个未设置属性的实例化对象引⽤的⽅法。结合我们的实例来看,,当Spring实例化了StudentA、StudentB、StudentC后,紧接着会去设置对象的属性,此时Studen
tA依赖StudentB,就会去Map中取出存在⾥⾯的单例StudentB对象,以此类推,不会出来循环的问题
3、setter⽅式原型,prototype
修改配置⽂件为:
<bean id="a" class="com.liuqing.student.StudentA" scope="prototype">
<property name="studentB" ref="b"></property>
</bean>
<bean id="b" class="com.liuqing.student.StudentB" scope="prototype">
<property name="studentC" ref="c"></property>
</bean>
<bean id="c" class="com.liuqing.student.StudentC" scope="prototype">
<property name="studentA" ref="a"></property>
</bean>
scope="prototype" 意思是每次请求都会创建⼀个实例对象。两者的区别是:有状态的bean都使⽤Prototype作⽤域,⽆状态的⼀般都使⽤singleton单例作⽤域。
测试⽤例:
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/l");
//此时必须要获取Spring管理的实例,因为现在scope="prototype" 只有请求获取的时候才会实例化对象
System.out.Bean("a", StudentA.class));
}
}
打印结果:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
对于“prototype”作⽤域Bean,Spring容器⽆法完成依赖注⼊,因为“prototype”作⽤域的Bean,Spring容器不进⾏缓存,因此⽆法提前暴露⼀个创建中的Bean。
四、Spring怎么解决循环依赖
Spring的循环依赖的理论依据基于Java的引⽤传递,当获得对象的引⽤时,对象的属性是可以延后设置的。(但是构造器必须是在获取引⽤之前)
Spring的单例对象的初始化主要分为三步:
(1)createBeanInstance:实例化,其实也就是调⽤对象的构造⽅法实例化对象
(2)populateBean:填充属性,这⼀步主要是多bean的依赖属性进⾏填充
(3)initializeBean:调⽤spring xml中的init ⽅法。
从上⾯单例bean的初始化可以知道:循环依赖主要发⽣在第⼀、⼆步,也就是构造器循环依赖和field循
环依赖。那么我们要解决循环引⽤也应该从初始化过程着⼿,对于单例来说,在Spring容器整个⽣命周期内,有且只有⼀个对象,所以很容易想到这个对象应该存在Cache
中,Spring为了解决单例的循环依赖问题,使⽤了三级缓存。
这三级缓存分别指:
singletonFactories :单例对象⼯⼚的cache
earlySingletonObjects :提前暴光的单例对象的Cache
singletonObjects:单例对象的cache
在创建bean的时候,⾸先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。如果获取不到,并且对象正在创建中,就再从⼆级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存Object()(三级缓存)获取,如果获取到了则:从singletonFactories中移除,并放⼊earlySingletonObjects中。其实也就是从三级缓存移动到了⼆级缓存。
从上⾯三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级
cache。这个cache的类型是ObjectFactory。这⾥就是解决循环依赖的关键,发⽣在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调⽤了构造器)。这个对象已经被⽣产出来了,虽然还不完美(还没有进⾏初始化的第⼆步和第三步),但是已经能被⼈认出来了(根据对象引⽤能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让⼤家认识,让⼤家使⽤。
这样做有什么好处呢?让我们来分析⼀下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A⾸先完成了初始化的第⼀步,并且将⾃⼰提前曝光到singletonFactories中,此时进⾏初始化的第⼆步,发现⾃⼰依赖对象B,此时就尝试去get(B),发现B还没有被create,所以⾛create流程,B在初始化第⼀步的时候发现⾃⼰依赖了对象A,于是尝试get(A),尝试⼀级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试⼆级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将⾃⼰提前曝光了,所以B能够通过Object拿到A对象(虽然A还没有初始化完全,但是总⽐没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将⾃⼰放⼊到⼀级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成⾃⼰的初始化阶段2、3,最终A也完成了初始化,进去了⼀级缓存singletonObjects中,⽽且更加幸运的是,由于B拿到了A的对象引⽤,所以B现在hold住的A对象完成了初始化。
知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造⽅法中依赖了B的实例对象,同时B的构
造⽅法中依赖了A的实例对象”这类问题了!因为加⼊singletonFactories三级缓存的前提是执⾏了构造器,所以构造器的循环依赖没法解决。
五、总结
不要使⽤基于构造函数的依赖注⼊,可以通过以下⽅式解决:
1.在字段上使⽤@Autowired注解,让Spring决定在合适的时机注⼊
2.⽤基于setter⽅法的依赖注⼊。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论