美文网首页
3-bean 实例化——3-6 解决循环依赖

3-bean 实例化——3-6 解决循环依赖

作者: 鹏程1995 | 来源:发表于2020-02-25 16:01 被阅读0次

概要

过度

我们上文介绍了创建 Bean 实例并进行依赖注入、初始化的整体逻辑,我们介绍的是doCreateBean方法。还画了一个流程图,如下图所示:

1.png

其中,我们分别在3-3,3-4,3-5中介绍了创建Bean实例、依赖注入、调用初始化钩子的操作。我们之前说过在得到 Bean 实例后有两条路:

  1. 看如果是单例的话就提前暴露出去方便解决循环依赖,在最后完成创建后看是否成功解决了循环依赖
  2. 继续进行 Bean 的创建,包括依赖注入和调用初始化函数

现在,我们看一下解决循环依赖相关的逻辑。

内容简介

介绍创建 Bean 实例时的提前暴露引用和创建完成后判断是否成功接触循环依赖的逻辑。

所属环节

将初步获得的实例引用提前暴露出去。后面判断暴露出去的是不是对的

上下环节

上文:需要创建 Bean 实例,且通过锁定构造的函数得到了实例。【这里双线并行】

下文:返回得到的实例或者报错。

源码解析

入口

不放入口了,在最开始介绍doCreateBean时都放了。

将初步获得的实例暴露出去

// 如果要解决循环依赖问题,需要提前将实例引用暴露。
// 暴露出去之后慢慢再进行实例填值、初始化、后处理钩子调用
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
  if (logger.isDebugEnabled()) {
    logger.debug("Eagerly caching bean '" + beanName +
                 "' to allow for resolving potential circular references");
  }
  // 把找到的 bean 实例增加到缓存到 ObjectFactory 里去
  // 注意,这里使用在本 Factory 中注册的 SmartInstantiationAwareBeanPostProcessor 的方法获得
  // 对应的提前引用
  addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

关键词:

  • DefaultSingletonRegistry,singletonFactories
  • 闭包

简单粗暴

判断是否成功解除循环依赖

// 提前暴露出去了实例引用,但是我们在后续调用初始化钩子时可能会改变bean的地址,这里要做一下修复和回归
//
// 注意,我们只是完成了创建这个 bean 实例的操作,但是并没有改变 singleton 的注册状态,也就是说我们现在用 getSingleton()获得的
// 还是之前放进去的用于提前暴露的引用
if (earlySingletonExposure) {
  Object earlySingletonReference = getSingleton(beanName, false); //得到之前提前暴露的实例引用
  if (earlySingletonReference != null) { // 刚开始觉得有点冗余,看了后面的实现感觉还是有必要的
    if (exposedObject == bean) { // 我们一通操作,没有改变引用地址,说明我们后面的操作能即使同步出去
      exposedObject = earlySingletonReference;
    } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { // 我们的一通操作,改变了引用地址【出现了代理或者包裹】
      // 1. 是否提前注入原始的 bean 实例来防止循环引用,即使最终这个 bean 会被包裹【否】
      // 2. 这个提前暴露出去的 bean 已经被人依赖了
      String[] dependentBeans = getDependentBeans(beanName); // 获得依赖这个 bean 的 bean 列表,一个一个修改
      Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
      for (String dependentBean : dependentBeans) {
        if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { // 将还没有正式使用的 bean 实例删除
          // 删除失败,当时创建这个 bean 是有正经用途的
          actualDependentBeans.add(dependentBean);
        }
      }
      // 经过层层筛选,我们发现提前暴露的 bean 里是有正经用途的,不能直接删,那就直接 fail-fast 失败,防止出现隐藏的bug造成更大的损失
      if (!actualDependentBeans.isEmpty()) {
        throw new BeanCurrentlyInCreationException(beanName,
                                                   "Bean with name '" + beanName + "' has been injected into other beans [" +
                                                   StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                                                   "] in its raw version as part of a circular reference, but has eventually been " +
                                                   "wrapped. This means that said other beans do not use the final version of the " +
                                                   "bean. This is often the result of over-eager type matching - consider using " +
                                                   "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
      }
    }
  }
}

原因很简单:

因为我们在调用初始化钩子时,我们调用的Factory的后处理器是有可能对实例作代理的,也就是说我们一通处理之后的实例引用的可能不是最开始我们暴露出去的。

我们处理的思路也很简单:如果我们发现处理之后的地址和我们最开始的地址不同,看看有没有正经使用的实例在用你提前暴露的实例。【通过我们注册的依赖关系的map和forTypeCheckOnly】。如果有,不好意思,报错结束。如果没有,就把那些不正经的删了,结束。

扩展

为什么在提前暴露时是以singletonFactories而不是earlySingletonObjects

我们用singletonFactories缓存工厂方法,在调用之前如果我们的后处理器改了地址,在其他线程/实例获得此实例地址时都能拿到最新的。

如果我们直接把引用丢到earlySingletonObjects,就等于直接锁死实例地址了,后面可能跑冤枉路。

其实差别不是很大,算个小小小技巧吧。

为什么我们创建完成后没有把实例移动到singletonObjects

我们只负责创建,不负责注册,其实singletonFactories如果不是要解决循环依赖,我们也不打算放的。

而且,根据调用的上下文会发现,整个函数当成工厂传到getSingleton里面调用的,他会完成创建单例的对应注册工作和所有的缓存工作。

doCreateBean还剩一点东西

还剩下一点东西,在这里扫尾吧

// Register bean as disposable.
// bean 销毁可能还有一些钩子,进行一些注册
// 这里先注册销毁的钩子,再注册创建的钩子,防止出现在注册的间隙提前暴露出去并立刻进行销毁的极端情况
try {
  registerDisposableBeanIfNecessary(beanName, bean, mbd);
} catch (BeanDefinitionValidationException ex) {
  throw new BeanCreationException(
    mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}
// 注册销毁的钩子完成,对外暴露最终的 bean
// 注意:如果是提前暴露的单例 bean ,是从 singletonFactories 的缓存转移到 earlySingletonObjects 就不再动了,此函数只负责创建 bean ,
// 最外面我们是把 createBean 当作 singletonFacotry 传到 getSingleton 里面去的,在完成此方法后,外面的 getSingleton 会完成将 对应的
// bean 实例缓存至 singletonObjects 的。
return exposedObject;

如果有配置销毁的钩子,就注册一下。很简单,而且在DefaultSingletonRegistry中介绍过了,不再赘述。

问题遗留

参考文献

相关文章

  • 3-bean 实例化——3-6 解决循环依赖

    概要 过度 我们上文介绍了创建 Bean 实例并进行依赖注入、初始化的整体逻辑,我们介绍的是doCreateBea...

  • 循环依赖

    循环依赖 1、循环依赖的介绍 循环依赖,bean之间相互持有各自的引用,最终形成闭环。比如 bean的实例化有如下...

  • spring如何解决循环依赖

    什么是循环依赖 循环依赖指的是两个bean互相依赖彼此,beanA在示例化的时候要注入beanB,beanB在实例...

  • Spirng 循环依赖

    Spring 通过3级缓存的机制解决了循环依赖死循环的问题 1级缓存存刚刚实例化还没来得及赋值的Bean 2级缓存...

  • 3-bean 实例化——3-4 依赖注入

    概要 过度 我们上文介绍了创建 Bean 实例并进行依赖注入、初始化的整体逻辑,我们介绍的是doCreateBea...

  • 2020-08-21:Spring在Bean创建过程中是如何解决

    前言 每日一题专栏 Spring在Bean创建过程中是如何解决循环依赖的? 循环依赖只会存在在单例实例中,多例循环...

  • 循环依赖问题

    问题描述 有多个对象,A依赖B,B依赖A。导致实例化的时候会进入死循环导致内存溢出。 三种循环依赖 1.构造器的循...

  • Spring 常见面试题

    1、Bean的生命周期 2、循环依赖的解决方式 1、实例化和初始化分开,提前暴露对象;2、三级缓存,暂存半成品对象...

  • Spring之循环依赖

    Spring在Bean的实例化过程中,提供了对循环依赖的解决方案,但是这部分代码非常的生涩难懂,今天,我们就从一个...

  • Bean实例化过程以及循环依赖

    1. Bean创建 1. 实例化Bean 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean...

网友评论

      本文标题:3-bean 实例化——3-6 解决循环依赖

      本文链接:https://www.haomeiwen.com/subject/drwtchtx.html