likes
comments
collection
share

学过这么一遍,Spring循环依赖问题难不倒我

作者站长头像
站长
· 阅读数 11

“我正在参加「掘金·启航计划」”

简单介绍

碰上问题了

今天原本愉快的在 CRUD,结果一次循环依赖的问题打断了我的编码:

学过这么一遍,Spring循环依赖问题难不倒我

以往每次遇到循环依赖问题我都是通过让 Spring 允许循环依赖的方式去解决的。

但是想想看自己根本就不清楚这个 bug 到底该怎样健康解决,不懂 Spring 循环依赖的原理...

一直都不知道的话可还怎么和面试官对线?想到这里泪流了下来(bushi

学过这么一遍,Spring循环依赖问题难不倒我

菜咱就赶紧学起来,顺便记下一篇博客用来回顾。

接下来我们将依次剖析循环依赖的问题本身和原理。

什么是循环依赖?

循环依赖简单定义

就是对象依赖对象,依赖关系形成一条链,最后闭环到自己。

我们新建一个小的 SpringBoot 项目来复现一下循环依赖问题:

在 SpringBoot 项目下我创建一个类 A 并且依赖一个类 B。

 @Component
 public class A {
     @Resource
     private B b;
 }

同理我们创建一个类 B 并且依赖类 A。

 @Component
 public class B {
     @Resource
     private A a;
 }

这里我们能够想象到它的依赖链是一个 A->B->A,这样就是一个简单的循环依赖。

接着我们启动项目,报错如下:

学过这么一遍,Spring循环依赖问题难不倒我

果不其然出现问题。并且顺着依赖链我们同样可以推理出来 a 依赖 b 然后 b 依赖 a 的事实。

解决开始的问题

通过上面的小实验我们已经可以解决最开始我碰到的问题了

学过这么一遍,Spring循环依赖问题难不倒我

这里通过报错信息推理依赖链,是 PSignController 依赖 pSignService(对象),然后 pSignService 依赖自己。

我们看到源代码中的情况,下面是 PSignController 确实依赖一个 pSignService

学过这么一遍,Spring循环依赖问题难不倒我

然后是 PSignService 接口的实现类,里面依赖了一个 pSignService

学过这么一遍,Spring循环依赖问题难不倒我

所以由于 pSignService 自己依赖自己,导致出现循环依赖问题...

于是将该依赖删除掉,让 Service 层去依赖 Dao 层,这样循环依赖就解决了!所以说业务层之间还是尽量不要互相依赖为好。

仅仅解决问题是不够的,我们还要顺便将循环依赖问题的原理弄清楚

Spring 解决循环依赖的原理

不考虑 Spring 循环依赖是问题吗?

不考虑 Spring 其实循环依赖并不是问题,因为对象之间相互依赖是很正常的事情。

比如我们改造上面的代码如下:

 @Getter
 @Setter
 class A {
     private B b;
 }
 ​
 @Getter
 @Setter
 class B{
     public A a;
 }
 ​
 ​
 @SpringBootTest
 public class CircularDependencyTest {
 ​
     @Test
     public void testAB(){
         A a=new A();
         B b=new B();
         a.setB(b);
         b.setA(a);
     }
 ​
 }

我们启动测试类,产生了如下图的循环依赖:

学过这么一遍,Spring循环依赖问题难不倒我

但是程序本身是不会有报错的。

为什么在 Spring 中的循环依赖是一个问题?

在 Spring 中,一个对象并不是简单 new 出来了,而是会经过一系列的 Bean 的生命周期,接着注册进 IOC 容器中。

就是因为 Bean 的生命周期所以才会出现循环依赖问题。

在 Spring 中,出现循环依赖的场景很多,有的场景 Spring 自动帮我们解决了,而有的场景则需要程序员来解决。

接着我们就首先来研究下 Spring 下一个 Bean 的创建过程

Bean 生命周期

Spring Bean 的生成是一个很复杂的流程,这里我们不详细展开 Bean 的生命周期,了解就好

  • Spring 扫描 class 得到 BeanDefinition
  • 根据得到的 BeanDefinition 去根据 name/type 生成 bean
  • 首先根据 class 推断构造方法
  • 根据推断出来的构造方法,反射,得到一个对象(暂时叫做原始对象)
  • 利用依赖注入完成 Bean 中所有属性值的配置注入
  • 如果原始对象中的某个方法被 AOP 了,那么则需要根据原始对象生成一个代理对象
  • 把最终生成的代理对象放入单例池( singletonObjects )中,下次 getBean 时就直接从单例池拿即可

Spring Bean 生成过程中的主要执行方法链

学过这么一遍,Spring循环依赖问题难不倒我

  • createBeanInstance:实例化,其实也就是调用对象的构造方法或者工厂方法实例化对象
  • populateBean:填充属性,这一步主要是对 bean 的依赖属性进行注入(@Autowired)
  • initializeBean:回调执行 initMethodInitializingBean 等方法

这里可以知道循环依赖问题应该是发生在 「populateBean 填充属性」阶段的,这个时候的实例状态属于已经实例化,还未初始化的中间状态。

了解了 Bean 生命周期后我们再重新分析一下为什么会出现循环依赖问题

还是拿上文的 A 类,B 类举例子。

  • 首先创建 A 类的 Bean,A 类中存在一个 B 类的 b 属性,所以当A类生成了一个原始对象之后,就会去给 b 属性去赋值,此时就会根据 b 属性的 name/type 去 BeanFactory 中去获取 B 类所对应的单例 bean。

    • 如果此时 BeanFactory 中存在 B 类对应的 Bean,那么直接拿来赋值给 b 属性;
    • 如果此时 BeanFactory 中不存在 B 类对应的 Bean,则需要生成一个 B 对应的 Bean,然后赋值给 b 属性。

    问题就出现在第二种情况,如果此时 B 类在 BeanFactory 中还没有生成对应的 Bean,那么就需要去生成,就会经过 B 的 Bean 的生命周期。于是我们的下一步就是创建一个 B 的 Bean。

  • 接着创建 B 类的 Bean,如果 B 类中存在一个 A 类的 a 属性,那么在创建 B 的 Bean 的过程中就需要 A 类对应的Bean,但是,触发B类 Bean 的创建的条件是A类 Bean 在创建过程中的依赖注入。

    所以这里就出现了循环依赖:

    ABean 创建-->依赖了 b 属性-->触发 BBean 创建---> B 依赖了 a 属性--->需要 ABean(但 ABean 还在创建过程中)

由于以上的过程(Bean生命周期),最终导致 ABean 创建不出来,BBean 也创建不出来。

Spring 三级缓存

Spring 能解决什么情况下的循环依赖?

依赖情况依赖注入方式循环依赖是否被解决
AB相互依赖(循环依赖)均采用field注入
AB相互依赖(循环依赖)均采用setter方法注入
AB相互依赖(循环依赖)均采用构造器注入
AB相互依赖(循环依赖)A中注入B的方式为setter方法,B中注入A的方式为构造器
AB相互依赖(循环依赖)B中注入A的方式为setter方法,A中注入B的方式为构造器

Spring 如何解决循环依赖问题?三级缓存具体是什么?怎么用?

首先我们需要知道 Spring 仅仅解决单例模式下属性依赖的循环问题。

而 Spring 为了解决单例的循环依赖问题,使用了如下「三级缓存」:

 // 一级缓存,单例对象缓存池。存储所有创建好了的单例Bean
 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
 ​
 // 二级缓存。完成实例化,但是还未进行属性注入及初始化的对象,也就是半成品对象
 private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
 ​
 // 三级缓存。提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象
 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
  • 一级缓存:Map singletonObjects

    用于存储单例模式下创建的 Bean 实例(已经创建完毕)。

    该缓存是对外使用的,指的就是使用 Spring 框架的程序员。

    K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)

  • 二级缓存:Map earlySingletonObjects

    用于存储单例模式下创建的 Bean 实例(该 Bean 被提前暴露的引用,该 Bean 还在创建中)。 该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。

    K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)

  • 三级缓存:Map<String, ObjectFactory<?>> singletonFactories

    通过 ObjectFactory 对象来存储单例模式下提前暴露的 Bean 实例的引用(正在创建中)。

    该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。

    三级缓存是解决循环依赖的核心!这一点将在我们分析完成 Spring 获取单例对象的过程后搞清楚。

    K:bean 的名称 V:ObjectFactory 该对象持有提前暴露的 bean 的引用

Spring 获取单例对象过程,和三级缓存的关系

下面是 Spring 中获取单例的方法 getSingleton

 protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   // Spring首先从singletonObjects(一级缓存)中尝试获取
   Object singletonObject = this.singletonObjects.get(beanName);
   // 若是获取不到而且对象在建立中,则尝试从earlySingletonObjects(二级缓存)中获取
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
     synchronized (this.singletonObjects) {
         // 尝试从二级缓存中获取
         singletonObject = this.earlySingletonObjects.get(beanName);
         if (singletonObject == null && allowEarlyReference) {
           // 获取二级缓存
           ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
           if (singletonFactory != null) {
             //调用三级缓存,调用到lambda表达式
             //若是仍是获取不到而且容许从singletonFactories经过getObject获取,则经过singletonFactory.getObject()(三级缓存)获取
               singletonObject = singletonFactory.getObject();
               //若是获取到了则将singletonObject放入到earlySingletonObjects,也就是将三级缓存提高到二级缓存中
               //放入到二级缓存中
               this.earlySingletonObjects.put(beanName, singletonObject);
               //三级缓存中移除beanName的lambda表达式
               this.singletonFactories.remove(beanName);
           }
         }
     }
   }
   // 完整对象或者还未初始化的对象
   return (singletonObject != NULL_OBJECT ? singletonObject : null);
 }

分析 getSinglelton方法的过程:

Spring 首先从一级缓存 singletonObjects 中获取。 若是获取不到,而且对象正在建立中,就再从二级缓存 earlySingletonObjects 中获取。若是仍是获取不到且容许 singletonFactories 经过 getObject() 获取,就从三级缓存 singletonFactory.getObject() (三级缓存) 获取,若是获取到了则从三级缓存移动到了二级缓存。最后就是获取到一个半成品对象所依赖的一个完整对象,然后将完整对象注入半成品对象中。

简单来说获取 bean 的顺序就是:从一级缓存中取,若不存在,从二级缓存中取,若还是不存在,则从三级缓存中取。

  • setter 注入解决循环依赖问题

    这一部分内容可信度有限,因为我用 2.6.2 版本的 SpringBoot 实际测试的结果是 setter 注入依旧会导致循环依赖问题。但是网上的大部分言论都是 setter 注入能解决,并且我认为也有一定道理,但是为了知识的完整度也试着汇总分享出来。如果有大佬希望能在评论区解答这个问题🙏

    我们还是使用 A 和 B 的例子来介绍如上流程如何解决循环依赖问题。不过这次我们的依赖注入方式我们用的是 setter 注入。

     @Component
     public class A {
         private B b;
         @Autowired
         public void setB(B b){
             this.b=b;
         }
     }
     ​
     @Component
     public class B {
         private A a;
         @Autowired
         public void setA(A a){
             this.a=a;
         }
     }
    

    依赖注入的流程如下:

    学过这么一遍,Spring循环依赖问题难不倒我

    其中没有产生循环依赖问题!

  • setter 注入为什么能解决循环依赖?

    setter 方式解决循环依赖的核心就是提前将仅完成实例化的bean暴露出来,提供给其他bean

第三级缓存 singletonFactories,Spring 解决循环依赖的核心!

经过分析我们清楚,三级缓存最重要的就是这个第三级缓存 singletonFactories

它的元素类型是 ObjectFactory 源码如下:

 public interface ObjectFactory<T> {
     T getObject() throws BeansException;
 }

下面的这个匿名内部类实现了上面的接口:

 addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
 });

此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance(创建实例)以后,此时单例对象已经被建立。

此时对象已经被生产出来了,虽然还不完美,可是已经能被人认出来了(根据对象引用能定位到堆中的对象),因此Spring此时将这个对象提早曝光出来用来供认识和使用。

小结

本篇文章我们解决了突发的循环依赖问题,并且较为详细的解释了循环依赖究竟是什么样的问题,Spring 是如何解决循环依赖问题的。当然要彻底搞清楚这个知识点的内容还需要深入研究 Spring 源码。

本文参考: