框架源码阅读的方法与技巧

代码是形式,逻辑是神韵。


引子

“解锁优秀源代码的基本方法与技巧” 一文中,探讨了阅读优秀源码的基本步骤、方法、技巧、所面临的障碍及克服之策。多加训练,应该可以达成如下目标:

  • 能够读懂独立类和基本容器的实现;
  • 能够读懂小型的基础库和框架;
  • 通过源码阅读来调试和解决实际中的问题。

本文进一步探索如何阅读成熟框架的源码。

温馨提示

欲速则不达。阅读源码很容易理解为就是直接去阅读代码本身。实际上,代码只是形式,逻辑才是神韵。

凡是有助于去理解逻辑,理解其原理、架构、实现的,都是值得阅读的。包括而不限于官方文档和 API 文档、架构设计分析文章、原理分析文章、源码阅读分析文章。磨刀不误砍柴工。准备工作做充足,充分借助各种资源辅助,阅读源码才能事半功倍。


预思考

有需求才有目标,有目标才有设计,有设计才有框架。在阅读某个源码模块之前,思考若干基本问题是必要的。

  • 需求是什么?用一句话说清楚;
  • 设计目标是什么?用一句话说清楚;
  • 核心优势和适用场景是什么?分别用一句话说清楚;
  • 基本原理是怎样的?先自己思考怎么实现,然后阅读框架原理文章;
  • 整体设计是怎样的? 先自己思考怎么设计,然后阅读架构设计的文章;
  • 技术难点是什么?先自己思考其中的难点及解决方案,然后阅读相关文章;
  • 数据结构及算法流程是如何设计的?阅读框架的源码解析文章。

比如 SpringBean 模块:

  • 需求:有一套通用机制去创建和装配应用所需要的完整的 Bean 实例,使得应用无需关注 Bean 实例的创建和管理,只要按需获取;
  • 设计目标:根据指定的配置文件或注解,生成和存储应用所需要的装配完整的 Bean 实例,并提供多种方式来获取 Bean 实例;
  • 核心优势:支持多种装配方式、自动装配、依赖关系自动注入;支持不同作用域的 Bean 实例创建和获取;稳定高效;
  • 适用场景:有大量的 Bean 需要创建,这些 Bean 存在复杂的依赖关系;
  • 基本原理:反射机制 + 缓存;
  • 算法流程:创建 bean 工厂对象 -> 扫描资源路径,获得 bean 的 class 文件 -> 生成 bean 定义的 beanDefinition 实例 -> 根据 beanDefinitioin 实例创建 bean 实例并缓存到 bean 工厂对象 -> 依赖自动注入 -> 执行钩子方法 -> 完整的 bean 实例准备就绪。
  • 技术难点:依赖自动装配、循环引用;解决自动依赖注入和循环引用问题需要用到缓存机制。

需求与目标

需求与目标往往容易混为一谈。但需求不等于目标。

  • 需求是宽泛的,目标是具体的;
  • 目标是需求的一种实现途径,往往是设计一个具备某些关键特性的系统或产品。

目标是功能与质量的结合体;除了功能部分,确定质量指标也是尤为关键的。质量指标可参阅:“Web服务端软件的服务品质概要”

对于某个框架来说,需求、适用场景和核心优势,都是可以直接在官网或项目主页获取到的。如何还原框架的设计目标呢?可以从核心优势中获取基本说明,更多的就要从 API 文档里来提炼了。

方法

很多开发童鞋可能对阅读源码心生畏惧。其实读源码既不神秘也不复杂:写个 Demo,打断点,运行,然后细细揣摩。阅读源码就是观摩高手出招的过程。

  1. 确立目标,通常是理解某个模块的原理、设计或者为了解决实际问题;
  2. 写个 demo,能够将主流程运行起来;
  3. 找到框架运行的入口点,通过静态代码分析,大致了解整个实现流程;
  4. 在预估会经过的关键地方打断点,单步调试;
  5. 仔细查看主流程经过的主路径、每一个主要对象及其成员变量的值及变化,细细揣摩其设计意图和方法技巧;
  6. 绘制整体流程框图和类的交互图;
  7. 学习和理解关键类及关键方法及实现(代码)。

阅读源码,要把握主要与扩展:

  • 首先把主流程及涉及到的主要类弄透彻;
  • 理解其扩展机制;
  • 理解主要扩展实现(需要的时候徐图之)。

阅读源码,常常要将“静态代码分析”和“单步调试”结合起来使用。

静态代码分析

静态代码分析,就是沿着方法调用链,“顺藤摸瓜”一路点击下去。通常能够对整体流程有一个大概的了解。

由于框架实现常常基于接口编程,有时会遇到有多个实现的情形。这时,可以根据直觉和经验,选择一个最有可能的默认实现继续跟下去,或者通过单步调试来弄清楚是哪个具体实现。

单步调试

单步调试,是看似笨拙却很实用的源码阅读方法。单步调试在以下情形尤其有用:

  • 接口调用有多个实现,难以确定是哪个是具体实现时;
  • 查看某个比较复杂的具体类的成员时;
  • 理解实现细节时。

框架解析

框架的设计实现通常包括三层:

  • 问题域及解决方案构成的抽象层,解决问题的核心部分;
  • 封装和交互构成的设计层,确保灵活性、可扩展性和应用集成;
  • 各种细节构成的实现层,用于保证性能和容错等。

阅读顺序是:抽象层 -> 封装与交互层 -> 细节实现层 或者 抽象层 -> 细节实现层 -> 封装与交互层。抽象层好比匣中的宝珠,不能干买椟还珠的事情。

抽象层

抽象层即是问题求解层。技术面试中问到的原理或实现机制,通常都属于这一层。

由于封装和交互、实现细节的大量代码往往会将用于解决问题的核心代码“淹没”,因此,在探索抽象层时,要学会大胆过滤封装和细节,直接跳过大量的分支条件语句,暂时跳过令人疑惑的地方,始终聚焦和直击解决问题的核心部分。用于解决基本问题的核心代码通常是不多的。

比如,Bean 实例创建的核心代码是 ClassPathBeanDefinitionScanner.doScan(扫描资源路径,生成 beanDefinition 对象) 和AbstractAutowireCapableBeanFactory.doCreateBean 方法(根据 beanDefinition 创建 bean 实例)。

设计层

要弄明白设计层,就要先弄清楚框架的整体设计:

  • 有哪些子模块,子模块的设计意图是什么;
  • 子模块之间的关联是怎样的,如何串联成一个完整的设计意图。

框架的设计实现常常会用到设计模式。

  • 常用设计模式:工厂、单例、外观、策略、适配器、装饰、代理、模板、组合、观察者、迭代器;
  • 不同问题域可能会用到的设计模式,比如 DB 驱动接口实现会用到生成器模式和桥接模式,web 请求处理用到职责链模式。

常用设计模式的使用场景:

  • 如果需要创建实例,则通常离不开工厂和单例模式;
  • 如果涉及较为复杂的算法流程,部分算法需要在子类实现,则会用到模板方法模式;
  • 如果需要多种实现,并依据特定场景来选取使用,则会用到策略模式;
  • 如果要将客户端接口及实现与框架的调用隔离,则会用到动态代理模式;
  • 如果要灵活叠加多种功能,则会用到装饰器模式;
  • 如果涉及到事件机制,则离不开观察者模式;
  • 如果需要在库实现的基础上提供简洁接口,则通常用到外观模式;
  • 如果要将多种实现与多种接口定义进行连接,则会用到桥接模式;
  • 如果需要涉及大量配置(规格)并生成实例,则通常用到生成器模式;
  • 如果涉及容器元素访问,则离不开迭代器模式;
  • 如果需要以统一接口访问整体与部分的行为,且整体由部分组成,则通常用到组合模式。

理解基本设计模式的特征和适用场景,识别设计模式的使用,可以更自如地在框架源码之间穿梭。

细节层

细节是最考验源码阅读的心性了。细节藏魔鬼。关键细节考虑不周全,可能会导致整个设计的失败。因此,细节层也是值得仔细推敲的。技术面试中也常常考察实现细节。如果能够回答上来,大概率会让面试官眼前一亮。

有时,一些实现细节可能让人摸不到头脑。此时,可以上网搜索一下,往往会“茅塞顿开”。

克服障碍

阅读成熟框架源码,遇到的一大挑战就是对象之间的错综复杂的交互关系。令人生畏。这实际上考验着开发者的抽象和建模能力。

原理流程图

原理流程图非常重要,就像地图一样,指引人更容易地在“代码迷宫”中穿行而不迷失方向。

在阅读源码之前,设法弄到并理解框架的原理流程图,往往能起到事半功倍的效果。就如行兵打仗,先弄清楚天时与地形。不可不重视之。

概念图景

优秀的软件设计,往往是先建立一个比较完整的概念图景。概念图景,就是关于某个问题域的概念及其关联关系的整体图。

譬如盖房子吧。有的人盖房子就是:砌砖!砌砖!!砌砖!!!要安装窗户怎么办?把其中一大块砖墙锤空了再安。

有的人,则会“设计先行”:

  • 原材料 => 子部件 => 组合与集成。
  • 原材料:砖、石、木、铝、铜、玻璃等;
  • 子部件:墙、窗框、窗户、门、地板、楼梯、锁、通道等;
  • 房子:由子部件进行组合和集成而成;
  • 机制:子部件的组合与集成的原理支撑,比如形状的组合与契合、承压计算等。

如何理清其中的复杂交互关系,从而理解其中蕴含的设计思想呢?需要先理清楚框架的概念图景。

有两种技巧可以结合使用:

  • 由于接口定义了具体类的行为规范,可以通过阅读接口定义及文档来了解其设计思路和骨架;
  • 查看具体类的实例成员(暂不涉及方法),根据经验揣摩其设计意图。

核心类成员

要深入到具体实现,则无法避免核心类的阅读。核心类往往拥有十几个甚至几十个成员及方法,展示出了十足的源码阅读劝退诚意。面对这种情况怎么办呢? 有三个技巧可以结合使用:

  • 按快捷键 Alt+7,可以查看该类的所有成员及方法,概览一下,大致猜测其意图;
  • 首先只关注那些对核心问题求解有重要影响的成员,暂时忽略那些用来提升性能、可扩展性等方面的成员;
  • 单步调试,仔细看看运行时的成员对象如何,这样会更加直观具体一些。比如 DefaultListableBeanFactory 这个类,单步调试后得到如下图示:

技术难点

技术难点也是理解源码实现的一个主要障碍。技术难点主要有三类:

  • 数据结构与算法:比如 HashMap 用到了哈希表和红黑树,需要先阅读文献(比如《算法导论》)理解其结构与算法;
  • 原理机制:比如 IO 读写、内存管理、文件系统、编译原理、网络协议,先学习相关的原理机制,夯实基础;
  • 编程模型:特别的编程手法和技巧,比如读 hystrix 源码,就要先熟悉函数式编程和响应式编程。

如何找到论述原理机制的相关文献呢?有一些基本方法可循:

  • 经典书籍:比如数据结构与算法,就有《算法导论》、《算法》、《计算机程序设计艺术》(排序与查找)等;
  • 经典论文:一些还没来得及写入书籍的论文解读,比如 Raft 算法等;
  • 技术书籍:比如 Linux 操作系统内核实现,TCP 协议详解等;
  • 官方文档:优秀项目主页的文档部分,往往有相关原理机制的介绍,比如 ES 的官方文档;
  • JavaDoc:优秀源码的 Java Doc 往往会引用相关出处,比如 AQS 的源码;
  • 优秀博文:优秀博文往往有一些文献引用,可以阅读相关文献引用;
  • 百科与搜索:在维基百科上搜索出处和引用来源;或者使用搜索引擎。

越到后面,就会发现,真正需要仔细阅读和钻研的书籍和论文,其实并不多。花费很多时间阅读网络文章,这些偷懒和捷径,反而是走了弯路。


耐心与意志

阅读框架源码需要很大的耐心和意志。有点像蚕宝宝吃桑叶,需要一点一点地啃。各个击破。在这个过程中,需要克服不少障碍,才能“修得正果”。

可以使用多种辅助手段:

  • 边听音乐边阅读代码;
  • 拉取代码分支,边读边做标记并提交;
  • 阅读原理、架构及源码分析文章。

小结

源码阅读技能,可以说是程序员的“内功心法”之一。若是能读通优秀源码,则应对日常编程工作游刃有余,而应对难题则有路可循。

路漫漫其修远兮,吾将上下而求索。

posted @ 2021-02-15 12:15  琴水玉  阅读(1006)  评论(0编辑  收藏  举报