【Spring 源码解析】Spring 循环依赖解析

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  就是 ABC 对应的集合,curVal != nulltrue, curVal instanceof Settrue,
 并且 (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 就是 ABC 对应的集合,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。

以上就是关于循环依赖的内容,谢谢观看!希望有所收获,加油,共勉!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值