阅读Spring Frameworks源码的思考
转载请注明出处即可。
这不是一篇讲Spring源码解析的文章,也不是剖析Spring内部设计的文章。只是在阅读中的一些思考。
一、为什么很多优秀框架或系统源码感觉难以阅读或理解
(1) 时间的考验
任何优秀的框架、系统和编程语言等都是经历了很长时间的考验,比如Spring 1.0的这篇距今也有14年的时间。Python和Linux的出现也要早于utf8。这也是Python2对字符串操作一直被人诟病的原因之一。
长时间的发展也会让最初可能简单的逻辑,变得非常复杂。比如早期版本的Linux只有1万多行代码,现在拥有了超过3000万行的代码量。
代码量对比
正如上图对比的那样,随着时间的发展,优秀项目的代码量会越来越多,并且越来越复杂。导致如果直接去看源码很容易抓不住重点,把思维淹没在各种细节之中不能自拔。
但是无论版本功能如何迭代,最初的核心设计思想是不会进行变化的。比如Linux 0.11就有了进程。Python Flask的路由和始终和早期版本的实现几乎一样没有变化。所以如果直接打开Spring5的源码一脸蒙圈的话,其实可以去下载Spring早期版本的源码去看下设计思想和原理,因为代码量和功能的减少,更容易抓住核心设计思想,而不是和部分细节纠缠不清。
回到我们的主题Spring
spring-beans-1.2
spring-context-1.2
spring-core-1.2
因为版本很老,需要下载jar包,然后在add jar到项目中(当然这三个包肯定是跑不起来的,还需要依赖apache和logger等其他相关的依赖包,本小节结尾会附录老版本源码下载地址)。
我们看下老版本的使用, 在使用上几乎没有变化。
我们在看下
ApplicationContext
的类图,并和Spring5的做一个对比Spring 1.2类图
Spring 5类图
当然还可以对比下类的数量,Spring 1.2的也会少很多, 但核心的接口也都在。
XmlBeanDefinitionReader
读到这里,如果陷入了Spring5源码的泥潭不得主线,那么推荐阅读下老版本的源码,去理解下早期"最核心"的那些框架思想。
Spring老版本源码下载地址
Spring Framework-1.2源码
(2) 因为简单所以复杂
在技术领域,简洁的背后一定有很复杂的逻辑。
用个支付领域的例子,看似简单的支付界面,背后其实拥有很复杂的逻辑。
滴滴支付 支付系统架构
Linux系统虽然3000W+的代码量,但系统调用依然维持在200+的数量,而MacOS的系统调用还不到200。JVM的指令也在200+。
Spring其实也一样,ApplicationContext的方法虽然不多,但拥有很长的继承链,和很多技术细节。
ApplicationContext
所以使用起来很简单的东西,其内部为了简化外部的使用,会做很多封装。但是无需被背后复杂的逻辑阻断源码阅读的进程,还是有一些其他手段来解决对繁杂代码的理解。
(3) 一团浆糊
阅读源码的时,经常会发现看的细节越来越多,越往后就可能记不清楚前面的逻辑。这个是十分正常的现象。因为在人的大脑进行记忆并进行分析的特征(机器学习里面的概念,也可以叫做属性)是有限的。
当特征(体重、翼展等)数量不是很多时,相关专家可以根据在脑海中记忆的经验,来判断当前鸟属于哪个种属。专家系统的作用也是类似,有专门对这些特征处理,判断"经验",来确定种属。但是当特征数很多,比如上万个特征,数十万的特征时,人脑远不能处理这么海量的数据,更不用说去分析了。所以这个时候,机器学习和深度学习就起到了它们应有的作用。
在阅读源码时,一般会通过记忆,然后理清顺序的执行流程,然后加上之前的记忆整理和分析,得出某个功能的实现逻辑或者设计思想。但是如果在阅读链路比较长的情况下,找不到分析的切入点是很正常的情况,甚至在后续的阅读中导致前面的记忆开始模糊不清。在这种情况下,硬着头皮记笔记,画类图是解决记忆模糊和简化理解的方式之一。
当然还可以换一种方式去理解源码,比如,将理解源码的过程分为两步,第一, Spring 对外提供的接口,包含了哪些功能, 并且这些功能上有什么分类(不是ioc, aop这种分类,后面小节会详述)。第二, 将第一步分类后的接口,去查看继承链和引用关系,控制只在一个分类中进行阅读, 降低整体功能多导致的记忆、理解、分析的问题。当然如果还可以继续分类的话,还可以进一步降低阅读的难度。
如果在从某一个方法调用开始打断点后,一直往下读,读不懂的话,可以试试这个方式。并且不用担心会丢失整体的逻辑。因为所有的技术都是某种组合。这意味着任何具体技术都是由当下的部件、集成件或系统组件构建或组合而成的。其次,技术的每个组件自身也是微缩的技术。
(4) 明白了执行过程,但感觉没抓住设计思想
借用《如何阅读一本书》里面的阅读层次划分,阅读源码的也应该有四个层次,可以对标下自己的源码理解在哪个层次。
a. 基础阅读
在这个层次,应该可以熟练的编写Java代码,并且熟悉常用的类,反射,动态搭理、设计模式,面向对象的设计思想等相关内容。这些是阅读Spring Frameworks的必备知识,否则很容易知其然不知所以然。比如大家都看过NBA,但是不同人看到的NBA是不同的,比如一个妙传,大部分人都认为是好球,但是在教练看来就可能是某种战术的成功。在球员的眼里也不一样。
曼巴精神
这本书在微信阅读里面有中文版。
b. 检视阅读
检视阅读是系统化略读的一门艺术。在这个阶段,也是建立横向逻辑(后续小节会详述)的一个过程。需要能找到相对独立的模块,并且找到模块与模块之间的关联关系。这里不进行详述,后面再说。
c. 分析阅读
分析阅读是指,一直要读到对整个源码深刻的理解。既能理解整体架构,又能理解核心技术细节。当然在阅读的过程中可能会有疑惑,这个在后续章节会描述解决疑惑的方法。
d. 主题阅读
主题阅读也叫做比较阅读,也就是说需要阅读其他的类似的框架源码,并不只是Spring。不仅仅要能列出不同框架的相关之处,也能对不同框架优缺点有深刻的理解。艺术书并不能保证你阅读之后成为艺术家,只能告诉你其他艺术家用过的工具、技术和思维过程。当然阅读Spring也不能担保成为好的软件工程师。但主题阅读的这个阶段,就已经是对原理和思想内化的过程,也是整个源码阅读中最有收货的。因为获益良多,所以绝对值得花时间去努力做到这个层次。
(5) 好代码潜规则
整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。
好的代码是一种艺术,Spring更是如此。虽然代码量相对较大,但Spring依然保持了十分整洁的代码。比如所以类和方法的命名都有其正确的意义。就算单独的一个方法名看不出来含义。上下逻辑中依然保持了足够的上下文用于理解。并且函数的"摆放"位置也十分的"考究"。
以SimpleAliasRegistry
为例(在不考虑setter和getter的情况下)
方法checkForAliasCircle
一定在文件最后一个调用方法的下面。
registerAlias
, resolveAliases
两个方法都调用了checkForAliasCircle
但由于在文件中最后一个调用,所以放在了resolveAliases
的后面。
二、如何在阅读源码过程中建立逻辑
(1) 什么是逻辑
逻辑是指把源码的理解组织起来,这个也是达到检视阅读和分析阅读的必要步骤。逻辑分为两个分类。纵向逻辑是指开发都能看懂的因果关系。因为A,所以B, 因为B, 所以C。横向逻辑是指开发能看懂的总分关系,没有遗漏和重复。A包括B和C。
逻辑
(2) 纵向逻辑
纵向逻辑是指,因为A所以B,既达到某个源码细节(方法, 类)的逻辑达到可以理解的地步。
纵向逻辑薄弱一般有三个原因。
a. 前提条件不同
前提条件不同虽然是因为A,所以B,但是还有A', A''等隐含条件。
比如下面这段代码
示例代码
,
通过这个可以说ApplicationContext接口的实现类提供了通过通配符读取Resource数组的功能(这个是错误的说法)。看到这里就算没有读过源码的人可能在想了"真的是这样吗?" 得出这样错误的结论是因为忽视了底层实现的隐含条件,导致了纵向逻辑薄弱或者直接导致逻辑错误。
实际ApplicationContext并没有亲力亲为,而是委托给了
PathMatchingResourcePatternResolver
。
b. 把不同性质的东西混为一谈
把不同性质的东西混为一谈这里用分布式里面的经常混淆的一个问题来说明。笔者所在的技术群,某个群友说分布式系统就是在CAP下进行权衡。且不说这句话对还是不对。这句话其实混淆了分布式系统的概念和分布式系统的问题。导致了这句话不太经得起推敲。首先分布式系统的概念是指 若干独立计算机的集合,这些计算机对于"用户"来说就像是单个相关系统。一致性其实是在分布式系统理论中要解决的问题。那么一致性问题的解决,其实不仅仅包含着CAP和BASE等基础理论,还需要考虑从以数据为中心的一致性模型和以用户为中心的一致性模型。以数据为中心的,还包含了严格一致性、顺序一致性等。以用户为中心的包含了单调读,单调写等。
c. 硬套逻辑
在Linux中在用户空间可以通过nice命令设置进程的静态优先级,这在内部会调用nice系统调用。进程nice值在-20和+19之间。这是一个被历史淹没的诡异的范围。那么根据这个,我就可以说,当时作者的年龄是20,她女朋友年龄19,所以有了这么一个诡异的范围。当然我肯定是在胡扯,所以,没有逻辑的时候就不要硬去套一个纵向逻辑了。
(3) 横向逻辑
总分关系比较好理解,在源码阅读来说其实就是在区分模块(分类)来进行阅读
ApplicationContext
通过继承如图的接口来扩展自身的功能,那么就可以把这些接口根据一定规则进行分模块,分别来进行阅读,降低整体的阅读复杂度。
(4) 金字塔逻辑
把横向逻辑和纵向逻辑形成金字塔结构的话,如下图,我相信至少已经达到了分析阅读的层次了。
金字塔逻辑
三、如何解决在阅读过程中的疑惑
在阅读过程中肯定会有疑惑的地方,在这里先说一个小故事,因明是古印度的逻辑学,提倡因明立量,最常见的三种量, 现量: 用事实证明; 比量: 用逻辑推论; 圣言量: 圣人所说。当年玄奘西行求法就曾深入学习过《因明》。因为那时候辨经几乎都是赌命的。输了可能要跳火山口的。在源码阅读的过程中,也可以理解为是源码作者在透过源码,在跟我们对话。那么遇到不理解的地方,或者有疑惑的地方,也可以通过现量的方式,比如某个方法的调用逻辑和实现的功能通过方法名称无法理解。那么就可以直接在往源码下层去阅读,甚至直接读到JDK里面的类。比量的话,其实就可以利用前面所讲到过的两个方向的逻辑思维。
当然如果通过现量和比量还是无法理解的话,建议采取逆向思维,想一想,如果不这么设计和实现,会怎么样^_^
。
还有可能有些实现细节一直在困扰着你,这时,除了求人,求百度Google以外。建议可以先放下,先把整体框架理解了再说。把不理解的地方记录下来。就算现在不理解,没人能解答,可能过几年经验丰富了,就已经有了答案了。人生的所有问题,总会有一个答案的。
四、阅读的Spring源码的切入点
经过上面的巴拉巴拉的一大堆,其实就已经隐含了阅读源码的切入点ApplicationContext
了。需要先通过横向逻辑研究对外提供的功能和内部实现进行分类,然后在每一个小模块把实现原理研究清楚,并且如果模块功能还是很繁杂,依然可以再次进行分类,直到理解为止。在每个模块内部在通过纵向逻辑,把执行的流程理清。在这个过程无论是通过画图也好,还是何种方式,只要能方便记忆和理解的都是好猫。
五、结束语
后续打算通过前面说的逻辑思维来整理Spring Frameworks的部分源码。帮助读者达到分析阅读的层次(其实是在帮我自己,虚荣一把)。
下篇可能会先说Resource的相关实现内容,也有可能去看看K8s的Schedule。平时工作略忙,不定期更新,尽量至少一周一篇。
路还很长,要想达到确保系统在整体上能够从任何未曾预料到的重创中恢复过来,还有很长的路要走。
参考
https://spring.io/
https://github.com/spring-projects/spring-framework
《机器学习实战》
《技术的本质》
《Spring揭秘》
《Spring深度源码解析》
《如何阅读一本书》
《行者玄奘》
《代码整洁之道》
《精准表达》
《软件架构》
《架构宝典》
《恰如其分的软件架构》
《The Mamba Mentality: How I Play》
《发布!设计与部署稳定的分布式系统》
《分布式系统原理与泛型 第二版》
《持续演进的Cloud Native:云原生架构下微服务最佳实践》
《大规模分布式存储系统:原理解析与架构实战》
《深入Linux内核架构》
都是好书, kindle商店买一波