在关于 Spring 的面试中,我们经常会被问到一个问题:Spring 是如何解决循环依赖的问题的。这是个高频的面试题,本文主要针对这个问题,从以下几个方面进行讲解:
循环依赖其实就是循环引用,也就是两个或则两个以上的对象互相依赖,最终形成一个闭环。比如 A 依赖于 B,B 依赖于 C,C又依赖于 A 。
Spring 解决循环依赖是有前置条件的:
关于第一点,对于 原型 (Prototype) 作用域的 bean,Spring 容器无法完成依赖注入,因为 Spring 容器不进行缓存 prototype 作用域的 bean ,因此无法提前暴露一个创建中的 bean 。
原因也挺好理解的,原型模式每次请求都会创建一个实例对象,即使加了缓存,循环引用太多的话,就比较麻烦了,所以 Spring 不支持这种方式,直接抛出 BeanCurrentlyInCreationException 异常:
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
接下来我们根据 4 中测试方式来验证一下第一点。
第一种:A 和 B 都采用 setter 方法注入,最终结果是成功的。
第二种:A 和 B 都采用构造器方法注入,出现循环依赖
第三种:A 中注入 B 的方式为 setter 方法,B 中注入 A 的方式为构造器,最终结果是成功的。
第四种:A 中注入 B 的方式为构造器,B 中注入 A 的方式为 setter 方法
从上面的测试结果我们可以看到,不是只有在 setter 方法注入的情况下循环依赖才能被解决,即使存在构造器注入的场景下,循环依赖依然被可以被正常处理。
首先,Spring 内部维护了三个 Map,也就是我们通常说的三级缓存,我们可以注意到三级缓存与前两级缓存不太一样,Map 中维护的值是 ObjectFactory 类型。
//单例缓存池 beanName - instance 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
//bean的早期引用, bean name to bean instance 二级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
//单例工厂 beanName - ObjectFactory 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
这 3 个 Map 中的后两个其实是临时使用的,只是创建 Bean 的时候,用来借助了一下,创建完成就清掉了。
这里还要说明一下这个 singletonsCurrentlyInCreation,因为下面讲解变化过程需要使用到,它的意思是,表示当前正在创建的bean(仅仅存放名字 beanName),如果没创建完成,都会保存在这里面
下面我们接着看在创建 A,B 时它是如何从上述的缓存中变化的。
1、这里我们以 A 和 B 为例,我们首先创建 A,此时是第一次创建,默认四个容器中都是空的。
2、现在开始创建 A,此时需要往 singletonsCurrentlyInCreation 放入 A,表示 A 正在实例化,此时状态如下:
3、下面正式创建 A 到 A 创建完成,此时将 A 封装成 ObjectFactory 对象,但是属性还没有赋值,我们先定义名称为 aObject ,此时状态如下:
4、那么到这一步就要给 A 的属性赋值,了解循环依赖的小伙伴就知道,这里其实就是给 B 赋值,那么就要去创建 B,创建 B 的过程和创建 A 过程是一样的,依次从一级缓存、二级缓存、三级缓存中获取 B,很明显是获取不到的,那么就要创建 B,在singletonsCurrentlyInCreation 中加入 B,表示当前 B 正在创建,状态如下:
5、接下来的步骤和步骤 3 中差不多,B 创建完成,但是 B 中的属性 b 还没有赋值,此时将 B 封装成 bObject 放到三级缓存,状态如下:
6、此时开始给 B 的属性赋值,也就是给 a 赋值,那么它就要去创建 A,并且把 A 的内存地址给 B 中的 a 属性,那么创建 A 的过程和之前的一样,先依次从一级、二级、三级缓存中获取 A,那么这时可以从三级缓存中获取到 A 的,将获取到的 A 赋值给 B 的 a 属性,此时 B 即将创建完成了,在全部创建完的前一步,将三级缓存中的 B 移到二级缓存,因为实例化 B 的全部步骤全部做完了,此时的状态如下:
7、B 实例化以后,需要从当前正在创建的容器(singletonsCurrentInCreation)中移除 B,这步表示 B 已经完成了,并且同时将 B 移到一级缓存 singletonObjects 中,此时状态如下:
8、前面 B 已经创建完成,所以我们又到了给 A 的属性赋值的时候了,此时我们知道 B 已经创建完成了,所以 b ==B,此时 A 的实例化也快要结束了,A 也要从三级缓存移到二级缓存,过程和步骤 6 类型,此时的状态如下:
9、最后 A 也要做一些类似步骤 7 结束的动作 ,到这里 A 和 B 都已经全部实例化完成了,此时的状态如下:
我们进入 AbstractBeanFactory 的 getBean 跟 doGetBean方法。
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 方法1、从一级、二级、三级缓存中看看能否获取到
Object sharedInstance = getSingleton(beanName);
// 省略无关代码
}
else {
// 如果是多例的循环引用,则直接报错
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// 省略若干无关代码
try {
// Create bean instance.
if (mbd.isSingleton()) {
// 方法2、获取单例对象
sharedInstance = getSingleton(beanName, () -> {
try { //方法3、创建 ObjectFactory 中 getObject 方法的返回值
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);
}
}
// 省略若干无关代码
return (T) bean;
}
在 doGetBean 方法整个过程当中会调用三次 doGetBean() 方法,我们还是以 A 和 B 进行分析。
第一次调用的时候会尝试获取 A 对象实例,此时走的是第一个 getSingleton() 方法,因为没有已经创建的 A 对象,于是这里获得的是null。
而后就会调用第二个 getSingleton() 方法,建立 A 对象的实例,而后递归的调用 doGetBean() 方法,尝试获取 B 对象的实例以注入到 A 对象中。
此时因为 Spring 容器中也没有 B 对象实例,于是仍是会走到第二个 getSingleton() 方法,在该方法中建立 B 对象的实例,建立完成以后,尝试获取其所依赖的 A 的实例做为其属性,于是仍是会递归的调用 doGetBean() 方法。
此时需要注意的是,在前面因为已经有了一个 A 对象的实例,于是这个时候,再尝试获取 A 对象的实例的时候,会走第一个 getSingleton() 方法,在该方法中会获得一个 A 对象的实例。而后将该实例返回,而且将其注入到 B 对象的属性 a 中,此时 B 对象实例化完成。而后将实例化完成的 B 对象递归的返回,此时就会将该实例注入到 A 对象中,这样就获得了一个 A 对象。
这个方法比较长,对于解决循环引用来说,上面标出来的 3 个方法起到了至关重要的作用,下面我们会挨个讲解到的。
我们的流程进行到 AbstractBeanFactory 的 doGetBean 方法时,会执行 Object sharedInstance = getSingleton(beanName),接着会执行 getSingleton(beanName,true),一路跟进去,最终会进到DefaultSingletonBeanRegistry 的getSingleton方法,这个方法十分重要,我们具体看一看:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//先从一级缓存中获取已经实例化属性赋值完成的 bean
Object singletonObject = this.singletonObjects.get(beanName
// 如果没有从一级缓存中获取到,且对象在正在创建中
// 判断对象正在创建中的方法是判断 beanName 是不是在 singletonCurrentlyInCreation 这个 Set 里
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//从二级缓存中获取,获取 bean 的早期引用,实例化完成但是未赋值完成的 bean
singletonObject = this.earlySingletonObjects.get(beanName);
//如果二级缓存中没有获取到,并且允许创建早期引用(二级缓存中添加)
if (singletonObject == null && allowEarlyReference) {
//从三级缓存中获取,实例化完成,属性未装配完成
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//获取目标对象的实例
singletonObject = singletonFactory.getObject();
//将获取到的实例保存到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
//从三级缓存中移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
从源码可以得知,首先从一级缓存 singletonObjects 获取目标对象,若不存在且目标对象被标记为创建中,从二级缓存 earlySingletonObjects 中获取 bean。如果不存在,继续访问三级缓存 singletonFactories ,得到 bean 工厂对象,通过工厂对象获取目标对象。将目标对象放入二级缓存,删除三级缓存。
另外一个 Singleton 重载的方法:public Object getSingleton(String beanName, ObjectFactory> singletonFactory)
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
//调用 getObject 方法创建 bean 实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//重要步骤
//这个方法会将 beanName 添加到 singletonsCurrentlyInCreation 这个 Set 中
//这步添加操作会在后面调用 getSingletion(String, boolean )的时候起作用
beforeSingletonCreation(beanName);
boolean newSingleton = false;
//传入的 lambda 在这里会被执行,调用 createBean 方法创建一个 bean 后返回
singletonObject = singletonFactory.getObject();
newSingleton = true;
singletonObject = this.singletonObjects.get(beanName);
}
// 创建完成后将对应的 beanName 从 singletonsCurrentlyInCreation 移除
afterSingletonCreation(beanName);
}
if (newSingleton) {
//加入一级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
//添加到一级缓存
this.singletonObjects.put(beanName, singletonObject);
//三级缓存移除
this.singletonFactories.remove(beanName);
//二级缓存移除
this.earlySingletonObjects.remove(beanName);
//beanName 放进单例注册表中
this.registeredSingletons.add(beanName);
}
}
从上面两个方法中,我们可以看到 bean 实例是由 singletonFactory.getObject() 获取到的,也就是通过 doGetBean() 方法中判断是否单例后的匿名内部类获取到的,从而知道获取到的 bean 是由 createBean() 方法创建的。
creatBean() 方法调用了 doCreatBean() 方法,所以实际的创建逻辑就再 doCreatBean() 方法中。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
//bean 实例包装类
BeanWrapper instanceWrapper = null;
//创建 bean 的时候,这里创建 bean 的实例有三种方法
//1.工厂方法创建
//2.构造方法的方式注入
//3.无参构造方法注入
instanceWrapper = createBeanInstance(beanName, mbd, args);
//实例化后的 bean 对象,这里获取到的是一个早期对象,即没有进行属性填充的对象
Object bean = instanceWrapper.getWrappedInstance();
//解决循环依赖的关键步骤
//earlySingletonExposure 表示是否提前暴露早期对象的引用
//因为不论这个 bean 是否完整,他前后的引用都是一样的,所以提前暴露的引用到后来也指向完整的 bean
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));
//是否允许单例提前暴露
if (earlySingletonExposure) {
//如果一级缓存中没有当前 bean,就将当前 bean 放入三级缓存
//同样也会移除二级缓存中对应的 bean,即便没有
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
//此时 bean 已经实例化完成, 开始准备初始化
// bean 为原始对象
Object exposedObject = bean;
try {
//属性装配,属性赋值的时候,如果有发现属性引用了另外一个 bean,则调用 getBean 方法
populateBean(beanName, mbd, instanceWrapper);
//调用初始化方法,完成 bean 的初始化操作
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
//如果 bean 允许被早期暴露,进入代码
if (earlySingletonExposure) {
//第二参数为 false 表示不会从三级缓存中在检查,最多从二级缓存中找,其实二级缓存就够了,其实之前 getSingleton 的时候,已经触发了 A 的 ObjectFactory.getObject(),A 实例已经放入二级缓存中
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
//如果没有代理,进入这个分支
if (exposedObject == bean) {
exposedObject = earlySingletonReference; /
}
}
接着我们看一下 addSingletonFactory方法源码:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
//如果一级缓存中不存在
if (!this.singletonObjects.containsKey(beanName)) {
//添加到三级缓存中
this.singletonFactories.put(beanName, singletonFactory);
//从二级缓存中移除
this.earlySingletonObjects.remove(beanName);
//beanName 放进单例注册表中
this.registeredSingletons.add(beanName);
}
}
}
从上面源码中,我们知道 bean 在实例化完成之后会直接将未装配的 bean 工厂存放在三级缓存中,并且移除二级缓存。
我们以 A 依赖 B, B 依赖 A,即 A 和 B 相互依赖,我们从头到尾说一下这个流程,如下:
讲解完以上的源码怎么实现的,接下来需要处理以下两个问题:
从上面源码中流程可以看出来,在 bean 调用构造器实例化之前,一级、二级、三级缓存并没有 bean 的任何相关信息,在实例化之后才放入三级缓存中,因此当 getBean 的时候缓存并没有命中,这样就抛出了循环依赖的异常了。
由于 Spring 关于循环依赖的源码比较多,这里我只挑选了重点部分进行讲解, 其实主要思想就是利用二级、三级缓存对未初始化完成的 bean 进行提前的引用暴露,也就是将其设置为可引用的,这样当依赖于他的 bean 在进行属性填充时就可以直接拿到引用,解决了死循环的问题。