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

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


目录
相关文章
|
6天前
|
存储 缓存 Java
面试问Spring循环依赖?今天通过代码调试让你记住
该文章讨论了Spring框架中循环依赖的概念,并通过代码示例帮助读者理解这一概念。
面试问Spring循环依赖?今天通过代码调试让你记住
|
3天前
|
缓存 Java Spring
spring如何解决循环依赖
Spring框架处理循环依赖分为构造器循环依赖与setter循环依赖两种情况。构造器循环依赖不可解决,Spring会在检测到此类依赖时抛出`BeanCurrentlyInCreationException`异常。setter循环依赖则通过缓存机制解决:利用三级缓存系统,其中一级缓存`singletonObjects`存放已完成的单例Bean;二级缓存`earlySingletonObjects`存放实例化但未完成属性注入的Bean;三级缓存`singletonFactories`存放创建这些半成品Bean的工厂。
|
22天前
|
Java Spring 容器
Spring Boot 启动源码解析结合Spring Bean生命周期分析
Spring Boot 启动源码解析结合Spring Bean生命周期分析
59 11
|
22天前
|
缓存 Java 程序员
spring IoC 源码
spring IoC 源码
38 3
|
4天前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
|
29天前
|
缓存 Java 开发者
Spring循环依赖问题之Spring循环依赖如何解决
Spring循环依赖问题之Spring循环依赖如何解决
|
29天前
|
Java Spring 容器
Spring循环依赖问题之两个不同的Bean A,导致抛出异常如何解决
Spring循环依赖问题之两个不同的Bean A,导致抛出异常如何解决
|
29天前
|
存储 缓存 Java
Spring循环依赖问题之循环依赖异常如何解决
Spring循环依赖问题之循环依赖异常如何解决
|
21天前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
1月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
87 0