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,后续会有更多实战、源码、架构干货分享!

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


目录
相关文章
|
8月前
|
监控 安全 Java
解决 Spring Boot 中 SecurityConfig 循环依赖问题的详解
本文详细解析了在 Spring Boot 中配置 `SecurityConfig` 时可能遇到的循环依赖问题。通过分析错误日志与代码,指出问题根源在于 `SecurityConfig` 类中不当的依赖注入方式。文章提供了多种解决方案:移除 `configureGlobal` 方法、定义 `DaoAuthenticationProvider` Bean、使用构造函数注入以及分离配置类等。此外,还讨论了 `@Lazy` 注解和允许循环引用的临时手段,并强调重构以避免循环依赖的重要性。通过合理设计 Bean 依赖关系,可确保应用稳定启动并提升代码可维护性。
647 0
|
4月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
8月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
514 70
|
7月前
|
druid Java 关系型数据库
Spring Boot与Druid升级解决方案
好的,我需要帮助用户解决他们遇到的数据库连接问题,并升级项目的依赖。首先,用户提供的错误信息是关于Spring Boot应用在初始化数据源时抛出的异常,具体是Druid连接池验证连接失败。同时,用户希望升级项目的依赖版本。
685 10
|
8月前
|
监控 Java 关系型数据库
Spring Boot整合MySQL主从集群同步延迟解决方案
本文针对电商系统在Spring Boot+MyBatis架构下的典型问题(如大促时订单状态延迟、库存超卖误判及用户信息更新延迟)提出解决方案。核心内容包括动态数据源路由(强制读主库)、大事务拆分优化以及延迟感知补偿机制,配合MySQL参数调优和监控集成,有效将主从延迟控制在1秒内。实际测试表明,在10万QPS场景下,订单查询延迟显著降低,超卖误判率下降98%。
346 5
|
8月前
|
SQL 前端开发 Java
深入分析 Spring Boot 项目开发中的常见问题与解决方案
本文深入分析了Spring Boot项目开发中的常见问题与解决方案,涵盖视图路径冲突(Circular View Path)、ECharts图表数据异常及SQL唯一约束冲突等典型场景。通过实际案例剖析问题成因,并提供具体解决方法,如优化视图解析器配置、改进数据查询逻辑以及合理使用外键约束。同时复习了Spring MVC视图解析原理与数据库完整性知识,强调细节处理和数据验证的重要性,为开发者提供实用参考。
347 0
|
8月前
|
安全 前端开发 Java
Spring Boot 项目中触发 Circular View Path 错误的原理与解决方案
在Spring Boot开发中,**Circular View Path**错误常因视图解析与Controller路径重名引发。当视图名称(如`login`)与请求路径相同,Spring MVC无法区分,导致无限循环调用。解决方法包括:1) 明确指定视图路径,避免重名;2) 将视图文件移至子目录;3) 确保Spring Security配置与Controller路径一致。通过合理设定视图和路径,可有效避免该问题,确保系统稳定运行。
544 0
|
10月前
|
网络协议 Java Shell
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
664 7
|
10月前
|
IDE Java 应用服务中间件
spring boot 启动流程
Spring Boot 启动流程简介: 在使用 Spring Boot 之前,启动 Java Web 应用需要配置 Web 容器(如 Tomcat),并将应用打包放入容器目录。而使用 Spring Boot,只需运行 main() 方法即可启动 Web 应用。Spring Boot 的核心启动方法是 SpringApplication.run(),它负责初始化和启动应用上下文。 主要步骤包括: 1. **应用启动计时**:使用 StopWatch 记录启动时间。 2. **打印 Banner**:显示 Spring Boot 的 LOGO。 3. **创建上下文实例**:通过反射创建
548 5