1、前言
面试的时候被问到的Spring相关问题,循环依赖一定有一席之地。
到底spring 是如何处理循环依赖的?
这篇文章就对这个问题解析下。
2、实例展示
先写下循环依赖的案例情况,然后对这个问题进行验证。
我们知道spring的注入方式有两种,分别为setter注入和构造器注入。
而作用域又分为singleton,prototype,和其他(request,session等)。
下面依次验证。
- 循环案例
class A :
public class A {
private B b;
public A() {
System.out.println("A() ...");
}
public A(B b) {
System.out.println("A(B b) ...");
this.b = b;
}
public B getB() {
System.out.println("getB() ...");
return b;
}
public void setB(B b) {
System.out.println("setB(B b) ...");
this.b = b;
}
}
class B :
public class B {
private C c;
public B() {
System.out.println("B() ...");
}
public B(C c) {
System.out.println("B(C c) ...");
this.c = c;
}
public C getC() {
System.out.println("getC() ...");
return c;
}
public void setC(C c) {
System.out.println("setC(C c) ...");
this.c = c;
}
}
class C:
public class C {
private A a;
public C() {
System.out.println("C() ...");
}
public C(A a){
System.out.println("C(A a) ...");
this.a = a;
}
public A getA() {
System.out.println("getA() ...");
return a;
}
public void setA(A a) {
System.out.println("setA(A a) ...");
this.a = a;
}
}
以上粘贴比较多的原因是为了打印到控制台,分别添加了输出日志。
然后是通过xml配置到spring管理:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="a" class="com.fankf.bean.A">
<property name="b" ref="b"/>
</bean>
<bean id="b" class="com.fankf.bean.B">
<property name="c" ref="c"/>
</bean>
<bean id="c" class="com.fankf.bean.C">
<property name="a" ref="a"/>
</bean>
</beans>
测试代码:
@Test
public void test7() {
BeanFactory context = new XmlBeanFactory(new ClassPathResource("bean7.xml"));
A a = (A) context.getBean("a");
System.out.println("【实例使用】"+a);
}
执行结果:
A() ...
B() ...
C() ...
setA(A a) ...
setC(C c) ...
setB(B b) ...
【实例使用】com.fankf.bean.A@293a5bf6
也就是setter 注入解决了循环依赖的问题。
- 构造器注入
以上是通过setter方式注入,下面说下构造器注入:
XML配置文件修改成:
<bean id="a" class="com.fankf.bean.A">
<!--<property name="b" ref="b"/>-->
<constructor-arg name="b" ref="b"/>
</bean>
<bean id="b" class="com.fankf.bean.B">
<!--<property name="c" ref="c"/>-->
<constructor-arg name="c" ref="c"/>
</bean>
<bean id="c" class="com.fankf.bean.C">
<!--<property name="a" ref="a"/>-->
<constructor-arg name="a" ref="a"/>
</bean>
同样的测试代码运行,运行结果:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'a' defined in class path resource [bean7.xml]: Cannot resolve reference to bean 'b' while setting constructor argument;
nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'b' defined in class path resource [bean7.xml]: Cannot resolve reference to bean 'c' while setting constructor argument;
nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'c' defined in class path resource [bean7.xml]: Cannot resolve reference to bean 'a' while setting constructor argument;
nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
这里就报错提示 : BeanCurrentlyInCreationException 异常,并 Is there an unresolvable circular reference? 提示是否是循环依赖。
也就是构造注入的方式spring不能解决循环依赖的问题,直接报错提醒。
- 原型模式
xml 配置修改:
<bean id="a" class="com.fankf.bean.A" scope="prototype">
<property name="b" ref="b"/>
</bean>
<bean id="b" class="com.fankf.bean.B" scope="prototype">
<property name="c" ref="c"/>
</bean>
<bean id="c" class="com.fankf.bean.C" scope="prototype">
<property name="a" ref="a"/>
</bean>
执行后和单例构造方式报错相同。也就是原型模型下无法解决循环依赖。
3、源码解析
我们这里分析下setter注入方式可以成功,而构造注入和原型模式不行的原因。
这个说到处理Spring 循环依赖,其实还是需要知道Spring Bean在不同的情况下是如何加载的。
这里可以看下我之前的几篇文章关于Spring bean的加载做了初步的说明。这里的关注点是创建bean,
这里说下关于循环依赖的问题,仍然查看之前获取bean的方法:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException
- 原型模型
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return (curVal != null &&
(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}
当创建A的时候,先放入 prototypesCurrentlyInCreation 代表正在创建的bean缓存。
(1) 首先创建A ,放入了A 对应的bean
(2) 然后A 依赖于 B,去创建B,放入了B对应的bean
(3) 然后 B 依赖于C,去创建C,放入了C对应的bean
(4) 最后C 依赖于 A, 去创建A,这个时候 curVal 就是 A、B、C 对应的集合,curVal != null 为 true, curVal instanceof Set 为true,
并且 (Set<?>) curVal).contains(beanName) 为 true, 也就是 curVal 已经包含了A,返回true
(5) isPrototypeCurrentlyInCreation(beanName) 为 true, 抛出异常 BeanCurrentlyInCreationException
这个时候我们发现原型模式的错误原因知道了,那么缓存时什么地方放入的呢?
查看创建原型模式bean的代码,然后就可以很明显的看到放入缓存的位置 beforePrototypeCreation:
if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
protected void beforePrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal == null) {
this.prototypesCurrentlyInCreation.set(beanName);
}
else if (curVal instanceof String) {
Set<String> beanNameSet = new HashSet<>(2);
beanNameSet.add((String) curVal);
beanNameSet.add(beanName);
this.prototypesCurrentlyInCreation.set(beanNameSet);
}
else {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.add(beanName);
}
}
- 单例构造情况
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// ... 省略,关注重点
beforeSingletonCreation(beanName);
// ... 省略,关注重点
}
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
构造器模式下, 和原型模式的类似都是 singletonsCurrentlyInCreation 放入正在创建的缓存:
(1) 首先创建A ,放入了A 对应的bean
(2) 然后A 依赖于 B,去创建B,放入了B对应的bean
(3) 然后 B 依赖于C,去创建C,放入了C对应的bean
(4) 最后C 依赖于 A, 去创建A,这个时候 singletonsCurrentlyInCreation 就是 A、B、C 对应的集合,singletonsCurrentlyInCreation.add(beanName) 返回false,inCreationCheckExclusions.contains(beanName) 一直时false,则进入方法抛出 BeanCurrentlyInCreationException 异常
上面我们看到对于inCreationCheckExclusions 假如放入A、B、C防止循环依赖行不行?
更改下测试代码:
@Test
public void test7() {
AbstractBeanFactory context = new XmlBeanFactory(new ClassPathResource("bean7.xml"));
context.setCurrentlyInCreation("a",false);
context.setCurrentlyInCreation("b",false);
context.setCurrentlyInCreation("c",false);
A a = (A) context.getBean("a");
System.out.println("【实例使用】"+a);
}
由于 setCurrentlyInCreation 是 DefaultSingletonBeanRegistry 的方法,而 AbstractBeanFactory 继承了此类,这个地方用 AbstractBeanFactory ,之后设置 a,b,c 防止循环依赖,然后执行测试:
java.lang.NoClassDefFoundError: Could not initialize class org.springframework.beans.factory.BeanCreationException
由于拿不到必须的属性类,直接报错。所以通过构造方法spring无法解决循环依赖。
- setter注入
构造器注入不行,而setter注入是如何解决的?
其实就是 setter注入和构造注入的区别在哪?
Spring有种特性,就是假如先创建 class ,那么就必须先创建 class的依赖,而setter注入没有依赖,使用的是无参构造,从而导致直接创建bean即可,也就避免了创建构造器参数导致的循环依赖的问题。
这样也会存在一个很有意思的现象:
@Test
public void test7() {
BeanFactory context = new XmlBeanFactory(new ClassPathResource("bean7.xml"));
A a = (A) context.getBean("a");
System.out.println("【实例使用】"+a);
System.out.println("【实例使用】"+a.getB().getC().getA().getB().getC());
}
测试结果:
C() ...
setA(A a) ...
setC(C c) ...
setB(B b) ...
【实例使用】com.fankf.bean.A@130d63be
getB() ...
getC() ...
getA() ...
getB() ...
getC() ...
【实例使用】com.fankf.bean.C@42a48628
也就是循环一圈,我仍然可以拿到自己。甚至可以无限循环下去 !
4、总结
对于循环依赖总结一些:
循环依赖的原因: Spring有种特性,就是假如先创建 class ,那么就必须先创建 class的依赖
这个性质导致 构造器注入和原型模型抛出异常!
setter 注入未抛出异常的原因:
setter注入的时候没有依赖,无需创建对应的class。
以上就是关于循环依赖的内容