吐血给女朋友讲解Spring循环依赖

开发 前端
有的小伙伴内心对于Spring的看法可能是,感觉自己懂了,但是让自己来叙述给一个小白的话,可能部分人就讲不出来了,这一篇呢,主要就是帮你解决这个痛点,让你彻底搞定Spring的循环依赖.

[[430219]]

前言

我们前面说了几遍Spring的文章,了解了比较核心的知识点IOC和AOP,还有就是事务传播这种,不知道大家听过Spring的循环依赖这个问题吗,而且这个问题是面试经常问的,属于Spring的一个比较重要的话题,也比较典型,比较考验一个人对Spring的研究程度,也算是Spring的一个高阶问题之一了

有的小伙伴内心对于Spring的看法可能是,感觉自己懂了,但是让自己来叙述给一个小白的话,可能部分人就讲不出来了,这一篇呢,主要就是帮你解决这个痛点,让你彻底搞定Spring的循环依赖,也让你下次不再担心,而是张口就来

面试前看一看,offer轻松拿一拿,你还不关注等啥呢,月薪50K的薪资等着你呢,到时候如果你纠结选择哪个offer,可以来把你的喜讯分享给我的嘞

循环依赖问题,本文会通过三个方面来简单介绍

1、什么是Spring的循环依赖

2、多种情况下的循环依赖

3、Spring如何解决循环依赖

了解Spring循环依赖

什么是循环依赖

  1. @Component 
  2. public class AService { 
  3.     // AService中注入了BService 
  4.  @Autowired 
  5.  private BService bService; 
  6.  
  7. @Component 
  8. public class BService { 
  9.     // BService中也注入了AService 
  10.  @Autowired 
  11.  private AService aService; 

这是属于比较常见的一种循环依赖,还有就是更多的之间的相互依赖,比如A依赖B,B依赖C,C依赖A,类似于三角恋...

当然也有特殊的,自己的依赖自己

  1. // 自己依赖自己 
  2. @Component 
  3. public class AService { 
  4.     // A中注入了A 
  5.  @Autowired 
  6.  private AService aService; 

关于上面service的Spring bean的创建,其实本质上也会一个对象的创建,既然是对象,就要明白一个对象需要完成的对象包含两个部分:当前对象的实例化和对象属性的实例化

我们如果用一个常人的思维去考虑,这肯定是做不到的啊,A需要B,B需要A,这不成死循环了,和死锁一个道理了,这就很尴尬了,该怎么解决呢,接着看下去

多种情况下的循环依赖

Spring的循环依赖也是可能出现多种情况的,比如构造器注入,setter注入等等,那什么情况下Spring可以解决循环依赖,什么情况下又不能解决呢

Spring解决循环依赖的前提条件就是:

1、出现循环依赖的bean必须是单例的

2、依赖注入的方式不能全是构造器注入

注意不能全是这几个字眼,这里需要强调一点的是,大家可能会看到很多关于Spring解决循环依赖的博客,其中只能解决setter注入的方式这种说法是错误的,只要不全是构造器注入Spring就可以解决

Spring bean的创建,其实本质上也会一个对象的创建,既然是对象,就要明白一个对象需要完成的对象包含两个部分:当前对象的实例化和对象属性的实例化,在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方法设置的,知道了这一点,可以更好的帮助大家去理解

上面说的第一点必须是单例的其实很好理解,你想啊,如果是多个实例,该引入哪个实例就不知道了,Spring框架就蒙圈了,大胆猜测一下,Spring以后可能会尝试解决这个问题,大概率是通过配置的方式来告诉该注入哪个

但是第二点呢,不能全是构造器注入呢,先看代码

  1. @Component 
  2. public class AService { 
  3. // @Autowired 
  4. // private BService bService; 
  5.  public AService(BService bService) { 
  6.  
  7.  } 
  8.  
  9.  
  10. @Component 
  11. public class BService { 
  12.  
  13. // @Autowired 
  14. // private AService aService; 
  15.  
  16.  public BService(AService aService){ 
  17.  
  18.  } 

上面这个例子大家看得懂吧,别告诉我你看不懂,AService中的BService注入是通过构造器,反之也是通过构造器注入,这个时候Spring是无法解决循环依赖问题的,如果项目中出现两个这样的引用,在启动的时候就会直接抛出异常

  1. Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference? 

解决循环依赖

首先呢,Spring解决循环依赖依靠的就是内部维护的三个Map,也就是咱们常说的三级缓存,不知道大家听过没有

之所以被叫做三级缓存,大概是因为注释上都是用Cache,而起的作用也类似一个缓存的作用

1、singletonObjects:这个是单例的容器池,缓存创建完成单例Bean的地方

2、singletonFactories:用来映射创建Bean的原始工厂

3、earlySingletonObjects:用来映射Bean的早起引用的,也就是说在这个Map中的Bean不是完整的,属于半成品,甚至还不能被称为Bean,只是一个Instance

后面的两个Map其实属于是过程中的消耗品,什么意思呢,就是创建Bean的时候,需要暂时存储在这里,过后完成之后就清除掉了

我们先来看一下这个创建过程,我把这个大致分了四个步骤,看着也比较清晰,大家也比较好理解:

1、A找B找不到:

创建AService的过程中发现需要BService,于是AService将去寻找BService,发现找不到,寻找的路径是一级缓存、二级缓存、三级缓存,于是AService把自己放到了三级缓存中

2、B找A找到了:

实例化BService,实例化过程中发现需要AService,于是也是按照上述路径去寻找,在三级缓存中找到了AService

3、B创建完成:

然后就把三级缓存中的AService拿出来,放到了二级缓存中,并删除三级缓存中的AService,此时BService可以成功引用,顺利初始化完毕,把自己放到了一级缓存中了(而此时BService中的AService依然是创建中的状态

4、A创建完成:

继续完善AService,此时再去寻找BService,拿出来直接引用就好了,把自己放入到一级缓存中,删除二级缓存,删除在创建的状态信息

问题的本质和two sum的本质有些类似,这个是leetcode上序号为1 的题目,问题的内容是:

给定nums = [2,7,11,15] , target = 9,那么要返回[0,1] ,因为2 + 7 = 9

这道题的解题思路就是通过Map,先去Map中找到需要的数字,没有就把当前数字放进去,有的话就直接拿到并一起返回

和三级缓存的本质类似,先去缓存中找到需要的Bean,找到了万事大吉,直接创建完成返回,找不到就把自己放进去

来一起简单的分析一波源码,不看太多,不用发愁

单例模式下,第一次获取Bean时由于Bean示例还未实例化,因此会先创建Bean然后放入缓存中,以后再次调用获取Bean方法将直接从缓存中获取,不再重新创建。

缓存添加过程主要发生在创建Bean也就是doCreateBean()过程中。

从代码中可以看到在这里将beanName放入三级缓存中,并且从二级缓存中移除。这里的addSingletonFactory发生在createBeanInstance之后,此时已经有了实例对象,但是还没创建完成便放入三级缓存当中。

在doCreateBean()方法执行完成之后,Bean实例创建完成,因此在第二个getSingleton()中的finally块中如果是新的单例对象则会调用addSingleton()方法

可以看到此时将加入一级缓存中,并且从二级、三级缓存中移除。

缓存的获取是通过getSingleton(String beanName)方法获取的,其源码如下:

在代码中,首先从一级缓存singletonObjects中获取Bean;如果获取不到并且获取的Bean被标记为正在创建中,则从二级缓存earlySingletonObjects中获取

如果二级缓存中依然获取不到Bean,并且允许从三级换从中获取Bean,则从三级缓存中获取Bean,此时如果获取到了Bean,则将该Bean从三级缓存中移除,然后添加进二级缓存(缓存升级),否则返回null。

最后问一个灵魂的问题,为什么不用二级缓存,而用三级缓存呢?

如果创建的Bean有对应的代理,那其他对象注入时,注入的应该是对应的代理对象;但是Spring无法提前知道这个对象是不是有循环依赖的情况,而正常情况下(没有循环依赖情况),Spring都是在创建好完成品Bean之后才创建对应的代理。这时候Spring有两个选择:

不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入。

不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。

Spring选择了第二种方式,那怎么做到提前曝光对象而又不生成代理呢?

Spring就是在对象外面包一层ObjectFactory,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory.getObject方式内实时生成代理对象,并将生成好的代理对象放入到第二级缓存Map

本文转载自微信公众号「Java贼船」

 

责任编辑:姜华 来源: Java贼船
相关推荐

2020-06-22 08:07:48

Spring依赖场景

2019-10-23 07:00:13

TCP三次握手四次挥手

2019-03-12 09:43:14

反向代理正向代理服务器

2019-04-09 09:40:23

2020-03-16 14:08:59

线程熔断限流

2019-10-09 10:45:16

云计算Web互联网

2021-09-14 12:00:11

VR字节跳动

2021-06-22 07:45:57

React18startTransiReact

2021-06-22 07:30:07

React18Automatic b自动批处理

2019-12-23 10:26:02

3PC分布式2PC

2023-05-04 08:06:27

Spring循环依赖

2019-04-19 09:48:53

乐观锁悲观锁数据库

2020-10-19 13:01:31

删库程序员思科

2021-03-11 16:45:29

TCP程序C语言

2019-04-26 14:46:18

GitGitHub局域网

2021-05-19 11:02:44

PythonTurtle参数

2019-07-22 10:34:31

大案牍术大数据Big Data

2015-08-26 10:17:29

程序员女朋友

2021-03-09 12:27:05

微服务 微服务架构应用程序

2020-10-25 08:22:28

V8 引擎JavaScript回调函数
点赞
收藏

51CTO技术栈公众号