面试题:Spring循环依赖
面试题:Spring循环依赖
什么是Spring循环依赖
简单来说就是两个或者更多的Bean之间互相依赖,比如BeanA需要BeanB的实例,BeanB需要BeanA的实例,看下面代码就明白了。
@Component
public class BeanA {
@Autowired
private BeanB beanB;
}
@Component
public class BeanB {
@Autowired
private BeanA beanA;
}
循环依赖的报错信息一般如下:
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| beanA defined in file [E:\lsl\learn\learn-workspace\spring-circular-dependency\target\classes\com\lsl\circular\dependency\bean\BeanA.class]
↑ ↓
| beanB defined in file [E:\lsl\learn\learn-workspace\spring-circular-dependency\target\classes\com\lsl\circular\dependency\bean\BeanB.class]
└─────┘
Spring是如何解决循环依赖的
简单描述一下Spring创建循环依赖的过程:
先创建一个A,这个时候A还没有完成属性填充,因为此时B还没创建完成,这个时候就会将这个不完成的A放到一个Map里面,接下来就去创建B,创建B的时候需要A,那么就可以从前面的Map中取出未完全创建的A来完成属性填充,这个时候B就是一个完整的B了,然后再回到A的属性填充,这个需要它就可以拿到一个完整的B来完成属性填充,最终A、B都完整了。

Spring解决循环依赖最关键的地方就是提前暴露未完全创建的Bean。Spring能够解决循环依赖需要满足两个前提条件:
- 循环依赖的Bean必须是单例对象
- 依赖注入的方式不能全是构造器注入,并且BeanName按字典序排序在前的不能是构造器注入。
为什么需要满足这两个前提条件
对于第一个条件,反过来想就很容易理解了,如果Bean都是原型模式(即scope取值prototype),那么创建BeanA0需要BeanB0,创建BeanB0时需要BeanA1,然后创建BeanA1又需要BeanB1了,这样就会无限循环下去了。从源码的角度来看,如果不是单例模式就会抛异常了。

对于第二个条件,如果全是构造器注入的话,构造函数BeanA(BeanB b);,那就是创建BeanA时需要new出一个BeanB,但是此时也需要new出一个BeanA,所以依旧无法解决循环依赖的问题;至于字典序的问题,Spring的创建Bean的时候是按照字典序创建的,所以一个是Setter注入,一个是构造器注入依旧存在无法解决循环依赖的问题。
public class A {
private B b;
/**
* 必须提供无参构造 否则即便是A使用setter注入 B使用构造器注入依旧无法解决循环依赖
*/
public A() {
}
public A(B b) {
this.b = b;
}
public void setB(B b) {
this.b = b;
}
public void aPrint() {
System.out.println("aPrint is called");
}
}
public class B {
private A a;
public B() {
}
public void setA(A a) {
this.a = a;
}
public B(A a) {
this.a = a;
}
public void bPrint() {
System.out.println("bPrint is called");
}
}
<?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">
<!-- a使用构造器注入 b使用setter注入 -->
<bean id="a" class="com.lsl.circular.dependency.bean.A">
<constructor-arg name="b" ref="b"/>
</bean>
<bean id="b" class="com.lsl.circular.dependency.bean.B">
<property name="a" ref="a" />
</bean>
</beans>
先简单说一下Bean的创建过程:加载bean信息–>实例化bean–>属性填充–>初始化阶段–>后置处理等步骤。对于上述代码,实例化A的时候Spring发现构造函数需要一个B,这个时候Spring就会去实例化B,然后对B做属性填充,但是这个时候Spring在Map里面找不到A,最终导致失败。
如果反过来则可以解决循环依赖,因为在实例化A时发现B还没创建,就会将A放入二级缓存中,之后实例化B,B从二级缓存中取出A完成属性填充,最后A再拿到完整的B完成属性填充就完事了。
<?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">
<!-- a使用setter注入 b使用构造器注入 -->
<bean id="a" class="com.lsl.circular.dependency.bean.A">
<property name="b" ref="b" />
</bean>
<bean id="b" class="com.lsl.circular.dependency.bean.B">
<constructor-arg name="a" ref="a" />
</bean>
</beans>
下面通过画图来解释为什么不提供无参构造会导致出现循环依赖


Spring解决循环依赖详细过程
单例模式下Spring解决依赖主要是靠三级缓存,其实就是三个Map。
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/**
* 一级缓存 存储完整的Bean,以beanName为key,单例Bean为value
* Cache of singleton objects: bean name to bean instance.
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
* 三级缓存 存储可以创建Bean的工厂,通过工厂创建的Bean会放到二级缓存
* Cache of singleton factories: bean name to ObjectFactory.
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/**
* 二级缓存 存储未完全创建的Bean
* Cache of early singleton objects: bean name to bean instance.
*/
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}
这三个Map工作流程大概如下:
- 获取单例Bean时先从singletonObjects(一级缓存)获取完整的Bean,如果能够获取得到则直接返回,否则往下走
- 先判断(isSingletonCurrentlyInCreation)Bean是否创建中,如果不是直接返回;是的话会去earlySingletonObjects(二级缓存)查找Bean,找得到直接返回,找不到往下走
- 获取锁,然后依次从一二级缓存中再次获取一次Bean,如果获取不到会从三级缓存获取Bean工厂,工厂存在则获取Bean然后将其放入二级缓存,并将其Bean工厂从三级缓存中移除。
- 如果三个缓存都找不到就会返回null
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从一级缓存获取bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 从二级缓存获取bean
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 防止多线程环境创建同一个Bean
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 从三级缓存获取创建bean的工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 放入二级缓存并将其Bean工厂从三级缓存移除,从三级缓存删除工厂是为了确保缓存的一致性,避免Bean被重复创建
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
最后执行完这段代码后返回null就代表这个Bean还没有被创建,这个时候就会标记这个Bean正在创建中,最终会调用doCreateBean,在doCreateBean里面会给三级缓存放入一个Bean工厂。
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 这里放入Bean工厂
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
接下来就是属性注入,发现需要B,B在属性注入时就会执行getBean(A),之后就会发现A正在创建中,从二级缓存找A,找不到就去三级缓存里面通过Bean工厂拿到Bean,最终完成属性注入,后面就是初始化直到完成Bean的创建并放入一级缓存,然后就会再回到A的属性注入,这个时候拿到完整的B完成注入,之后流程与B一样。
Spring为什么需要二级缓存
假设Spring只有一级缓存,能解决循环依赖吗?
解决是可以解决,只是有缺陷,当出现循环依赖是A在属性注入的时候发现需要B,完全可以把创建未完成的A放入一级缓存,然后在创建B的时候直接在缓存里面拿到A完成属性注入,这个时候B创建完成了放入一级缓存,然后A拿到完整的B也顺利完成属性注入并最终创建完毕。
表面上是可以,但是如果在B还没有创建完毕的时候如果有其他用户来取了A,这个时候A还未完成属性注入就有可能造成NPE。
Spring为什么需要三级缓存
三级缓存是为了AOP代理而存在的,如果A存在AOP代理,那么B里面属性注入的A并不是A本身,而是A的代理对象。但是Spring是无法提前预知这个对象是否存在代理的,根据Spring的Bean的生命周期,在不出现循环依赖的情况下,Spring的代理对象都是在Bean创建完毕后,也就是能够放入一级缓存后才创建的代理,这个时候Spring有两种选择:
- 不管有没有循环依赖都提前创建好代理对象放入缓存,出现循环依赖时自行取出代理对象完成属性注入
- 不提前创建好代理对象,出现循环依赖时才生成,这样Bean就可以按照Spring设计的生命周期来创建。
Spring选择了第二种,那么Spring是怎么做到提前暴露引用却不创建代理对象呢?
Spring选择了套娃再加一个中间层,也就是提前暴露了ObjectFactory对象,在属性注入的时候通过ObjectFactory.getObject()生成代理对象,并最终将代理对象放入二级缓存。
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
上面是提前暴露引用的源码,可以看到如果不存在代理最终Bean是不会产生任何变化就返回的,这个工厂的作用就是判断对象是否需要代理,不需要则原样返回,需要则返回代理对象。但是这和三级缓存有什么关系呢,完全可以在放到二级缓存的时候判断这个Bean是否需要代理,如果需要那就把代理对象放进去。但是这样就会违背了Spring设计的Bean生命周期了,因为代理对象是在初始化后才生成的。
