热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

浅谈Spring解决循环依赖必须要三级缓存吗

这篇文章主要介绍了浅谈Spring解决循环依赖必须要三级缓存吗,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

我们都知道 Spring 是通过三级缓存来解决循环依赖的,但是解决循环依赖真的需要使用到三级缓冲吗?只使用两级缓存是否可以呢?本篇文章就 Spring 是如何使用三级缓存解决循环依赖作为引子,验证两级缓存是否可以解决循环依赖。

循环依赖

既然要解决循环依赖,那么就要知道循环依赖是什么。如下图所示:

通过上图,我们可以看出:

  • A 依赖于 B
  • B 依赖于 C
  • C 依赖于 A
public class A {
  private B b;
}

public class B {
  private C c;
}

public class C {
  private A a;
}

这种依赖关系形成了一种闭环,从而造成了循环依赖的局面。

下面是未解决循环依赖的常规步骤:

  • 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  • A 对象发现需要注入 B 对象,但是容器中并没有 B 对象(如果对象创建完成并且属性注入完成和执行完初始化方法就会放入容器中)。
  • 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  • B 对象发现需要注入 C 对象,但是容器中并没有 C 对象。
  • 实例化 C,此时 C 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  • C 对象发现需要注入 A 对象,但是容器中并没有 A 对象。
  • 重复步骤 1。

三级缓存

Spring 解决循环依赖的核心就是提前暴露对象,而提前暴露的对象就是放置于第二级缓存中。下表是三级缓存的说明:

名称 描述
singletonObjects 一级缓存,存放完整的 Bean。
earlySingletonObjects 二级缓存,存放提前暴露的Bean,Bean 是不完整的,未完成属性注入和执行 init 方法。
singletonFactories 三级缓存,存放的是 Bean 工厂,主要是生产 Bean,存放到二级缓存中。

所有被 Spring 管理的 Bean,最终都会存放在 singletonObjects 中,这里面存放的 Bean 是经历了所有生命周期的(除了销毁的生命周期),完整的,可以给用户使用的。

earlySingletonObjects 存放的是已经被实例化,但是还没有注入属性和执行 init 方法的 Bean。

singletonFactories 存放的是生产 Bean 的工厂。

Bean 都已经实例化了,为什么还需要一个生产 Bean 的工厂呢?这里实际上是跟 AOP 有关,如果项目中不需要为 Bean 进行代理,那么这个 Bean 工厂就会直接返回一开始实例化的对象,如果需要使用 AOP 进行代理,那么这个工厂就会发挥重要的作用了,这也是本文需要重点关注的问题之一。

解决循环依赖

Spring 是如何通过上面介绍的三级缓存来解决循环依赖的呢?这里只用 A,B 形成的循环依赖来举例:

  • 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
  • 为 A 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  • 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
  • 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
  • 为 B 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  • 发现 B 需要注入 A 对象,此时在一级、二级未发现对象 A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A 还是一个半成品,并没有完成属性填充和执行初始化方法)
  • 将对象 A 注入到对象 B 中。
  • 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
  • 对象 A 得到对象 B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
  • 对象 A 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A。

我们从源码中来分析整个过程:

创建 Bean 的方法在 AbstractAutowireCapableBeanFactory::doCreateBean()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
  BeanWrapper instanceWrapper = null;
  
  if (instanceWrapper == null) {
    // ① 实例化对象
    instanceWrapper = this.createBeanInstance(beanName, mbd, args);
  }

  final Object bean = instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null;
  Class<&#63;> beanType = instanceWrapper != null &#63; instanceWrapper.getWrappedClass() : null;
  
  // ② 判断是否允许提前暴露对象,如果允许,则直接添加一个 ObjectFactory 到三级缓存
  boolean earlySingletOnExposure= (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
    // 添加三级缓存的方法详情在下方
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }

  // ③ 填充属性
  this.populateBean(beanName, mbd, instanceWrapper);
  // ④ 执行初始化方法,并创建代理
  exposedObject = initializeBean(beanName, exposedObject, mbd);
  
  return exposedObject;
}

添加三级缓存的方法如下:

protected void addSingletonFactory(String beanName, ObjectFactory<&#63;> singletonFactory) {
  Assert.notNull(singletonFactory, "Singleton factory must not be null");
  synchronized (this.singletonObjects) {
    if (!this.singletonObjects.containsKey(beanName)) { // 判断一级缓存中不存在此对象
      this.singletonFactories.put(beanName, singletonFactory); // 添加至三级缓存
      this.earlySingletonObjects.remove(beanName); // 确保二级缓存没有此对象
      this.registeredSingletons.add(beanName);
    }
  }
}

@FunctionalInterface
public interface ObjectFactory {
  T getObject() throws BeansException;
}

通过这段代码,我们可以知道 Spring 在实例化对象的之后,就会为其创建一个 Bean 工厂,并将此工厂加入到三级缓存中。

因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。为什么要这么做呢?

这实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。但是,如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到应该在哪里提前创建代理对象?

Spring 的做法就是在 ObjectFactory 中去提前创建代理对象。它会执行 getObject() 方法来获取到 Bean。实际上,它真正执行的方法如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  Object exposedObject = bean;
  if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
      if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
        SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
        // 如果需要代理,这里会返回代理对象;否则返回原始对象
        exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
  }
  return exposedObject;
}

因为提前进行了代理,避免对后面重复创建代理对象,会在 earlyProxyReferences 中记录已被代理的对象。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
    implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
  @Override
  public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 记录已被代理的对象
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
  }
}

通过上面的解析,我们可以知道 Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则。

如何获取依赖

我们目前已经知道了 Spring 的三级依赖的作用,但是 Spring 在注入属性的时候是如何去获取依赖的呢?

他是通过一个 getSingleton() 方法去获取所需要的 Bean 的。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 一级缓存
  Object singletOnObject= this.singletonObjects.get(beanName);
  if (singletOnObject== null && isSingletonCurrentlyInCreation(beanName)) {
    synchronized (this.singletonObjects) {
      // 二级缓存
      singletOnObject= this.earlySingletonObjects.get(beanName);
      if (singletOnObject== null && allowEarlyReference) {
        // 三级缓存
        ObjectFactory<&#63;> singletOnFactory= this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
          // Bean 工厂中获取 Bean
          singletOnObject= singletonFactory.getObject();
          // 放入到二级缓存中
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  return singletonObject;
}

当 Spring 为某个 Bean 填充属性的时候,它首先会寻找需要注入对象的名称,然后依次执行 getSingleton() 方法得到所需注入的对象,而获取对象的过程就是先从一级缓存中获取,一级缓存中没有就从二级缓存中获取,二级缓存中没有就从三级缓存中获取,如果三级缓存中也没有,那么就会去执行 doCreateBean() 方法创建这个 Bean。

二级缓存

我们现在已经知道,第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。

既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将addSingletonFactory() 方法进行改造。

protected void addSingletonFactory(String beanName, ObjectFactory<&#63;> singletonFactory) {
  Assert.notNull(singletonFactory, "Singleton factory must not be null");
  synchronized (this.singletonObjects) {
    if (!this.singletonObjects.containsKey(beanName)) { // 判断一级缓存中不存在此对象
      object o = singletonFactory.getObject(); // 直接从工厂中获取 Bean
      this.earlySingletonObjects.put(beanName, o); // 添加至二级缓存中
      this.registeredSingletons.add(beanName);
    }
  }
}

这样的话,每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。测试结果是完全正常的,Spring 的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

结论

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存呢?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。所以,Spring 选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,因为如果不提前创建代理对象,那么注入的就是原始对象,这样就会产生错误。

到此这篇关于浅谈Spring 解决循环依赖必须要三级缓存吗的文章就介绍到这了,更多相关Spring  循环依赖三级缓存内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 深入解析Spring Cloud Ribbon负载均衡机制
    本文详细介绍了Spring Cloud中的Ribbon组件如何实现服务调用的负载均衡。通过分析其工作原理、源码结构及配置方式,帮助读者理解Ribbon在分布式系统中的重要作用。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 本文探讨了在 ASP.NET MVC 5 中实现松耦合组件的方法。通过分离关注点,应用程序的各个组件可以更加独立且易于维护和测试。文中详细介绍了依赖项注入(DI)及其在实现松耦合中的作用。 ... [详细]
  • 本文详细介绍了W3C标准盒模型和IE传统盒模型的区别,探讨了CSS3中box-sizing属性的使用方法及其在布局中的重要性。通过实例分析,帮助读者更好地理解和应用这一关键概念。 ... [详细]
  • 题库来源:安全生产模拟考试一点通公众号小程序G3锅炉水处理报名考试是安全生产模拟考试一点通生成的,G3锅炉水处理证模拟考试题库是根据G3锅炉水处理最新 ... [详细]
  • 本文详细探讨了Netty中Future及其子类的设计与实现,包括其在并发编程中的作用和具体应用场景。我们将介绍Future的继承体系、关键方法的实现细节,并讨论如何通过监听器和回调机制来处理异步任务的结果。 ... [详细]
  • 本文介绍如何在 Unity 的 XML 配置文件中,将参数传递给自定义生命周期管理器的构造函数。我们将详细探讨 CustomLifetimeManager 类的实现及其配置方法。 ... [详细]
  • Ralph的Kubernetes进阶之旅:集群架构与对象解析
    本文深入探讨了Kubernetes集群的架构和核心对象,详细介绍了Pod、Service、Volume等基本组件,以及更高层次的抽象如Deployment、StatefulSet等,帮助读者全面理解Kubernetes的工作原理。 ... [详细]
  • Hadoop入门与核心组件详解
    本文详细介绍了Hadoop的基础知识及其核心组件,包括HDFS、MapReduce和YARN。通过本文,读者可以全面了解Hadoop的生态系统及应用场景。 ... [详细]
  • 本文详细探讨了Java中StringBuffer类在不同情况下的扩容规则,包括空参构造、带初始字符串和指定初始容量的构造方法。通过实例代码和理论分析,帮助读者更好地理解StringBuffer的内部工作原理。 ... [详细]
  • 本文探讨了领域驱动设计(DDD)的核心概念、应用场景及其实现方式,详细介绍了其在企业级软件开发中的优势和挑战。通过对比事务脚本与领域模型,展示了DDD如何提升系统的可维护性和扩展性。 ... [详细]
  • 深入了解 Windows 窗体中的 SplitContainer 控件
    SplitContainer 控件是 Windows 窗体中的一种复合控件,由两个可调整大小的面板和一个可移动的拆分条组成。本文将详细介绍其功能、属性以及如何通过编程方式创建复杂的用户界面。 ... [详细]
  • 深入解析 Apache Shiro 安全框架架构
    本文详细介绍了 Apache Shiro,一个强大且灵活的开源安全框架。Shiro 专注于简化身份验证、授权、会话管理和加密等复杂的安全操作,使开发者能够更轻松地保护应用程序。其核心目标是提供易于使用和理解的API,同时确保高度的安全性和灵活性。 ... [详细]
  • 本文探讨了在Linux系统上使用Docker时,通过volume将主机上的HTML5文件挂载到容器内部指定目录时遇到的403错误,并提供了解决方案和详细的操作步骤。 ... [详细]
author-avatar
琳琳小朋友m
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有