手撕Spring源码(三),彻底理解Spring循环依赖原理

ywz888
发布于 2022-10-9 10:37
浏览
0收藏

很多朋友大概有注意到,我写过一些文章解释清楚了一些:全网没有其他文章解释清楚,或者大多数文章都是错误的问题。比如:《说透分布式事务》里BASE理论和分布式事务到底是什么关系。

 

本篇文章我在动笔之前也搜索了一下,包含文章和收费视频。发现自己花了钱学习的东西老师(还是这个方面口碑很好的老师)在翻来覆去啰嗦那么几句话,就是没把问题讲透。为啥呢?我分析了一下,因为老师从一开始没有说明这么设计要解决的问题呀。

 

上篇回顾

 

本篇是手撕Spring源码系列的第三篇。由于上下文之间的逻辑关系,没看过前两篇的朋友强烈建议先看前两篇。上篇传送门:《手撕Spring源码(二),彻底理解Spring后置处理器》。

 

强化是深度记忆的有效手段。简单对上篇做个总结。Spring Bean创建的过程为:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

1、根据class生成Bean定义

2、根据Bean进行实例化

3、将实例的Bean属性进行填充

4、初始化Bean

5、Bean后置处理

6、完成Bean对象的创建

 

本系列的所有代码文字在 https://github.com/xiexiaojing/yuna 里可以找到。

 

引出问题

 

在上篇最后的代码中,咱们来做这么一件事,让 testService 引用 userService :

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

加上原来的 userService 引用了 testService :

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

运行启动类:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

循环引用导致栈溢出:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

原因看代码就很明白了,有递归调用导致死循环依赖:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

咱们模拟的就是Spring源码,原理和Spring源码是一样的。那Spring最初也遇到了这个问题。它是怎么解决的呢?

 

单例池二级缓存

 

Spring解决的思路也是很常规的思路:testService 引用了 userService 。要创建userService需要先获取testService的Bean。那能不能先创建一个半成品先用着呢?这就是Spring解决循环依赖的二级缓存思想。

 

有二级缓存,那就有一级缓存。什么是一级缓存呢?咱们头两节课里介绍获取Bean在Scope为单例时是从singletonObjects也就是单例池中获取的。这个单例池作用是缓存单例对象,这就是一级缓存。

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

那咱们来看二级缓存,也叫半成品池。思想就是如果获取Bean的时候发现Bean不存在,就从半成品池里获取。如果获取不到则立即实例化一个放到半成品池里。这样,其他Bean创建依赖于它时,就可以直接先拿来用。确保依赖它的可以实例化成功。

 

 

定义半成品池:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

获取Bean时如果如果获取不到则立即实例化一个放到半成品池里:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

总共加上了这四行代码之后咱们再来运行一次:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

单例池二级缓存存在的问题

 

记不记得咱们曾经手撕过mybatis的源码?可以回顾一下《mybatis的本质和原理》这篇文章。想抽空写一篇手撕spring整合mybatis的,大家有兴趣的可以在评论区留言。本篇在看超15,就是默认想看了,8小时内我就把文章肝出来~

 

spring整合mybatis时,咱们使用时定义接口,并把扫描注解标注到接口上:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

本质上Repository就是一个Component注解:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

那问题来了,mybatis的操作sql语句的方法是接口不是类,不能被实例化呀,二级缓存提前实例化是有问题的。不信咱们来试验一下:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

注入一个接口,启动时就报错了:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

因为本身啊,咱们利用Spring注入的Bean是代理类。比如mybatis的接口最终使用的是mybatis在执行完Bean后置处理器之后的代理类。

 

问题明确了:二级缓存由于提前实例化,实例化了一些不能被实例化的接口,会报错。

 

 

单例池三级缓存

 

那如果你是Spring的设计者怎么解决这个问题?那如果发现是接口就提前执行后置器处理器把代理类创建出来使用呗。别说,Spring的设计者还真也是这么处理的。

 

首先定义一个接口,这个接口直接从Spring源码里拷贝一下:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

这个接口的实现类作用就是实现getObject把接口真正替换成代理类。这里举得是mybatis的实现,具体可参考《mybatis的本质和原理》:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

本质上Repository就是一个Component注解的别名。为了让咱们在spring的注解能扫描到,咱们把它替换成@Component注解:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

由于类所在的位置,咱们把扫描范围扩大些:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

定义三级缓存也叫工厂池:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

如果是接口先实例化工厂池对象,从工厂池对象中获取真正的Bean对象:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

这里只是一个put操作,为了尽量和Spring源码保持一致,单独拆成方法,加了下面判断,其实核心就一行:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

其他就是因为提前实例化了,在创建Bean时判断如果已经实例化则不必再次实例化,只是一个判断:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

改造就是这些,咱们来运行一下:

手撕Spring源码(三),彻底理解Spring循环依赖原理-鸿蒙开发者社区

总结

 

本篇针对Spring创建时会遇到的两个问题:死循环依赖问题和接口实例化问题给出了Spring的解决方案。

 

 

虽然代码是我手撕的,和源码不完全一致,但原理是一样的。面试的时候跟人家说Spring就是这么实现的,一点问题也没有。

 

在本系列的第一篇《手撕spring核心源码,彻底搞懂spring流程》里其实Spring IoC控制反转和依赖注入原理就已经讲完并实现了。这两篇文章都是针对在此基础上的需求和问题。《手撕Spring源码(二),彻底理解Spring后置处理器》解决的是对Bean做增强的问题,本篇是随着Bean的多样性引发的问题是怎么解决的。

 

Spring本身就是如此:基于一个简单的想法,再解决设计中遇到的问题。

 

文章转载自公众号:编程一生

 

分类
已于2022-10-9 10:37:03修改
收藏
回复
举报
回复
    相关推荐