Spring 循环依赖问题解决方案以及简要源码流程剖析

简介: Spring 循环依赖问题解决方案以及简要源码流程剖析

循环依赖问题描述

循环依赖的本质就是你的完整对象实例要依赖与其他实例,而其他实例的完整对象也同样依赖于你,相互之间的依赖从而导致没法完整创建对象而导致失败/报错.

从对象的创建过程来描述这个问题,如下:

  • 当 A 实例化完成之后,要开始进行初始化赋值操作了,但是赋值的时候,值的类型有可能是引用类型 B,需要从 spring 容器中获取具体的对象来完成赋值操作
  • 此时,需要引用的对象可能被创建了,也可能没被创建,如果被创建了,那么直接获取即可
  • 若 B 没有创建,会涉及到对象的创建过程,而内部对象的创建过程中又会有其他的依赖,其他的依赖中有可能包含了当前的对象 A【也就是相互依赖】
  • 此时,A 对象还没有创建完成,所以就会产生循环依赖问题,形成闭环

如下图所示:

代码演示

public class CircularTest {
    public static void main(String[] args) {
        A a = new A();
    }
    static class A {
        private B classTwo = new B();
    }
    static class B {
        private A classOne = new A();
    }
}

执行后出现了 StackOverflowError 栈溢出异常,栈桢深不见底

Exception in thread "main" java.lang.StackOverflowError
  at com.vnjohn.cyclereference.CircularTest$B.<init>(CircularTest.java:17)
  at com.vnjohn.cyclereference.CircularTest$A.<init>(CircularTest.java:13)
  at com.vnjohn.cyclereference.CircularTest$B.<init>(CircularTest.java:17)
  at com.vnjohn.cyclereference.CircularTest$A.<init>(CircularTest.java:13)
  at com.vnjohn.cyclereference.CircularTest$B.<init>(CircularTest.java:17)
  at com.vnjohn.cyclereference.CircularTest$A.<init>(CircularTest.java:13)
  at com.vnjohn.cyclereference.CircularTest$B.<init>(CircularTest.java:17)
  at com.vnjohn.cyclereference.CircularTest$A.<init>(CircularTest.java:13)
  at com.vnjohn.cyclereference.CircularTest$B.<init>(CircularTest.java:17)

分析问题出现的原因

  1. 首先我们要明确一点的就是如果这个对象 A 还没创建成功,在创建的过程中要依赖另一个对象 B;而另一个对象 B 在创建中要依赖 A,这种情况下肯定是无解的
  2. 这时我们就要转换思路,我们先把 A 创建出来,但是还没有完成初始化操作,也就是这是一个半成品的对象,然后在赋值的时候先把 A 暴露出来,然后创建 B
  3. B 创建完成后找到暴露的 A 完成整体的实例化;这时候再把 B 交给 A,完成 A 的实例化操作,从而揭开了循环依赖的密码

通过伪代码解决

通过自身编写代码来解决 A->B 循环依赖问题,主要是涉及到反射+缓存来配合(单独使用反射来获取实例,一样是会产生循环依赖问题)再配合属性赋值的方式,代码如下:

public class CircularTest {
    private static final Map<String, Object> map = new ConcurrentHashMap<>();
    public static void main(String[] args) throws Exception {
        System.out.println(getBean(A.class));
        System.out.println(getBean(B.class));
    }
    public static <T> T getBean(Class<T> tClass) throws Exception {
        Object object = tClass.newInstance();
        String tClassName = tClass.getSimpleName();
        if (map.containsKey(tClassName)) {
            return (T) map.get(tClassName);
        }
        // 将半成品对象放入到容器中
        map.put(tClassName, object);
        for (Field field : tClass.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldClassName = field.getType().getSimpleName();
            // 插入一个逻辑,判断是否有该属性对应对象的半成品对象
            Class<?> fieldClass = field.getType();
            // 递归调用 getBean 方法
            field.set(object, map.containsKey(fieldClassName) ? map.get(fieldClassName) : getBean(fieldClass));
        }
        return (T) object;
    }
    public static class A {
        private B b;
        public B getB() {
            return b;
        }
        public void setB(B b) {
            this.b = b;
        }
    }
    public static class B {
        private A a;
        public A getA() {
            return a;
        }
        public void setA(A a) {
            this.a = a;
        }
    }
}

执行结果正常,如下:

com.vnjohn.Test1$A@26f0a63f
com.vnjohn.Test1$B@4361bd48

运行过程如下:

A->getBean->实例化 A 完成->A 存入 Map->填充 B属性->getBean(B.class)->实例化 B 完成->B 存入 Map->填充 A 属性,此时因为 A 属性已经在 Map 中存在了,直接取用即可,无须再调用 getBean 方法,此时问题迎刃而解.

Spring 循环依赖

Spring 中实例涉及到动态代理,如果向上面只用一个缓存(存放半成品对象)是不能够满足的,在 Spring 中一级缓存存放的是成品对象(可能是简单成品对象,也可能是代理成品对象,考虑的是该类是否被代理所修饰)二级缓存存放的是半成品对象,三级缓存存放是 lambda 表达式准备被实现动态代理的对象

FAQ

1、我们在获取完整对象的时候,分为了两个步骤,实例化和初始化,初始化环节的填充属性阶段有没有可能会改变当前对象的地址空间?

不会改变的

2、生成代理对象之前,要不要生成普通对象?

要,生成的代理对象,此时它的 superclass 就是普通对象

3、我们能否确定什么时候会调用具体的对象?无论是普通对象还是代理对象

在调用具体对象时,会检测当前对象是否需要被代理,要的话直接创建代理对象即可;

4、那怎么能够在随时需要时创建对象呢?

传递进去一个生成代理对象的匿名内部类,当需要调用时,直接调用匿名内部类(lambada 表达式)生成代理对象,类似于回调机制

5、有没有可能 spring 中同时存在相同 beanName 的普通对象和代理对象?如果有的话,那么我在调用时是根据 beanName 来获取对象,此时我要获取到两个对象吗?那么我应该用哪个?

有可能会同时存在;不需要同时获取两个对象,代理对象中包含了普通对象的所有功能,如果一个对象需要被代理了,那么此时普通对象就可以不存在,只需要使用代理对象即可.

6、什么时候要生成具体的代理对象呢?(代码实操)

  • 在进行属性注入时,调用该对象生成的时候是否需要被代理,如果需要,直接创建对象即可
  • 在整个过程中,没有其他的对象有当前对象的依赖,那么在生成最终的完整对象之前生成代理即可(BeanPostProcessor#after 方法会做这部分操作)

解决问题思路

实例化和初始化是分开处理的,当完成实例化之后就可以让其他对象引用当前对象,只不过当前对象不是一个完整对象而已,后续需要完成此对象的剩余步骤

直接获取半成品对象「实例化但未初始化」引用地址,保证对象能够能够被找到,而半成品对象在堆空间中无所谓是否有设置属性值

如果单独为了循环依赖问题,那么使用二级缓存足够解决问题;三级缓存存在的意义是为了代理,如果没有代理对象,二级缓存足以解决问题

  • 一级缓存是存放成品对象,实例化且初始化完成的
  • 二级缓存是存放半成品对象的,实例化完成但未初始化的
  • 三级缓存是存放代理对象的,存放的是一个 lambda 回调方法

上述描述的问题,主要是为了让 半成品对象能够提前的被暴露 出去,让其他依赖这个对象的能够直接拿出来填充到属性中去完成初始化操作.

源代码实现

Spring 具体如何去实现的,要先了解整个 Spring Bean 生命周期,才能知道 Spring 在整个过程中是如何解决这种问题的,解决问题流程图如下:

createBeanInstance 该方法是 Spring 创建实例对象的具体方法,在该方法调用执行完以后,此时的半成品对象就已经准备好了,但由于有动态代理的存在,所以将其加入到 Spring 中的三级缓存中

// 判断当前bean是否需要提前曝光:单例&允许循环依赖&当前 bean 正在创建中(创建时会记录状态,创建完会移除状态),检测循环依赖
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                     "' to allow for resolving potential circular references");
    }
    // 为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

addSingletonFactory 存放到三级缓存的源码

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
  Assert.notNull(singletonFactory, "Singleton factory must not be null");
  // 使用 singletonObjects 进行加锁,保证线程安全
  synchronized (this.singletonObjects) {
    // 如果单例对象的高速缓存【beanName->bean 实例】没有该 Bean 存在
    if (!this.singletonObjects.containsKey(beanName)) {
      // 将 beanName->singletonFactory 放到单例工厂的缓存【bean名称->ObjectFactory】
      this.singletonFactories.put(beanName, singletonFactory);
      // 从早期单例对象的高速缓存【beanName->bean 实例】 移除 beanName 相关缓存对象
      this.earlySingletonObjects.remove(beanName);
      // 将 beanName 添加已注册的单例集中
      this.registeredSingletons.add(beanName);
    }
  }
}

1、A 在填充属性阶段,会调用 getSingleton 方法看 B 在三个缓存集合中是否存在,不存在就进行 Bean 实例创建「getBean->doGetBean->createBean->doCreateBean」后,会执行到 addSingletonFactory 方法阶段

2、此时,B 代理对象的回调方法就会存入到三级缓存中,然后执行 B 填充阶段,会调用 getSingleton 方法判断缓存中是否存在 A 对象

3、若三级缓存中存在 A 对象,会执行回调方法 getObject 方法生成代理对象,将生成的代理对象存入到二级缓存中,再把三级缓存删除,最后返回给到 B 进行属性填充.

getSingleton 方法源码如下:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   // 从单例对象缓存中获取 beanName 对应的单例对象
   Object singletonObject = this.singletonObjects.get(beanName);
   // 若单例对象缓存中没有,并且该 beanName 对应单例 bean 正在创建中
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      // 从早期单例对象缓存中获取单例对象(
      // 之所以称之为早期单例对象,是因为 earlySingletonObjects 里对象都是
      // 通过提前曝光的 ObjectFactory 创建出来的,还未进行属性填充等操作的
      singletonObject = this.earlySingletonObjects.get(beanName);
      // 若在早期单例对象缓存中也没有,并且允许创建早期单例对象引用
      if (singletonObject == null && allowEarlyReference) {
         // 如果为空,则锁定全局变量并进行处理
         synchronized (this.singletonObjects) {
            singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
               singletonObject = this.earlySingletonObjects.get(beanName);
               if (singletonObject == null) {
                  // 当某些方法需要提前初始化的时候则会调用 addSingletonFactory 方法
                  // 将对应 ObjectFactory 初始化策略存储在 singletonFactories
                  ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                  if (singletonFactory != null) {
                     // 若存在单例对象工厂,则通过工厂创建一个单例对象
                     singletonObject = singletonFactory.getObject();
                     // 记录在缓存中,二级缓存、三级缓存的对象不能同时存在
                     this.earlySingletonObjects.put(beanName, singletonObject);
                     // 从三级缓存中移除
                     this.singletonFactories.remove(beanName);</mark>
                  }
               }
            }
         }
      }
   }
   return singletonObject;
}

此时,最开始的三级缓存中存入的是 lambda 回调方法,经过该方法的调用生成代理对象以后,存入到二级缓存中,再从三级缓存移除该 bean 对应的元素

填充属性工作完毕,再走完初始化工作流程,整个 B 就已经是完整对象了,最后会调用 addSingleton 方法将对象存放到一级缓存中,方法源码如下:

protected void addSingleton(String beanName, Object singletonObject) {
  synchronized (this.singletonObjects) {
    // 将映射关系添加到单例对象的高速缓存中
    this.singletonObjects.put(beanName, singletonObject);
    // 移除 beanName 在单例工厂缓存中的数据
    this.singletonFactories.remove(beanName);
    // 移除 beanName 在早期单例对象的高速缓存的数据
    this.earlySingletonObjects.remove(beanName);
    // 将beanName添加到已注册的单例集中
    this.registeredSingletons.add(beanName);
  }
}

到这里,B 走完了整个 Bean 创建流程,然后返回到 A 填充属性阶段->初始化->addSingleton,这样循环依赖的问题就迎刃而解

总结

循环依赖问题是经常会问的一个地方,该篇文章讲解该问题出现的原因,体现了在本地处理、Spring 中处理的不同,最后通过伪代码的方式进行了更深的分析,同时讲解了在 Spring 中是怎么去解决&避免此问题出现的.

通过自己在源码中的阅读和理解分享出来的,希望能够帮助到大家!!!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!


目录
相关文章
|
11天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
1天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
20 9
|
27天前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
|
27天前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
28天前
|
XML Java 数据格式
手动开发-简单的Spring基于注解配置的程序--源码解析
手动开发-简单的Spring基于注解配置的程序--源码解析
43 0
|
28天前
|
XML Java 数据格式
手动开发-简单的Spring基于XML配置的程序--源码解析
手动开发-简单的Spring基于XML配置的程序--源码解析
75 0
|
28天前
|
JSON 前端开发 JavaScript
优雅!Spring Boot 3.3 实现职责链模式,轻松应对电商订单流程
本文介绍如何使用 Spring Boot 3.3 实现职责链模式,优化电商订单处理流程。通过将订单处理的各个环节(如库存校验、优惠券核验、支付处理等)封装为独立的处理器,并通过职责链将这些处理器串联起来,实现了代码的解耦和灵活扩展。具体实现包括订单请求类 `OrderRequest`、抽象处理器类 `OrderHandler`、具体处理器实现(如 `OrderValidationHandler`、`VerifyCouponHandler` 等)、以及初始化职责链的配置类 `OrderChainConfig`。
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
28天前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
143 2
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决