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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 今天给大家带来的是一篇源码解析文章,关于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可以查看这个类的继承关系。对于梳理源码中类与类之间的关系非常有用。在图里还可以添加和删除类,定制化展示我们关注的类关系。

不要陷入细节

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

目录
相关文章
|
8天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
10天前
|
搜索推荐 Java Spring
Spring Filter深度解析
【10月更文挑战第21天】Spring Filter 是 Spring 框架中非常重要的一部分,它为请求处理提供了灵活的控制和扩展机制。通过合理配置和使用 Filter,可以实现各种个性化的功能,提升应用的安全性、可靠性和性能。还可以结合具体的代码示例和实际应用案例,进一步深入探讨 Spring Filter 的具体应用和优化技巧,使对它的理解更加全面和深入。
|
4天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
21 3
|
6天前
|
XML 缓存 Java
搞透 IOC、Spring IOC ,看这篇就够了!
本文详细解析了Spring框架的核心内容——IOC(控制反转)及其依赖注入(DI)的实现原理,帮助读者理解如何通过IOC实现组件解耦,提高程序的灵活性和可维护性。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
|
22天前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
48 5
|
23天前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
|
18天前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
46 0
|
24天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
58 0
|
24天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
49 0
|
24天前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
55 0

推荐镜像

更多