下面我们来看看面试官的几个关于spring bean循环的问题
- 什么是循环依赖?
- 如何检测是否存在循环依赖?
- 如何解决循环依赖?
- 多例的情况下,循环依赖问题为什么无法解决?
- 单例的情况下,虽然可以解决循环依赖,是否存在其他问题?
- 为什么采用三级缓存解决循环依赖?如果直接将早期bean丢到二级缓存可以么?
前面4个ok的,超越了80%的人,后面2个难度指数递增,能回答出来的算是千分之一,如果能回答上来,会让面试官相当佩服你的
什么是循环依赖
这个很好理解,多个bean之间相互依赖,形成了一个闭环。
比如:A依赖于B、B依赖于C、C依赖于A。
代码中表示:
class A {B b;
}class B {C c;
}class C {A a;
}
Spring 如何检测循环依赖
检测循环依赖比较简单,使用一个列表来记录正在创建中的bean,bean创建之前,先去记录中看一下自己是否已经在列表中了,如果在,说明存在循环依赖,如果不在,则将其加入到这个列表,bean创建完毕之后,将其再从这个列表中移除。
protected void beforeSingletonCreation(String beanName) {if (!this.inCreationCheckExclusions.contains(beanName) &&!this.singletonsCurrentlyInCreation.add(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}
}
singletonsCurrentlyInCreation 就是用来记录目前正在创建中的bean名称列表,this.singletonsCurrentlyInCreation.add(beanName) 返回 false ,说明beanName已经在当前列表中了,此时会抛循环依赖的异常 BeanCurrentlyInCreationException
上面是单例bean检测循环依赖的源码,再来看看非单例bean的情况。
以prototype情况为例,源码位于org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 方法中,将主要代码列出来看一下:
//检查正在创建的bean列表中是否存在beanName,如果存在,说明存在循环依赖,抛出循环依赖的异常
if (isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);
}
//判断scope是否是prototype
if (mbd.isPrototype()) {Object prototypeInstance = null;try {//将beanName放入正在创建的列表中beforePrototypeCreation(beanName);prototypeInstance = createBean(beanName, mbd, args);}finally {//将beanName从正在创建的列表中移除afterPrototypeCreation(beanName);}
}
Spring如何解决循环依赖的问题
spring创建bean主要的几个步骤:
- 步骤1:实例化bean,即调用构造器创建bean实例
- 步骤2:填充属性,注入依赖的bean,比如通过set方式、@Autowired注解的方式注入依赖的bean
- 步骤3:bean的初始化,比如调用init方法等。
从上面3个步骤中可以看出,注入依赖的对象,有2种情况:
- 通过步骤1中构造器的方式注入依赖
- 通过步骤2注入依赖
构造器的情况比较容易理解,实例化ServiceA的时候,需要有serviceB,而实例化ServiceB的时候需要有serviceA,构造器循环依赖是无法解决的,大家可以尝试一下使用编码的方式创建上面2个对象,是无法创建成功的!
再来看看非构造器的方式注入相互依赖的bean,以set方式注入为例,下面是2个单例的bean:mather和father:
@Service
public class Father {@Getterprivate Mather mather;@Autowiredpublic void setMather(Mather mather) {this.mather = mather;}public void method1() {System.out.println("Father of method!!!!");}
}
另外一个相互依赖的类
@Service
public class Mather {@Getterprivate Father father;@Autowiredpublic void setFather(Father father) {this.father = father;}public void method2() {System.out.println("mather of method");father.method1();}
}
由于单例bean在spring容器中只存在一个,所以spring容器中肯定是有一个缓存来存放所有已创建好的单例bean;获取单例bean之前,可以先去缓存中找,找到了直接返回,找不到的情况下再去创建,创建完毕之后再将其丢到缓存中,可以使用一个map来存储单例bean,比如下面这个
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
下面来看一下spring中set方法创建上面2个bean的过程
1.spring轮询准备创建2个bean:father和mather
2.spring容器发现singletonObjects中没有father
3.调用father的构造器创建father实例
4.father准备注入依赖的对象,发现需要通过setMather注入mather
5.father向spring容器查找mather
6.spring容器发现singletonObjects中没有mather
7.调用mather的构造器创建mather实例
8.mather准备注入依赖的对象,发现需要通过setFather注入father
9.mather向spring容器查找father
10.此时又进入步骤2了
卧槽,上面过程死循环了,怎么才能终结?
可以在第3步后加一个操作:将实例化好的father丢到singletonObjects中,此时问题就解决了。
spring中也采用类似的方式,稍微有点区别,上面使用了一个缓存,而spring内部采用了3级缓存来解决这个问题,我们一起来细看一下。
3级缓存对应的代码:
/** 第一级缓存:单例bean的缓存 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 第二级缓存:早期暴露的bean的缓存 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 第三级缓存:单例bean工厂的缓存 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
下面来看spring中具体的过程,我们一起来分析源码
开始的时候,获取father,会调用下面代码
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {String beanName = this.transformedBeanName(name);//1.查看缓存中是否已经有这个bean了Object sharedInstance = this.getSingleton(beanName);//@Object bean;if (sharedInstance != null && args == null) {if (this.logger.isTraceEnabled()) {if (this.isSingletonCurrentlyInCreation(beanName)) {this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");} else {this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'");}}bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition) null);} else {//若缓存中不存在,准备创建这个beanif (mbd.isSingleton()) {//2.下面进入单例bean的创建过程sharedInstance = this.getSingleton(beanName, () -> {try {return this.createBean(beanName, mbd, args);} catch (BeansException e) {this.destroySingleton(beanName);throw e;}});bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}return bean;}
}
@1:查看缓存中是否已经有这个bean了,如下:注意这里 allowEarlyReference 是true
/**** @param beanName bean名称* @param allowEarlyReference allowEarlyReference:是否允许从三级缓存singletonFactories中通过getObject拿到bean* @return bean*/
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {//1.先从一级缓存中找Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {//2.从二级缓存中找singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {//3.二级缓存中没找到 && allowEarlyReference为true的情况下,从三级缓存中找ObjectFactory<?> singletonFactory = (ObjectFactory) this.singletonFactories.get(beanName);if (singletonFactory != null) {//三级缓存返回的是一个工厂,通过工厂来获取创建beansingletonObject = singletonFactory.getObject();//将创建好的bean丢到二级缓存中this.earlySingletonObjects.put(beanName, singletonObject);//从三级缓存移除this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}
刚开始,3个缓存中肯定是找不到的,会返回null,接着会执行下面代码准备创建 father
if (mbd.isSingleton()) {sharedInstance = getSingleton(beanName, () -> { //@1try {//这里会实例化bean,将他丢到三级缓存中return createBean(beanName, mbd, args);}catch (BeansException ex) {destroySingleton(beanName);throw ex;}});
}
@1:进入 getSingleton 方法,而 getSingleton 方法代码比较多,为了方便大家理解,无关的代码我给剔除了,如下:
/**** @param beanName bean名称* @param singletonFactory bean 工厂对象,一个函数式接口,实现已经在lambda表达式中* 也就是调用singletonFactory.getObject(),会执行lambda表达式中的代码* @return*/
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {synchronized (this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {//单例bean创建之前调用,将其加入正在创建的列表中,上面有提到过,主要用来检测循环依赖用的beforeSingletonCreation(beanName);boolean newSingleton = false;try {//调用工厂创建bean,这里会实例化bean,将他丢到三级缓存中singletonObject = singletonFactory.getObject();//@1 会执行lambda表达式中的代码newSingleton = true;} finally {//单例bean创建之前调用,主要是将其从正在创建的列表中移除afterSingletonCreation(beanName);}if (newSingleton) {//将创建好的单例bean放入缓存中addSingleton(beanName, singletonObject);//@2}}return singletonObject;}
}
上面@1和@2是关键代码,先来看一下@1,这个是一个ObjectFactory类型的,从外面传入的,如下
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {BeanWrapper instanceWrapper = null;if (instanceWrapper == null) {//通过反射调用构造器实例化instanceWrapper = this.createBeanInstance(beanName, mbd, args);}//变量bean:表示刚刚同构造器创建好的bean示例Object bean = instanceWrapper.getWrappedInstance();Class<?> beanType = instanceWrapper.getWrappedClass();//判断是否需要暴露早期的bean,条件为(是否是单例bean && 当前容器允许循环依赖 && bean名称存在于正在创建的bean名称清单中)boolean earlySingletonExposure = mbd.isSingleton() && this.allowCircularReferences && this.isSingletonCurrentlyInCreation(beanName);if (earlySingletonExposure) {//若earlySingletonExposure为true,通过下面代码将早期的bean暴露出去this.addSingletonFactory(beanName, () -> this.getEarlyBeanReference(beanName, mbd, bean));//@1}}
这里需要理解一下什么是早期bean?
刚刚实例化好的bean就是早期的bean,此时bean还未进行属性填充,初始化等操作
@1 :通过 addSingletonFactory 用于将早期的bean暴露出去,主要是将其丢到第3级缓存中,代码如下:
/**** @param beanName bean的名字* @param singletonFactory lambda表达式*/
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {synchronized(this.singletonObjects) {//第1级缓存中不存在beanif (!this.singletonObjects.containsKey(beanName)) {//将其丢到第3级缓存中this.singletonFactories.put(beanName, singletonFactory);//后面的2行代码不用关注this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}
上面的方法执行之后,father就被丢到第3级的缓存中了。
后续的过程father开始注入依赖的对象,发现需要注入mather,会从容器中获取mather,而mather的获取又会走上面同样的过程实例化mather,然后将mather提前暴露出去,然后mather开始注入依赖的对象,mather发现自己需要注入father,此时去容器中找father,找father会先去缓存中找,会执行 getSingleton(“father”,true) ,此时会走下面代码,这个时候,father已经在三级缓存中了:
protected Object getSingleton(String beanName, boolean allowEarlyReference)
上面的方法走完之后,father会被放入二级缓存 earlySingletonObjects 中,会将father返回,此时mather中的father注入成功,mather继续完成创建,然后将自己返回给father,此时father通过set方法将mather注入。
father创建完毕之后,会调用 addSingleton 方法将其加入到缓存中,这块代码如下:
protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {//将bean放入第1级缓存中this.singletonObjects.put(beanName, singletonObject);//将其从第3级缓存中移除this.singletonFactories.remove(beanName);//将其从第2级缓存中移除this.earlySingletonObjects.remove(beanName);}
}
到此,两个bean的循环依赖注入就完成了。
下面捋一捋整个过程:
1.从容器中获取father
2.容器尝试从3个缓存中找father,找不到
3.准备创建father
4.调用father的构造器创建father,得到father实例,此时father还未填充属性,未进行其他任何初始化的操作
5.将早期的father暴露出去:即将其丢到第3级缓存singletonFactories中
6.father准备填充属性,发现需要注入mather,然后向容器获取mather
7.容器尝试从3个缓存中找mather,找不到
8.准备创建mather
9.调用mather的构造器创建mather,得到mather实例,此时mather还未填充属性,未进行其他任何初始化的操作
10.将早期的mather暴露出去:即将其丢到第3级缓存singletonFactories中
11.mather准备填充属性,发现需要注入father,然后向容器获取father
12.容器尝试从3个缓存中找father,发现此时father位于第3级缓存中,经过处理之后,father会从第3级缓存中移除,然后会存到第2级缓存中,然后将其返回给mather,此时father通过mather中的setFather方法被注入到mather中
13.mather继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除
14.mather将自己返回给father
15.father通过setMather方法将mather注入进去
16.father继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除
循环依赖无法解决的情况
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
探讨:为什么需要用3级缓存
如果只使用2级缓存,直接将刚实例化好的bean暴露给二级缓存出是否可以否?
答案肯定是不行的,spring是有他自己设计奇妙之处。
这样做是可以解决:早期暴露给其他依赖者的bean和最终暴露的bean不一致的问题。
若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样的,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的和最终的bean不是同一个bean的,将产生意向不到的效果,而三级缓存就可以发现这个问题,会报错。
单例bean解决了循环依赖,还存在什么问题
循环依赖的情况下,由于注入的是早期的bean,此时早期的bean中还未被填充属性,初始化等各种操
作,也就是说此时bean并没有被完全初始化完毕,此时若直接拿去使用,可能存在有问题的风险。