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

您所在的位置:网站首页 spring怎么解决 学过这么一遍,Spring循环依赖问题难不倒我

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

2023-12-13 14:57| 来源: 网络整理| 查看: 265

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

简单介绍

碰上问题了

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

image.png

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

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

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

img

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

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

什么是循环依赖?

循环依赖简单定义

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

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

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

image.png

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

解决开始的问题

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

image.png

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

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

image.png

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

image.png

所以由于 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);     }  ​  }

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

image.png

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

为什么在 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 生成过程中的主要执行方法链

image.png

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>(16);

一级缓存:Map singletonObjects

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

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

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

二级缓存:Map earlySingletonObjects

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

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

三级缓存:Map 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;     }  }

依赖注入的流程如下:

image.png

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

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 源码。

本文参考:

juejin.cn/post/698533… developer.aliyun.com/article/766… pdai.tech/md/spring/s… doocs.github.io/source-code… www.jianshu.com/p/804954859… www.cnblogs.com/zhangshuaiy…


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3