高频面试题:Spring 如何解决循环依赖?

论坛 期权论坛 爱问     
yzls   2022-5-24 00:07   11229   20
在关于Spring的面试中,我们经常会被问到一个问题:Spring是如何解决循环依赖的问题的。
这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够一下子思考出个中奥秘。
本文主要针对这个问题,从源码的角度对其实现原理进行讲解。
1. 过程演示
关于Spring bean的创建,其本质上还是一个对象的创建,既然是对象,读者朋友一定要明白一点就是,一个完整的对象包含两部分:当前对象实例化和对象属性的实例化。
在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。
这个过程可以按照如下方式进行理解:



理解这一个点之后,对于循环依赖的理解就已经帮助一大步了,我们这里以两个类A和B为例进行讲解,如下是A和B的声明:
@Component
public class A {
  private B b;
  public void setB(B b) {
    this.b = b;
  }
}
@Component
public class B {
  private A a;
  public void setA(A a) {
    this.a = a;
  }
}
可以看到,这里A和B中各自都以对方为自己的全局属性。这里首先需要说明的一点,Spring实例化bean是通过ApplicationContext.getBean()方法来进行的。
如果要获取的对象依赖了另一个对象,那么其首先会创建当前对象,然后通过递归的调用ApplicationContext.getBean()方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。
这里我们以上面的首先初始化A对象实例为例进行讲解。
首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象
然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例
但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。
读者需要注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。
在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例
因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。
此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。
这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了
读者朋友可能会比较疑惑的是,前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象。
而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了
这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。
下面我们通过一个流程图来对这个过程进行讲解:



图中getBean()表示调用Spring的ApplicationContext.getBean()方法,而该方法中的参数,则表示我们要尝试获取的目标对象。
图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用绿色的箭头表示。
从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。
2. 源码讲解
对于Spring处理循环依赖问题的方式,我们这里通过上面的流程图其实很容易就可以理解
需要注意的一个点,Spring是如何标记开始生成的A对象是一个半成品,并且是如何保存A对象的。
这里的标记工作Spring是使用ApplicationContext的属性SetsingletonsCurrentlyInCreation来保存的,而半成品的A对象则是通过MapsingletonFactories来保存的
这里的ObjectFactory是一个工厂对象,可通过调用其getObject()方法来获取目标对象。在AbstractBeanFactory.doGetBean()方法中获取对象的方法如下:
protected  T doGetBean(final String name, @Nullable final Class requiredType,
    @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
  
  // 尝试通过bean名称获取目标bean对象,比如这里的A对象
  Object sharedInstance = getSingleton(beanName);
  // 我们这里的目标对象都是单例的
  if (mbd.isSingleton()) {
   
    // 这里就尝试创建目标对象,第二个参数传的就是一个ObjectFactory类型的对象,这里是使用Java8的lamada
    // 表达式书写的,只要上面的getSingleton()方法返回值为空,则会调用这里的getSingleton()方法来创建
    // 目标对象
    sharedInstance = getSingleton(beanName, () -> {
      try {
        // 尝试创建目标对象
        return createBean(beanName, mbd, args);
      } catch (BeansException ex) {
        throw ex;
      }
    });
  }
  return (T) bean;
}这里的doGetBean()方法是非常关键的一个方法(中间省略了其他代码),上面也主要有两个步骤
第一个步骤的getSingleton()方法的作用是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象;如果第一个步骤没有获取到目标对象的实例,那么就进入第二个步骤
第二个步骤的getSingleton()方法的作用是尝试创建目标对象,并且为该对象注入其所依赖的属性。
这里其实就是主干逻辑,我们前面图中已经标明,在整个过程中会调用三次doGetBean()方法
第一次调用的时候会尝试获取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对象。
我们这里可以阅读上面的第一个getSingleton()方法:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  
  // 尝试从缓存中获取成品的目标对象,如果存在,则直接返回
  Object singletonObject = this.singletonObjects.get(beanName);
  
  // 如果缓存中不存在目标对象,则判断当前对象是否已经处于创建过程中,在前面的讲解中,第一次尝试获取A对象
  // 的实例之后,就会将A对象标记为正在创建中,因而最后再尝试获取A对象的时候,这里的if判断就会为true
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
   
    synchronized (this.singletonObjects) {
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
        
        // 这里的singletonFactories是一个Map,其key是bean的名称,而值是一个ObjectFactory类型的
        // 对象,这里对于A和B而言,调用图其getObject()方法返回的就是A和B对象的实例,无论是否是半成品
        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
         
          // 获取目标对象的实例
          singletonObject = singletonFactory.getObject();
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}这里我们会存在一个问题就是A的半成品实例是如何实例化的,然后是如何将其封装为一个ObjectFactory类型的对象,并且将其放到上面的singletonFactories属性中的。
这主要是在前面的第二个getSingleton()方法中,其最终会通过其传入的第二个参数,从而调用createBean()方法,该方法的最终调用是委托给了另一个doCreateBean()方法进行的
这里面有如下一段代码:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
  throws BeanCreationException {
  // 实例化当前尝试获取的bean对象,比如A对象和B对象都是在这里实例化的
  BeanWrapper instanceWrapper = null;
  if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
  }
  if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  }
  // 判断Spring是否配置了支持提前暴露目标bean,也就是是否支持提前暴露半成品的bean
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
    && isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
   
    // 如果支持,这里就会将当前生成的半成品的bean放到singletonFactories中,这个singletonFactories
    // 就是前面第一个getSingleton()方法中所使用到的singletonFactories属性,也就是说,这里就是
    // 封装半成品的bean的地方。而这里的getEarlyBeanReference()本质上是直接将放入的第三个参数,也就是
    // 目标bean直接返回
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }
  try {
    // 在初始化实例之后,这里就是判断当前bean是否依赖了其他的bean,如果依赖了,
    // 就会递归的调用getBean()方法尝试获取目标bean
    populateBean(beanName, mbd, instanceWrapper);
  } catch (Throwable ex) {
    // 省略...
  }
  return exposedObject;
}到这里,Spring整个解决循环依赖问题的实现思路已经比较清楚了。对于整体过程,读者朋友只要理解两点:

  • Spring是通过递归的方式获取目标bean及其所依赖的bean的;
  • Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
End
作者:爱宝贝丶
来源:https://my.oschina.net/zhangxufeng/blog/3096394

免费分享java技术资料,需要的朋友在关注后私信“资源”即可免费获取
分享到 :
0 人收藏

20 个回复

倒序浏览
2#
ymodq  1级新秀 | 2022-5-24 00:08:01 发帖IP地址来自 北京
躲不开的呀,循环依赖……你一个bean的确需要这么多的其他bean来创建
3#
ei73i  1级新秀 | 2022-5-24 00:08:11 发帖IP地址来自 北京
那么为何会出现一样的代码,有时启动成功,有时启动会报循环依赖错误?
4#
14lie  1级新秀 | 2022-5-24 00:08:26 发帖IP地址来自 湖北
这种题我是不敢问的,问了90%的人答不出
5#
shensu9  禁止访问 | 2022-5-24 00:08:52 发帖IP地址来自 中国
提示: 作者被禁止或删除 内容自动屏蔽
6#
期权超人  2级吧友 | 2022-5-24 00:09:43 发帖IP地址来自 中国
那是bean的设计有问题,从设计角度上出现循环依赖就是个badsmell的信号,互相依赖的代码本来可能就是相关的,不应该拆两个服务。
7#
tcu1j  1级新秀 | 2022-5-24 00:10:35 发帖IP地址来自 北京
这种问题实际中也算挺常见的,一般能答出不用构造器注入用setter注入就算过
8#
ngdh9  1级新秀 | 2022-5-24 00:11:33 发帖IP地址来自 北京
的确,工程问题和理论是两码事,工程内大家怎么方便怎么做,spring官方推构造器注入,在国外推广得也很不错,但是一到国内,受大量二流教程和培训班的影响,大家只会怎么方便怎么做,现在80%以上的人还是@Autowired字段的,还有为数不少的在用@Resource。
9#
yymb  1级新秀 | 2022-5-24 00:12:01 发帖IP地址来自 云南
对的,这种循环依赖的就不该出现,就算spring能处理,设计上也存在问题
10#
83dj  1级新秀 | 2022-5-24 00:12:10 发帖IP地址来自 江西新余
你是在哪个官方看到的?我看的spring in action首推的就是注解方式配置bean,其次是java config也就是代码配置bean这样自定义更方便,最差是xml只有在个别拿不到源码的情况下使用
11#
rm0wep  1级新秀 | 2022-5-24 00:12:35 发帖IP地址来自 中国
就在spring docs里面关于DI的描述里面,介绍constructor injection一篇里面有个讨论constructor injection和field injection的段落。
里面有提到应该尽可能地使用constructor injection。
12#
uoaed9  1级新秀 | 2022-5-24 00:12:47 发帖IP地址来自 中国
昨天面试阿里问了这个题,还好提前看了一下,但是没有仔细看源码,没有达到面试官想要的深度
13#
9to1s  1级新秀 | 2022-5-24 00:13:20 发帖IP地址来自 北京
我8个月经验,今年3月面试的时候被问到了,理解错意思了,我以为问的是出现了循环依赖怎么解决,就说了几种自己碰到过的解决方法,尴尬
14#
ldjo4  1级新秀 | 2022-5-24 00:13:54 发帖IP地址来自 四川宜宾
这个问题我看源码搞明白了,即使不用构造器注入依然可能会有循环依赖的问题。例如@Async就是典型的例子,@Transactional是在从单例工厂获取时就实现代理了,而@Async则是在注入后才实现代理对象。而有时成功有时失败是由于操作系统不同导致读取jar时文件顺序不一样,最后实例创建循序不一样。
15#
5xh6yj  1级新秀 | 2022-5-24 00:14:13 发帖IP地址来自 中国
文档我最近也看了,你说这个是setter和constructor两种当时优先使用构造方法,循环依赖的情况下考虑用setter但这都是xml内部的当时的比较,xml和注解则是各有又是,注解往往是更好的选择,但是xml可以在拿不到源码的情况下声明bean
16#
b_kzb  1级新秀 | 2022-5-24 00:15:07 发帖IP地址来自 中国
现在xml唯一强处就是在当前类没法写在代码里面的时候,但是这种情况spring boot都通过大量预置的autoconfiguration解决掉了。
而且,从spring boot的changelog可以看出,是越来越偏向于「不变」,而构造器注入正是为了不变性而服务的一个环节。
17#
hewing  1级新秀 | 2022-5-24 00:15:50 发帖IP地址来自 北京
??注解注入和构造器注入有关系吗?
18#
vntka  1级新秀 | 2022-5-24 00:16:17 发帖IP地址来自 中国
我的意思是,xml现在在spring boot环境下已经几乎没用了,唯一用途是初始化那些老框架,稍微有点知名度的框架spring boot都已经做好自动装配或者框架自身提供starter,xml已经失去大部分使用场景,所以setter注入方式也已经不需要再考虑兼容,从而可以开始转向构造器注入。
19#
4ruch  1级新秀 | 2022-5-24 00:16:54 发帖IP地址来自 北京
不是吧,注解的方式既不属于构造器注入也不属于setter注入啊
20#
gt_pz  1级新秀 | 2022-5-24 00:17:51 发帖IP地址来自 北京
@Autowired只是有了@ComponentScan之后为了简化注入的一个产物而已,本质还是setter,并且在xml和javaconfig里面都无法配置(private字段)必须scan才能用,和spring框架强绑定。
21#
pxge  1级新秀 | 2022-5-24 00:18:35 发帖IP地址来自 北京林业大学
第二张图中不应该出现“初始化”三个字吧
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP