JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

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

wys521 2025-02-10 14:44:13 精选教程 25 ℃ 0 评论

简单介绍

碰上问题了

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

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

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

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

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

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

什么是循环依赖?

循环依赖简单定义

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

我们新建一个小的 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,这样就是一个简单的循环依赖。

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

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

解决开始的问题

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

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

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

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

所以由于 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 中,一个对象并不是简单 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 生成过程中的主要执行方法链

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

这里可以知道循环依赖问题应该是发生在 「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 singletonObjects = new ConcurrentHashMap(256);
 ?
 // 二级缓存。完成实例化,但是还未进行属性注入及初始化的对象,也就是半成品对象
 private final Map earlySingletonObjects = new HashMap(16);
 ?
 // 三级缓存。提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象
 private final Map> singletonFactories = new HashMap>(16);
复制代码
  • 一级缓存:Map singletonObjects
  • 用于存储单例模式下创建的 Bean 实例(已经创建完毕)。
  • 该缓存是对外使用的,指的就是使用 Spring 框架的程序员。
  • K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)
  • 二级缓存:Map earlySingletonObjects
  • 用于存储单例模式下创建的 Bean 实例(该 Bean 被提前暴露的引用,该 Bean 还在创建中)。 该缓存是对内使用的,指的就是 Spring 框架内部逻辑使用该缓存。
  • K:bean 的名称 V:bean 的实例对象(有代理对象则指的是代理对象,已经创建完毕)
  • 三级缓存:Map> 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; } } 复制代码
  • 依赖注入的流程如下:

  • 其中没有产生循环依赖问题!
  • setter 注入为什么能解决循环依赖?
  • setter 方式解决循环依赖的核心就是提前将仅完成实例化的bean暴露出来,提供给其他bean

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

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

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

 public interface ObjectFactory {
     T getObject() throws BeansException;
 }
复制代码

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

 addSingletonFactory(beanName, new ObjectFactory() {
    @Override
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
 });
复制代码

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

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

小结

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



原文链接:
https://juejin.cn/post/7202108826974978106


本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表