荒腔走板
上周一冲动买了个游戏手柄。
小时候很喜欢玩游戏,那个时候手柄游戏还是插卡的,冒险岛和魂斗罗什么的。后来接触了电脑游戏,就很少玩手柄游戏了。
之前下载了一个《古剑奇谭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
在上面的类图里,我们关注里面两个比较重要的接口:BeanFactory
和ApplicationContext
。
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的属性进行解析,然后注入相应的依赖。
如果是属性注入,底层是使用的BeanWapperImpl
的setValue
方法,它是基于反射来实现的。
至此,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可以查看这个类的继承关系。对于梳理源码中类与类之间的关系非常有用。在图里还可以添加和删除类,定制化展示我们关注的类关系。
不要陷入细节
陷入细节是读源码时非常容易碰到的一个误区。很多人觉得读源码很难,看不懂,可能就是太过于陷入细节。其实我们研究源码不一定要每一行都看懂,只要看懂它主要的实现逻辑就行了,对于我们感兴趣的,可以在后面再深入进去看,这样的话就有了一个宏观的视角,才能理清楚整个设计思路,来龙去脉。