Bean的循环依赖
循环依赖概述
循环依赖:A对象中有B对象的引用,B对象中有A对象的引用,

public class Husband {
private String name;
private Wife wife;
}
public class Wife {
private String name;
private Husband husband;
}Bean的循环依赖
Bean 的作用域和注入方式的不同,循环依赖的处理结果也不同。
循环依赖处理不当,容易产生无限递归直至溢栈。
singleton + set注入
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="name" value="⼩花"/>
<property name="husband" ref="husbandBean"/>
</bean>在 singleton + set 注⼊的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
过程:
- singleton 下,对象实例化后会直接曝光
- 所有 singleton 的 Bean 实例化完且曝光后,才会进行 set 注入
曝光:对象实例化完,托管在容器内的过程。曝光完毕后,这个对象可从容器中获得。
- 曝光与set注入的先后,决定了set注入循环依赖的属性前,这个值(对象)已经在容器内部了,可以直接由容器获得,因此不会造成递归。
prototype + set注入
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
<property name="name" value="⼩花"/>
<property name="husband" ref="husbandBean"/>
</bean>当循环依赖的所有Bean的scope="prototype"的时候,产⽣的循环依赖,Spring是⽆法解决的,会出现BeanCurrentlyInCreationException异常。
过程:
- 当Bean的作用域为 prototype 时,容器只负责交付之前的生命周期,并不永久存储Bean。
- 当Bean的作用域为 prototype 时,每次从容器中获得的Bean都是新创建并交付的。
- 当循环依赖双方都是 prototype 时(多例时),set 注入的过程中会不断新建对象,从而引起递归,最终报错。
- 当循环依赖的一方是 singleton 时,set注入过程中并不会不断地新建对象,从而不会发生递归。
实例化B(singleton) -> 曝光B -> set注入B.a ->【实例化A(prototype) -> set 注入 A.b -> 容器直接交付B不新建 -> (A属性注入完毕)】 -> 完成注入B.a
singleton + 构造注入
<bean id="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wBean"/>
</bean>
<bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
<constructor-arg name="name" value="⼩花"/>
<constructor-arg name="husband" ref="hBean"/>
</bean>过程:
实例化 hBean -> 从容器内获取 wBean -> 容器内无 wBean -> hBean初始化失败 -> 报错。
Bean 的曝光在实例化完成后,当 A、B 双方都是构造方法完成注入时,会导致 Bean未曝光(实例化还未完成)导致 Bean 的缺失,从而实例化失败。
- 主要原因:构造⽅法注⼊会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在⼀起完成导致的。
prototype + 构造注入
<bean id="b-prototype-constructor" class="com.guitar.bean.B" scope="prototype">
<constructor-arg index="0" ref="a-prototype-constructor"></constructor-arg>
</bean>
<bean id="a-prototype-constructor" class="com.guitar.bean.A" scope="prototype">
<constructor-arg index="0" ref="b-prototype-constructor"></constructor-arg>
</bean>同上,构造注入时缺少Bean,实例化失败。
但不会在解析上下文的时候报错,而是在使用时(prototype使用时新建并交付)报错。
Spring 解决循环依赖的机理
循环依赖需要理清创建对象的过程,避免出现无限递归和Bean缺失。
总体上看,构造注入需要注意Bean是否缺失,而set注入需要注意是否无限递归(单例或多例)。
解决循环依赖,核心在于 singleton + set注入,singleton 保证了容器内对象的唯一,不会每次都新建,set 注入保证了Bean的实例化与属性注入的分离,从而使得Bean在第一时间内曝光(容器内可见)。
Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到⼀个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调⽤setter⽅法给属性赋值。 这样就解决了循环依赖的问题。
Spring 底层实例化与set注入分离的实现:

分析 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 源码可知,底层采用 Map 集合实现Bean的映射,三个重要的 Map:
singletonObjects:Cache of singleton objects: bean name to bean instance.
单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
earlySingletonObjects:Cache of early singleton objects: bean name to bean instance.
早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
singletonFactories:Cache of singleton factories: bean name to ObjectFactory.
单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
该类中还有一个方法:addSingletonFactory,用以提前曝光Bean

- 把 Bean 的工厂放入三级缓存中,曝光 Bean。
该类中还有一个方法:getSingleton,用以获取singleton作用域的Bean。

分析可知,对于 singleton Bean的获取,spring会先从⼀级缓存中获取Bean,如果获取不到,则从⼆级缓存中获取Bean,如果⼆级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
- 提前曝光,即存储Bean在三级缓存中。
- 从三级缓存中获取Bean时,会把Bean存储至二级缓存中(获取的Bean长什么样,是由对应的工厂决定的,而工厂取决于配置)。
总结:
Spring只能解决setter⽅法注⼊的单例bean之间的循环依赖。
ClassA依赖ClassB,ClassB⼜依赖 ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,⼜发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性 时,⼜发现需要ClassA的属性,但此时的ClassA已经被提前曝光加⼊了正在创建的bean的缓存中,则⽆需创建新的的ClassA的实例,直接从缓存中获取即可。从⽽解决循环依赖问题。
