秃头系列-Spring循环依赖

129 阅读6分钟

「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

前言

  • 关于作者:励志不秃头的一个CURD的Java农民工
  • 关于文章:Spring循环依赖,面试和工作中总绕不过去的一个坎,希望下面的文章对你有一点点帮助

Spring循环依赖

什么时循环依赖

就是有个Bean A依赖于 Bean B;Bean B初始化又依赖于 Bean A;当new A() 的时候需要用到B,于是去new B() ,又需要用到A,形成死循环

Spring的依赖注入方式

  1. 构造器注入
  2. Setter 方法注入(支持解决循环依赖)
  3. 接口注入

面试题:三级缓存分别是什么?三个Map有什么异同 **

三级缓存分别对应的是三个Map;分别是singletonObjects, earlySingletonObjects, singletonFactories,主要为了去解决Bean初始化中的循环依赖问题

一级缓存

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。

二级缓存

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完。)

三级缓存

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂

注意: 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

Spring解决循环依赖过程 ***:

  1. 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
  2. getSingleton()方法中,从一级缓存中查找,没有,返回null
  3. doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
  4. 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
  5. 进入AbstractAutowireCapableBeanFactory#ndoCreateBean,先反射调用构造器创建出beanA的实例,然后判断:是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中(即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
  6. 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
  7. 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
  8. 此时 beanB依赖于beanA,调用getSingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
  9. 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
  10. 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中

面试题:为什么一定要用三级缓存?***

因为一级缓存、二级缓存无法解决问题

一级缓存

如果只留第一级缓存,那么单例的Bean都存在singletonObjects 中,Spring循环依赖主要基于Java引用传递,当获取到对象时,对象的field或者属性可以延后设置,理论上可以,但是如果延后设置出了问题,就会导致完整的Bean和不完整的Bean都在一级缓存中,这个引用时就有空指针的可能,所以一级缓存不行,至少要有singletonObjects 和earlySingletonObjects 两级。

二级缓存

现在有A的field或者setter依赖B的实例对象,同时B的field或者setter依赖了A的实例,A首先开始创建,并将自己暴露到 earlySingletonObjects 中,开始填充属性,此时发现自己依赖B的属性,尝试去get(B),发现B还没有被创建,所以开始创建B,在进行属性填充时初始化A,就从earlySingletonObjects 中获取到了实例化但没有任何属性的A,B拿到A后完成了初始化阶段,将自己放到singletonObjects中,此时返回A,A拿到B的对象继续完成初始化,完成后将自己放到singletonObjects中,由A与B中所表示的A的属性地址是一样的,所以A的属性填充完后,B也获取了A的属性,这样就解决了循环的问题。

似乎完美解决,如果就这么使用的话也没什么问题,但是再加上AOP情况就不同了,被AOP增强的Bean会在初始化后代理成为一个新的对象,也就是说:

如果有AOP,A依赖于B,B依赖于A,A实例化完成暴露出去,开始注入属性,发现引用B,B开始实例化,使用A暴露的对象,初始化完成后封装成代理对象,A再将代理后的B注入,再做代理,那么代理A中的B就是代理后的B,但是代理后的B中的A是没用代理的A。

显然这是不对的,所以在Spring中存在第三级缓存,在创建对象时判断是否是单例,允许循环依赖,正在创建中,就将其从earlySingletonObjects中移除掉,并在singletonFactories放入新的对象,这样后续再查询beanName时会走到singletonFactory.getObject(),其中就会去调用各个beanPostProcessor的getEarlyBeanReference方法,返回的对象就是代理后的对象。

是否可以关闭循环依赖?

根据doCreateBean方法,可以得知:判断是否支持循环依赖是根据allowCircularReferences这个布尔值来决定的

/** Whether to automatically try to resolve circular references between beans */
	private boolean allowCircularReferences = true;

从源码可以看出:

当前方法是属于AbstractAutowireCapableBeanFactory这个类的,这个类一看就知道是一个BeanFactory对象,其实如果我们可以修改spring源码的话,在容器初始化之前,也就是refresh方法执行之前,我们可以加这样一行代码修改这个值的属性来关闭循环依赖:setAllowCircularReferences(false)

方法:

通过spring提供了api来供我们获取beanfactory,我们可以在容器刷新前调用:

public static void main(String[] args) {
		AnnotationConfigApplicationContext ac =
				new AnnotationConfigApplicationContext();
 
		AbstractAutowireCapableBeanFactory beanFactory = (AbstractAutowireCapableBeanFactory) ac.getBeanFactory();
		beanFactory.setAllowCircularReferences(false);
		ac.register(AppConfig.class);
		ac.refresh();
 
		System.out.println(ac.getBean("city"));
	}

Spring循环依赖其实并没有我们想象的复杂,只是平时我们都专注了业务的开发,并没有很多时间去搞懂它。希望以上文章对平时工作和面试时的你有一定的帮助。我是新生代农民工L_Denny,我们下篇文章见。