源码解析 - Spring如何实现IoC的?

简介: 今天给大家带来的是一篇源码解析文章,关于Spring IoC的。其实源码解析不太适合写文章,做成视频更好,因为代码比较多,而且繁杂,而且调用链长,用图文很难写清楚,我尽量把它写得清楚一点~

荒腔走板


上周一冲动买了个游戏手柄。

网络异常,图片无法展示
|

小时候很喜欢玩游戏,那个时候手柄游戏还是插卡的,冒险岛和魂斗罗什么的。后来接触了电脑游戏,就很少玩手柄游戏了。


之前下载了一个《古剑奇谭3》,用键盘玩了一阵子,可能是我手残吧,始终感觉反应不过来,所以想买个手柄看能不能好一点。


最近在Steam上趁着打折买了巫师3,有朋友说,贵的不是买游戏的钱,而是时间。是的,工作以后基本上都很忙了,业余时间总是觉得不太够用,可以用来玩游戏的时间实在是太少了。这个巫师3估计够我玩一年了~


不过买手柄也是为了玩点比较轻松的游戏,工作之余适当放松一下还是有必要的,适度就好。


今天给大家带来的是一篇源码解析文章,关于Spring IoC的。其实源码解析不太适合写文章,做成视频更好,因为代码比较多,而且繁杂,而且调用链长,用图文很难写清楚,我尽量把它写得清楚一点~


本文所使用版本是SpringBoot 2.3.3.RELEASE,对应的Spring版本是5.2.8.RELEASE。


从Debug开始

一般来说,源码解析分为几个途径:直接读代码;分析类之间的关系,画UML图;Debug走一遍程序。

有时候源码可能比较复杂,比如Spring这种,链路比较长的,如果直接看代码是比较困难的,我们可以从Debug开始。

本文由于是源码解析文章,所以直接看可能有点费力,强烈推荐跟着文章一起Debug!!!

所以为了写这篇文章,我新建了一个空的SpringBoot项目,然后在启动类打了一个断点,Debug走起:

网络异常,图片无法展示
|

Debug一路往下走,会看到SpringApplication类的run方法里面,有一个创建ApplicationContext的操作:

网络异常,图片无法展示
|

当然了,在那之前有一些设置环境和Banner的操作。那这个ApplicationContext是什么东西呢?通过debug窗口我们可以看到创建的是一个AnnotationConfigServletWebServerApplicationContext实例。看看它的类图:

网络异常,图片无法展示
|


ApplicationContext

在上面的类图里,我们关注里面两个比较重要的接口:BeanFactoryApplicationContext

BeanFactory顾名思义,是一个工厂类,提供了一些获取Bean的方法,是IoC容器最基本的接口。而ApplicationContext接口继承了BeanFactory,另外通过继承其它接口赋予了更多的特性,可以看成是更高级的容器。我们从Debug也可以看到,Spring在启动的时候,创建了一个ApplicationContext,然后通过这个ApplicationContext对Bean进行后续的操作。


refresh

继续Debug,往下面走几行可以看到对之前创建的context进行了一个refresh的操作。进去后发现其实就是调用的这个context的refresh()方法。而这个方法的主要实现是在一个抽象类AbstractApplicationContext里面。

网络异常,图片无法展示
|

这个方法的代码看起来比较简单干净,是一系列的方法调用。这些方法都是protected修饰的,基本上交给子类去实现了。这里是一个比较典型的模板方法设计模式的应用。

每个方法上面都有英文的注释,说明这个方法是用来干嘛的,我这里就不翻译一遍了。不过我们需要关注的是第二个方法(533行,图中我打bookmark的地方),和第三个方法(536行,图中我打Debug断点的地方)。

这里顺便提一下Idea的bookmark功能,阅读源码的时候非常有用。你可以在你觉得比较重要的地方设置一个标记,这样以后就可以很方便地随时回到那个地方。在windows下,打一个普通的bookmark是F11键,而如果想给这个bookmark一个编号,可以按住shift + F11,可以给它打上一个数字或者一个字母用来特殊标识。如果是一个数字,比如3,那你在任何地方按ctrl + 3就可以跳回到这个打标记的地方。


注册Bean

注册Bean是通过上面提到的在refresh的一系列方法中的第二个方法来实现的:

invokeBeanFactoryPostProcessors(beanFactory);

Debug进去,发现使用了一个PostProcessorRegistrationDelegate类的静态方法invokeBeanFactoryPostProcessors。这个方法的代码很长,我们直接来到最关键的地方:

网络异常,图片无法展示
|

在ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法里面,通过最后一行方法进去processConfigBeanDefinitions方法。

这个方法的作用是找到配置类的入口。由于我是直接使用的SpringBoot,所以配置类入口只有我们自己定义的启动类SpringBaseApplication

在这个方法内部可以找到这样一行代码:

parser.parse(candidates);

注意这里的Candidates就是我们的Configuration类。

继续Debug进去,可以从下图看到它是会先判断这个candidate是不是一个用注解定义的Bean。而@Configuration注解本身是被@Component注解修饰了的,所以它是一个被注解修饰的Bean。

网络异常,图片无法展示
|

这里有一个BeanDefinition类,它是Spring用来包装和修饰一个Bean的数据结构,包括Bean的依赖、scope等等很多信息。

我们继续往下Debug,来到了ConfigurationClassParser类的processConfigurationClass方法。也是直接找到最关键的代码:

网络异常,图片无法展示
|

然后进去,发现又是一个代码非常多的方法。不要着急,我们仍然是看最关键的代码:

网络异常,图片无法展示
|

可以看到,这里取了@ComponentScan@ComponentScans注解,也就是我们会在配置类上面配置Spring应该去哪些包扫描Bean的。Spring就是在这个地方取出来的。取出来以后,从294行Debug进去看看它是怎么解析的:

网络异常,图片无法展示
|

这个方法内部比较简单,就是找到basePackage,然后scan。如果我们在@ComponantScan里面定义了basePackage,它就用我们定义的。有过SpringBoot使用经验的同学可能会知道,如果我们不用定义basePackage,Spring默认会扫描启动类所在包及其子包下的Bean。这是怎么实现的呢?原来,@SpringBootApplication会被@ComponentScan修饰,而它没有定义basePackage,那么Spring就会在上图中第123行代码,通过反射取得启动类所在的包,加入到basePackage里面。

然后我们继续通过第132行进入doScan方法。

这个方法内部就是用传进来的basePackage,通过扫描class获取到相应的BeanDefinition。然后循环去处理每个BeanDefinition,通过调用registerBeanDefinition方法,去注册Bean。

我们从registerBeanDefinition方法进去,发现最后走到了DefaultListableBeanFactory类的registerBeanDefinition方法里面。在这个方法里,Spring会先通过beanName尝试从this.beanDefinitionMap里取出一个BeanDefinition,如果没有,会把它put进这个map里。至此,Spring注册Bean的流程就算结束了。


初始化Bean

注册完Bean,只是把Bean交给Spring管理了,但这个时候Bean还没有初始化。我们回到最开始的AbstractApplicationContext类里面的那一系列模板方法的地方。下一个方法就是去初始化Bean。

registerBeanPostProcessors(beanFactory);

按照惯例,一路Debug进去,在PostProcessorRegistrationDelegate类的registerBeanPostProcessors方法里面,可以看到这里对我们定义的Bean,执行了一个beanFactory.getBean操作。而这个操作就是尝试去从Spring中拿一个Bean。如果这个Bean还没有初始化,Spring会进行初始化和依赖注入操作。

网络异常,图片无法展示
|

Debug进去,会发现主要逻辑是在AbstractBeanFactory类的doGetBean方法里面实现的。我们这个时候,我们用户定义的Bean还没有初始化,所以会走初始化流程。继续Debug,会发现这是通过doCreateBean方法来实现的。

网络异常,图片无法展示
|

在doCreateBean里面,会使用createBeanInstance创建Bean,如果发现有依赖,会通过populateBean方法来处理依赖。

在创建Bean的过程,会依次尝试使用工厂方法、构造函数、反射的方式来实现Bean的实例化。

在处理依赖过程,如果发现有依赖,会通过依赖的beanName调用getBean方法,这样就形成了一个递归调用(如果依赖又有其它依赖)。最后通过applyPropertyValues方法,对Bean的属性进行解析,然后注入相应的依赖。

如果是属性注入,底层是使用的BeanWapperImplsetValue方法,它是基于反射来实现的。

至此,Spring就完成了Bean的扫描、注册、实例化的整个过程。后面就可以通过ApplicationContext来获取Bean实例了。

当然,Spring的IoC做的非常完善,Bean的生命周期和扩展、如何解决循环依赖等等,都是有相应的代码来实现。这里只介绍了主线的从Spring启动到实例化Bean的过程,如果读者朋友对更多的细节感兴趣,可以自己去Debug多看看其它分支。


源码解析技巧

文章写完了,感觉源码解析类的文章还是蛮难写的,主要自己得理一遍完整的流程还是比较花时间。这里介绍一些源码解析的小技巧。

Debug

Debug是非常有用的,因为它能够直观地得到很多运行时信息。比如Spring的设计非常复杂,用到了很多设计模式,有很多接口和继承关系。如果不Debug,只是看代码的话,无法直观的看到这个地方的实现类是什么,想要点进去看发现有十几个实现类,一下子就懵逼了。

Debug的话,要把快捷键记熟练。这里推荐两个在Idea不常用但很实用的功能吧。

一个是F9,使用F9可以直接跳到下一个断点。而使用Alt + F9可以直接跳到光标所在的行。

另一个右键点击断点,可以给断点设置条件。这在循环里面非常有用,可以直接跳到你想要的那个条件下的地方。

bookmark

bookmark可以标记代码。我们在读源码的时候,很容易跳过去跳过来。如果不用标记的话,可能很快就找不到地方了。用了标记可以帮助我们记忆比较重要的代码,也可以快速跳转。

类图

在一个类里面点击右键,选择Diagrams -> Show Diagram可以查看这个类的继承关系。对于梳理源码中类与类之间的关系非常有用。在图里还可以添加和删除类,定制化展示我们关注的类关系。

不要陷入细节

陷入细节是读源码时非常容易碰到的一个误区。很多人觉得读源码很难,看不懂,可能就是太过于陷入细节。其实我们研究源码不一定要每一行都看懂,只要看懂它主要的实现逻辑就行了,对于我们感兴趣的,可以在后面再深入进去看,这样的话就有了一个宏观的视角,才能理清楚整个设计思路,来龙去脉。

目录
相关文章
|
5天前
|
XML 安全 前端开发
Spring Security 重点解析(下)
Spring Security 重点解析
18 1
|
5天前
|
缓存 前端开发 Java
【框架】Spring 框架重点解析
【框架】Spring 框架重点解析
21 0
|
3天前
|
Linux 网络安全 Windows
网络安全笔记-day8,DHCP部署_dhcp搭建部署,源码解析
网络安全笔记-day8,DHCP部署_dhcp搭建部署,源码解析
|
4天前
HuggingFace Tranformers 源码解析(4)
HuggingFace Tranformers 源码解析
6 0
|
4天前
HuggingFace Tranformers 源码解析(3)
HuggingFace Tranformers 源码解析
7 0
|
4天前
|
开发工具 git
HuggingFace Tranformers 源码解析(2)
HuggingFace Tranformers 源码解析
7 0
|
4天前
|
并行计算
HuggingFace Tranformers 源码解析(1)
HuggingFace Tranformers 源码解析
9 0
|
6天前
|
XML Java 数据格式
④【Spring】IOC - 基于注解方式 管理bean
④【Spring】IOC - 基于注解方式 管理bean
51 0
|
6天前
|
XML Java 数据格式
②【Spring】一文精通:IOC - 基于XML方式管理Bean
②【Spring】一文精通:IOC - 基于XML方式管理Bean
148 0
|
6天前
|
XML Java 数据格式
掌握 Spring IoC 容器与 Bean 作用域:详解 singleton 与 prototype 的使用与配置
在您的应用程序中,由 Spring IoC 容器管理的形成其核心的对象被称为 "bean"。一个 bean 是由 Spring IoC 容器实例化、组装和管理的对象
50 0

推荐镜像

更多