阅读总结
1. 前期准备
遇到心仪的开源项目时,切忌仓促上手阅读源码,一定要先做好前期的准备工作。如果前期准备工作全面,会使得源码阅读的过程事半功倍。
1.1 工具准备
“工欲善其事,必先利其器。”源码阅读必须有一个强大且顺手的开发工具的支持。通常来说,该开发工具必须支持以下几项功能。
- 代码的高亮显示功能;
- 跨文件的全局搜索功能;
- 引用跳转、变量定义跳转、子类/父类跳转、子方法/父方法跳转等各类代码间的跳转功能;
- 单步调试功能及调试时的变量内容展示功能。
另外,还有一些功能是非必要的,如类间关系的自动分析功能、UML图的自动生成功能等。
1.2 项目选择
源码阅读是一项艰苦的工作,因此必须通过这项工作获得可观的收益才有意义。所以,一定要选择合适的项目进行源码阅读。选择项目时,通常从以下几个方面考虑。
- 项目的成熟度:我们要通过阅读源码来学习项目中的优点,帮助我们进步。因此,我们阅读的项目起到了教科书的作用,这就要求我们选择的项目一定要成熟、稳定。一般可以通过开源项目的年限、关注度、提交次数、团队规模、业界口碑等来综合考量。
- 项目的涉及面:每个项目都涉及特定的领域,如 MyBatis涉及的领域主要有数据库操作、对象关系映射、事务管理、代理实现等。阅读 MyBatis源码之后,我们对上述各领域的理解也会变得更深。因此,选择的源码阅读项目应该尽量多地涉及自己感兴趣的领域。
- 项目的应用广度:阅读一个项目的源码,除了能学到其中的编码技巧、架构方式等外,还会对整个项目的使用、原理等方面有详尽的了解。选择一个应用范围广的项目能让我们的这些知识有着更为实际的应用,这样能做到“一石二鸟”。
- 项目的规模:无论一个开源项目有多好,如果不能读完也不会有太大的收获。因此,一定要量力而为,选择自己能够驾驭的规模,切忌眼高手低、半途而废。
阅读一个项目的源码会耗费许多的时间、精力,因此一定要综合考虑以上各点选择合适的项目。为了使大家少走弯路,我们在第26章推荐了一些优秀的开源项目。
1.3 项目使用
记住一点:通过功能猜测源码要比通过源码猜测功能简单得多。这是作者阅读了大量源码后总结出来的规律。使用一个项目比开发一个项目简单,这意味着一个功能点可能需要众多抽象代码的支持。如果我们通过源码来猜测功能,则需要对这些抽象代码的结构、原理、调用链路等进行梳理和总结,这通常十分困难。而如果我们事先知道了功能点,则可以大体猜测出其实现原理,这时再去阅读项目的源码则会容易很多。所以说,在阅读一个项目的源码前,一定要先学会甚至是熟练使用该项目。这能帮助我们直观地建立起整个项目的外在轮廓。这样,在阅读源码时就能根据轮廓揣测源码的结构,起到事半功倍的效果。为了便于项目的源码阅读,可以在使用项目时做以下两方面的工作。
- 分清项目的核心功能、次要功能,了解各功能之间的依赖关系。找出核心功能会让我们在后续的源码阅读过程中有的放矢。
- 猜测核心功能的实现原理。可以试想如果是自己开发该功能会怎么实现。想不出来也没关系,这会成为疑问埋在我们的脑海中,然后会在我们读懂相关源码时给我们带来豁然开朗的感觉。
2. 项目初探
项目初探就是使用开发工具的调试功能追踪项目的运行过程。这个过程能让我们对项目的源码结构建立一个概括与模糊的认识。
在这个过程中要紧追核心功能,抓大放小。项目初探的目的是对项目建立概括与模糊的认识,而不是了解项目的全貌。因此,在整个过程中要抓大放小,只关注核心功能的实现流程即可,千万不要囿于项目的实现细节。
这个过程是基于代码的执行流程追踪代码而不是阅读代码,因此一定要善于利用开发工具提供的各种调试功能,如单步调试、断点调试、变量追踪等。
在追踪代码的运行流程时,可以将流程大体记录下来,作为后续源码阅读的框架;也可以将遇到的问题记录下来,待到源码阅读时再分析解决。
3. 源码阅读
各项前期准备工作结束后,就可以正式开始源码阅读工作。这是一项艰苦且持久的工作,但也有一些技巧可以遵循。这一节我们将总结和介绍这些技巧。
3.1 模块分析
一个成熟的项目是由多个模块组成的。因编程语言不同,模块可能略有差异,可能是包、组件、文件夹等。在阅读源码时不要一上手就深入细节,而是要先分析各个模块的功能。通常一个模块的功能是容易分析出来的,可能是通过模块的名称,也可能是通过调用关系。在项目初探环节进行的代码追踪也会给我们提供一些信息。在这一环节,我们要将各个模块的功能分析和总结出来。
3.2 模块归类
在了解了各个模块的功能之后,可以将模块进行归类。例如,在 MyBatis的源码阅读中,我们将模块分为基础功能包、配置解析包、核心操作包三类,然后对每一类单独进行源码阅读。通常,模块的归类可以做得尽可能细,而且结构不一定是扁平的,还可以是树状的、网状的,只要便于自己理解即可。
3.3 自底向上
在源码阅读时,自底向上是一个非常重要的准则。在软件测试领域,有两个概念:桩模块和驱动模块。存在依赖关系的两个模块,被依赖的模块为桩模块,依赖对方的模块为驱动模块。例如,在如下图所示的依赖关系中(箭头从调用方指向被调用方),B模块和 E模块是 A模块的桩模块,A模块是 B模块和 E模块的驱动模块。
桩模块中不涉及驱动模块的信息,而驱动模块中会涉及桩模块的信息。先阅读桩模块再阅读驱动模块,能保证我们在阅读代码时较少被未知代码干扰,这就是自底向上阅读源码的原因。当然,这也不是绝对的。如果驱动模块比较简单,而桩模块众多且比较复杂,则先阅读驱动模块会帮助我们厘清桩模块间的脉络关系。这种情况下,可以采用自顶向下的源码阅读流程。
3.4 合理猜测
相比于软件的功能,软件的源码是抽象的。源码阅读的过程就是将抽象的源码逐渐梳理清晰的过程。在这个过程中,合理的猜测是十分必要的。例如,在项目使用阶段,我们可以猜测功能的实现方式;在项目初探阶段,我们可以在代码流程追踪的过程中猜测模块之间的调用关系。哪怕我们做出的猜测不完善甚至是错误的,这些猜测也会为我们的源码阅读工作指引一些方向。面对复杂的功能,我们先猜测出了一个简化的不完善的实现原理,然后以此为指引展开源码阅读。在源码阅读的过程中,我们会不断地修正和完善我们的猜测,最终整个功能的框架也便清晰了。
3.5 类比阅读
项目源码中总会出现很多分支,大到功能分支,小到代码分支。不同分支的实现思路可能是一致的,因此全部阅读每个分支的代码是没有必要的。这时我们只需选取一些具有代表性的分支深入阅读,然后类比到其他分支上即可。在选择要阅读的分支时,有两个思路:
- 选择最为复杂的分支。当我们将最为复杂的分支的源码阅读完成时,其他简单分支的源码自然就清晰了。
- 选择最为简单的分支。我们可以先读懂最简单分支的源码,然后在此基础上,进一步去阅读更为复杂的分支。
具体遵循哪个思路进行分支的选择要根据具体情况来分析。通常来说,对于重要的代码选择复杂的分支;对于次要的代码选择简单的分支;对于简单的代码选择复杂的分支;对于复杂的代码选择简单的分支。阅读完所选择分支的源码后,直接类比到其他分支上,便可以较快地完成所有分支的源码阅读。
3.6 善于汇总
源码阅读过程中,我们可以借助一些工具让抽象的源码具象起来。这些工具有类图、伪代码、时序图、结构图等。在 MyBatis的源码阅读中,我们大量地使用了这些工具。这些工具能让我们从混乱的源码中抽离出来,以更高的视角来观察源码的关系、功能的依赖及跳转的逻辑。面对数量众多的类、方法时也要注意汇总。往往越是纷杂的类和方法,越有规律可以分类。例如,这些类可能有着共同的父类或者继承了共同的接口;这些方法可能是围绕某个核心方法重载的等。
3.7 网格阅读
在源码阅读时,我们往往以模块(包、组件、文件夹等)为单位各个击破。但是总有一些功能会横跨多个模块,例如,MyBatis中的缓存功能就主要横跨了 cache包和 executor包。这就需要我们在完成模块的源码阅读之后,再从功能的角度对源码进行串联和汇总。在源码阅读过程中,我们通常以模块为单位展开。然后会越读越细,逐渐深入到子模块、类、子类、属性和方法、语句。整个过程是一个纵向深入的过程。而从功能的角度进行串联和汇总是一个跨模块的横向扩展过程。于是,纵横交错,便组成了一个网格,如下图所示。
纵向的模块维度保证我们对源码结构的理解是透彻的,横向的功能维度保证我们对项目功能的理解是连贯的。最终,项目源码会按照图25-2所示的网格形式被我们细化和吸收。
优秀开源项目推荐
进行源码阅读时,选择优良的开源项目是十分重要的。如果开源项目选择不当,则可能会在花费大量的时间、精力阅读其源码后收获甚微。
1. Guava
Guava是一个开源的 Java工具库,它提供了许多严谨且易于使用的工具和类。这些工具和类涉及字符串、集合、IO、并发、缓存、反射、数学计算等诸多方面。使用 Guava能够提升我们代码编写的效率和质量。因为 Guava出色的易用性,其中的许多特性都被新版本的 Java采用。
例如,数据中键有重复,因此需要用 Map<String,List<Integer>>的形式进行存储。使用原生 Map操作的源码,在存储每个数据时我们分两种情况进行了处理:
- 如果对应的键还未存储过,则应该为该键新建一个列表并将数据加入该列表。
- 如果对应的键已经存储过,则应该取出该键对应的列表并将数据加入该列表。
而 Guava提供的 Multimap支持重复的键,能够简化上述操作并且支持更多的功能。使用 Multimap完成上述操作,其逻辑上更为清晰。
Guava 中提供的实用工具和实用类还有许多,相关代码都十分可靠和高效,值得我们使用和阅读借鉴。Guava的开源地址为 https://github.com/google/guava。目前该项目的 Star数目已经超过3.4万。阅读 Guava的开源代码能极大地增强我们对 Java基本类的认知,提升我们对基础工具的开发能力。因为 Guava提供多个方面(字符串、集合、IO、并发、缓存、反射、数学计算等)的工具和类,不同方面的工具和类之间耦合度较低,所以,阅读 Guava代码时可以方便地分模块展开,这就降低了源码阅读的难度。
2. Tomcat
Tomcat是一个免费的、开源的轻量级 Web应用服务器,应用极为广泛。通过 Tomcat,可以部署和发布 JSP 页面和 Servlet。我们在前面章节提及的 Spring 框架便可以部署到Tomcat上运行。Tomcat的开源地址为 https://github.com/apache/tomcat。目前该项目的 Star数目超过 3千,且该项目已经被 1.2万以上的项目引用过。阅读 Tomcat的源码能让我们在网络协议、文件读写、并发处理等方面有较大的提升。
3. Redis
Redis是一个开源的高性能内存数据库,基于它,我们可以实现分布式锁、共享缓存、消息系统等诸多功能,是一种非常重要的 NoSQL数据库。Redis是一个 key-value存储系统,但它支持的 value类型很多,包括字符串、链表、集合、有序集合和哈希类型,并且在 Redis中的数据操作都是原子性的。为了保证效率,Redis中的数据缓存在内存中,并且支持将内存中的数据持久化到磁盘中。同时,Redis还支持通过主从方式建立集群。Redis的开源地址为 https://github.com/antirez/redis。目前该项目的 Star数目超过 3.9万。阅读Redis的源码能让我们对链表和映射表等数据结构的高效使用、内存管理、事务、分布式主从协作与集群管理等方面建立更深刻的认知。
26.4 Dubbo
Dubbo是一个开源的高性能服务框架,基于它可以实现应用间的远程过程调用(Remote ProcedureCall,RPC)。目前,使用单体大应用提供服务的方式已经比较少见,取而代之的是使用微服务网络来提供服务。在微服务网络中,存在众多的服务节点,因此服务之间的发现和调用成了必须解决的问题。Dubbo就是为解决这个问题而诞生的。当然,Dubbo 除了具有服务发现和调用功能外,还具有一些其他功能,如服务降级、权限管理、服务监控等。下图所示是 Dubbo官方网站提供的功能简图。
Dubbo的开源地址为 https://github.com/apache/dubbo。目前该项目的 Star数目超过 2.9万,并且该项目被超过 1600个项目引用过。阅读 Dubbo的源码能让我们对分布式系统的协作原理、序列化与反序列化、代理、网络协议等方面建立更深刻的认知。
26.5 React
React是一个声明式的高效且灵活的用于构建用户界面的开源 JavaScript库。基于 React可以创建保存有自身状态的组件,并在组件之间方便地进行数据传递,最终基于这些组件创造出交互式的用户界面。React借助虚拟 DOM(Virtual DOM)来完成页面元素的高效渲染。虚拟 DOM本身是JavaScript中的一个数据结构。当组件更新时,React会将变动映射到虚拟 DOM上,并通过 diff算法找寻具体要变化的虚拟 DOM节点。最后,再把这个变动更新到浏览器实际的DOM节点上。这样的操作极大地提升了 React的渲染效率。React开源地址为 https://github.com/facebook/react。目前该项目的 Star数目超过 13.8万,并且被超过 250万个项目引用过。阅读 React的源码能让我们在组件设计、虚拟 DOM、比较算法、浏览器工作机制等方面学到很多有用的知识。