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

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


目录
相关文章
|
3月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
122 2
|
3月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
129 5
|
26天前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
3月前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
2月前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
67 2
|
3月前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
82 9
|
13天前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
107 17
Spring Boot 两种部署到服务器的方式
|
13天前
|
Dart 前端开发 JavaScript
springboot自动配置原理
Spring Boot 自动配置原理:通过 `@EnableAutoConfiguration` 开启自动配置,扫描 `META-INF/spring.factories` 下的配置类,省去手动编写配置文件。使用 `@ConditionalXXX` 注解判断配置类是否生效,导入对应的 starter 后自动配置生效。通过 `@EnableConfigurationProperties` 加载配置属性,默认值与配置文件中的值结合使用。总结来说,Spring Boot 通过这些机制简化了开发配置流程,提升了开发效率。
48 17
springboot自动配置原理
|
18天前
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
66 11
|
20天前
|
缓存 安全 Java
Spring Boot 3 集成 Spring Security + JWT
本文详细介绍了如何使用Spring Boot 3和Spring Security集成JWT,实现前后端分离的安全认证概述了从入门到引入数据库,再到使用JWT的完整流程。列举了项目中用到的关键依赖,如MyBatis-Plus、Hutool等。简要提及了系统配置表、部门表、字典表等表结构。使用Hutool-jwt工具类进行JWT校验。配置忽略路径、禁用CSRF、添加JWT校验过滤器等。实现登录接口,返回token等信息。
204 12