求求你再别问Spring循环依赖了(44张图+源码解析)

这段时间有去面试的小伙伴们是不是经常碰到面试官会这样问:

1.spring循环依赖是怎么解决的能简单说下吗?

2.用一级缓存行不行?

3.用二级缓存行不行?

很多小伙伴也都是一知半解,原因就是没有自己去调过源码,没有一步一步地来分析过,今天我们就来把这个问题说说清楚。本节的代码也都会提交到github供大家自己调试,用的是gradle哦,希望小伙伴们注意一下~

获取代码和流程图

一、循环依赖产生的原因

简单描述一下啥叫循环依赖,简单来说就是俄罗斯套娃,你中有我,我中有你。

image.png

圆和三角分别表示两个bean对象,圆里面引用了三角,三角里面引用了圆,这个就是循环依赖所产生的结果了。

在实际业务场景中其实也很多见,比如两个服务,一个是用户服务,一个是订单服务。

1.用户会通过自己的账号来查询自己的订单信息,这时候一种方案就需要在用户服务里面调用订单服务查找特定用户的订单列表。

2.商家会通过所有的订单信息按照用户进行分组,这时候就需要在订单服务里面调用用户服务来查找一批订单对应的用户的详细信息。

image.png

由于业务边界有时候很难划分清楚,就会伴随这种循环依赖的产生。但是同学们可能会说,可是我平常@Autowired的时候是那么自由自在,无拘无束,从不报错,那是因为spring已经帮我们解决了这个问题了。

二、代码准备

代码目录结构如下

image.png

1.InstanceA

public class InstanceA {
	private InstanceB b;

	public InstanceB getB() {
		return b;
	}

	public void setB(InstanceB b) {
		this.b = b;
	}
}
复制代码

2.InstanceB

public class InstanceB {

	private InstanceA a;

	public InstanceA getA() {
		return a;
	}

	public void setA(InstanceA a) {
		this.a = a;
	}
}
复制代码

3.aa.xml配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
  xmlns:util="http://www.springframework.org/schema/util"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd">

  <!-- 1. 使用property初始化Bean属性 -->
  <bean id="a" class="com.lyf.spring.config.InstanceA">
	  <property name="b" ref="b"></property>
  </bean>


  <bean id="b" class="com.lyf.spring.config.InstanceB">
	  <property name="a" ref="a"></property>
  </bean>
</beans>
复制代码

4.main函数

public static void main(String[] args) {
		ApplicationContext xc = new ClassPathXmlApplicationContext("aa.xml");
		InstanceA instanceA = xc.getBean(InstanceA.class);
	}
复制代码

三、图解+源码

我一直在想用文字如何描述比较好,考虑下来还是把关键点一步一步地和大家一起来看下,最后把流程图也给大家贴出来,如果描述不清楚的可以配合流程图来进行调试。

首先需要大家清楚的是三级缓存其实指的就是3个map,这三个名字请大家务必记牢

image.png

下面我们开始进入调试

1.找到入口方法refresh()

image.png

找到 refresh() 方法,这个是我们bean实例化的入口,也是我们本次调试循环依赖的入口 image.png

2.进入refresh()找到执行实例化动作的finishBeanFactoryInitialization(beanFactory)

image.png

3.进入finishBeanFactoryInitialization(beanFactory)找到beanFactory.preInstantiateSingletons()

image.png

4.preInstantiateSingletons()方法内部找到getBean(beanName)

1. 通过循环来分别执行InstanceA和InstanceB的实例化和初始化

image.png

2.经过一系列判断,进入else逻辑找到getBean(beanName)

//判断是否是抽象类/单例/懒加载
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
    //判断是否继承FactoryBean
    if (isFactoryBean(beanName))
复制代码

image.png

5.通过getBean(beanName)找到doGetBean进入其实现类重点看下getSingleton(beanName)

image.png

进入到getSingleton(beanName)里面,这个方法需要大家记住,我们实例化好的和初始化好的对象都会经过这个方法来从各级缓存中去尝试读取。

image.png

	/** Names of beans that are currently in creation */
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));
复制代码

这个集合需要记一下,用于记录当前处于创建阶段的bean的beanname,由于当前我们在实例化a的时候还没有进入到创建阶段,仅仅只是去缓存中尝试获取a,如果存在就直接拿到了,现在是不存在,所以返回null

6.由于一级缓存中不存在a,并且a也不处于create阶段,所以此时需要去触发a的实例化

image.png

image.png

7.进去getObject()进去匿名内部类中,createBean(beanName, mbd, args)

image.png

8.找到doCreateBean(beanName, mbdToUse, args)方法

image.png

9.进入doCreateBean(beanName, mbdToUse, args)

找到下面这个方法,这个方法的作用就是把a及其匿名内部类放入三级缓存

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
复制代码

1.判断传入的ObjectFactory是否为空

2.给一级缓存加锁

3.判断一级缓存中是否存在a,显然是不存在的

4.把a及其ObjectFactory放入singletonFactories也就是三级缓存中

image.png

到这一步我们的三级缓存中终于有东西进来了现在的状态如下图

image.png

bean a目前是这种状态,所以接下来需要把a里面的元素填充好 image.png

10 这里我们已经把a放入了三级缓存,开始填充bean

找到关键行

image.png

这里我们稍微跳一跳,因为中间涉及的校验操作比较多。

进入populateBean

找到最后一行

applyPropertyValues(beanName, mbd, bw, pvs);
复制代码

毫不犹豫的进入,啥也不管,冲就完事,跑到这个for循环

image.png

11.去缓存中去找b是否存在

找到这行关键代码

Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
复制代码

一直跑我们发现最后进到了我们最初的doGetBean里面

image.png

是不是似曾相识很熟悉的感觉,找到老乡了。

好这里我们发现,我们在a实例化的时候发现要填充bean b,但是由于b也不存在,所以又去创建bean b。

所以说同学们,很关键,这里按照同样的路径来实例化b的时候会有什么不同呢?同学们思考一下

image.png

我们之前创建a的时候是不是在步骤9的时候把a放入了,三级缓存了,我们接着往下走。

12.同样我们会把b也放入三级缓存

image.png 这点大家同不同意,然后现在三级缓存中会出现这样的画面

image.png

13.转折点就在填充b实例的时候

image.png

一路追踪下来,不知道大家还记不记得这张图片,通过一个for循环来逐个填充对象

image.png

在这个方法中我们回去缓存中去找a是否存在,显然a已经在三级缓存中存在了

Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
复制代码

再次进入getSingleton,熟悉的味道 image.png

但是此时有两个不同的点

1.此时b去缓存中找a的时候,一级缓存中确实不存在a,但是此时a已经在三级缓存中,所处的状态是创建中

2.a已经存在于singletonFactories三级缓存中

image.png

之前我们提到过,在三级缓存中放的是

(a, () -> getEarlyBeanReference(beanName, mbd, bean))
复制代码

所以这里执行getEarlyBeanReference这个方法进行来一些操作

同时把a从三级缓存移除,放入二级缓存,三级缓存中的分布发生如下变化

image.png

14.从缓存中取到了a,这时候b就很开心

此时我们要回到我们之前进入的代码,也就是步骤11的这一行代码

Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
复制代码

可以看到返回了一个实例化完成的a

image.png

a的实例化对象我们也拿到了,这时候就可以去初始化b了

15 别慌,一直往下走,光明就在前方

我们再次回到这里

image.png 之前我们不是执行了bean的填充,可以看到此时我们的b中已经有a了

image.png

下面就要执行b的初始化了

16.初始化具体做了啥,我们这边暂时不深入追踪,一直往下走,直到这一步

image.png

我们大胆猜测,此时b已经完成了初始化动作,这一步就把b从三级缓存直接丢进了一级缓存中。

image.png

记住这个方法,之后也会用到,这时候我们缓存的分布需要再次做调整,如下

image.png

17.带大家回想一下

同学们,我们从步骤1开始到现在步骤16结束了,我们最初要做的事情大家还记得吗?

是科学家吗?还是人民警察呢?

嘿嘿,不开玩笑,我们最初要做的是得到a对象,到目前为止我们完成的工作是把b实例化、初始化完成了,所以我们再次回到主线剧情,填充a对象

也就是梦开始的地方

image.png 可以看到此时b对象是长这样的

image.png

接下来的步骤大家应该清楚了把

1.填充a

2.初始化a

3.把a从二级缓存移到一级缓存

image.png

所以此时三级缓存的内存分布就完成了

image.png

所以我们的a对象已经放到了一级缓存中,可以供大家一起使用了,我们再来看一下a到底长啥样

image.png

和预想中的完全一样,开启了俄罗斯套娃模式,有兴趣的同学可以点点看,会不会有结束?

18.所以a完成了实例化和初始化了,大家还记不记得我们最开始的那个for循环

image.png

也就是我们最外层的循环,这时候我们同样需要去实例化、初始化b对象,但是可以预见到的是,由于b已经存在于一级缓存了,所以此时可以直接从一级缓存中把b取出来直接使用啦~~

所以大家清楚了整个流程了没呢? 最后来一张整个调试过程中记录下来的终极无敌流程图压场

image.png

image.png

希望小伙伴们也自己一步一步地去调试一下,光看不练假把式,最后记得点赞👍+关注哦~~


作者:敲脑壳呀敲脑壳
链接:https://juejin.cn/post/6942698857260122143
来源:掘金
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值