关于javascript:Spring-如何解决循环依赖问题

4次阅读

共计 5896 个字符,预计需要花费 15 分钟才能阅读完成。

在对于 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 的核心思想,倡议大家看下。

图中的彩色箭头示意一开始的办法调用走向,走到最初,返回了 Spring 中缓存的 A 对象之后,示意递归调用返回了,此时应用绿色的箭头示意。

从图中咱们能够很分明的看到,B 对象的 a 属性是在第三步中注入的半成品 A 对象,而 A 对象的 b 属性是在第二步中注入的成品 B 对象,此时半成品的 A 对象也就变成了成品的 A 对象,因为其属性曾经设置实现了。一张图搞懂 Spring bean 的残缺生命周期,倡议浏览下。

2、源码解说

对于 Spring 解决循环依赖问题的形式,咱们这里通过下面的流程图其实很容易就能够了解,须要留神的一个点就是,Spring 是如何标记开始生成的 A 对象是一个半成品,并且是如何保留 A 对象的。

这里的标记工作 Spring 是应用 ApplicationContext 的属性 Set singletonsCurrentlyInCreation 来保留的,而半成品的 A 对象则是通过 Map<String, ObjectFactory<?>> singletonFactories 来保留的,这里的 ObjectFactory 是一个工厂对象,可通过调用其 getObject()办法来获取指标对象。在 AbstractBeanFactory.doGetBean()办法中获取对象的办法如下:

protected <T> T doGetBean(final String name, @Nullable final Class<T> 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()办法的作用是尝试创立指标对象,并且为该对象注入其所依赖的属性。

关注微信公众号:Java 技术栈,在后盾回复:spring,能够获取我整顿的 N 篇最新 Spring 教程,都是干货。

这里其实就是骨干逻辑,咱们后面图中曾经表明,在整个过程中会调用三次 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 的属性的。

最初
如果你感觉此文对你有一丁点帮忙,点个赞。或者能够退出我的开发交换群:1025263163 互相学习,咱们会有业余的技术答疑解惑

如果你感觉这篇文章对你有点用的话,麻烦请给咱们的开源我的项目点点 star: https://gitee.com/ZhongBangKe… 不胜感激!

JAVA 学习手册:https://doc.crmeb.com
技术交换论坛:https://q.crmeb.com

正文完
 0