热门标签 | 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属性占位符与Jersey的@Path和@ApplicationPath注解结合使用,以便在资源路径中动态解析属性值。 ... [详细]
  • ABP框架是ASP.NET Boilerplate的简称,它不仅是一个开源且文档丰富的应用程序框架,还提供了一套基于领域驱动设计(DDD)的最佳实践架构模型。本文将详细介绍ABP框架的特点、项目结构及其在Web API优先架构中的应用。 ... [详细]
  • 从理想主义者的内心深处萌发的技术信仰,推动了云原生技术在全球范围内的快速发展。本文将带你深入了解阿里巴巴在开源领域的贡献与成就。 ... [详细]
  • 本文介绍如何在阿里云环境中利用 Docker 容器化技术部署一个简单的 Flask Web 应用,并确保其可通过互联网访问。内容涵盖 Python 代码编写、Dockerfile 配置、镜像构建及容器运行等步骤。 ... [详细]
  • 深入理解Dockerfile及其作用
    Dockerfile是一种文本格式的配置文件,用于定义构建Docker镜像所需的步骤。通过使用`docker build`命令,用户可以将Dockerfile中的一系列指令转换成一个可执行的Docker镜像。 ... [详细]
  • 2017年软件开发领域的七大变革
    随着技术的不断进步,2017年对软件开发人员而言将充满挑战与机遇。本文探讨了开发人员需要适应的七个关键变化,包括人工智能、聊天机器人、容器技术、应用程序版本控制、云测试环境、大众开发者崛起以及系统管理的云迁移。 ... [详细]
  • 本文详细介绍如何在华为鲲鹏平台上构建和使用适配ARM架构的Redis Docker镜像,解决常见错误并提供优化建议。 ... [详细]
  • Flutter 核心技术与混合开发模式深入解析
    本文深入探讨了 Flutter 的核心技术,特别是其混合开发模式,包括统一管理模式和三端分离模式,以及混合栈原理。通过对比不同模式的优缺点,帮助开发者选择最适合项目的混合开发策略。 ... [详细]
  • 使用Echarts for Weixin 小程序实现中国地图及区域点击事件
    本文介绍了如何使用Echarts for Weixin在微信小程序中构建中国地图,并实现区域点击事件。包括效果展示、条件准备和逻辑实现的具体步骤。 ... [详细]
  • 将字符串中的嵌套列表转换回嵌套列表 ... [详细]
  • 本文将探讨如何在 Struts2 中使用 ActionContext 和 ServletActionContext 来获取请求参数和会话信息,同时解释它们的内部机制和最佳实践。 ... [详细]
  • RTThread线程间通信
    线程中通信在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取& ... [详细]
  • 本文介绍了存储器的基本原理及其分类,包括不同类型的存储介质和存储方式,并详细解释了各种存储器的特点和应用场景。 ... [详细]
  • 本文总结了近年来在实际项目中使用消息中间件的经验和常见问题,旨在为Java初学者和中级开发者提供实用的参考。文章详细介绍了消息中间件在分布式系统中的作用,以及如何通过消息中间件实现高可用性和可扩展性。 ... [详细]
  • vue引入echarts地图的四种方式
    一、vue中引入echart1、安装echarts:npminstallecharts--save2、在main.js文件中引入echarts实例:  Vue.prototype.$echartsecharts3、在需要用到echart图形的vue文件中引入:   importechartsfrom&amp;quot;echarts&amp;quot;;4、如果用到map(地图),还 ... [详细]
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社区 版权所有