首页 » 软件开发 » 如何阅读源代码(建议收藏)(源码函数学习设计项目)「源码阅读工具」

如何阅读源代码(建议收藏)(源码函数学习设计项目)「源码阅读工具」

南宫静远 2024-07-23 22:14:44 软件开发 0

扫一扫用手机浏览

文章目录 [+]

主要表现为:

读源码的时候并不知道该读啥很容易迷失在细节中,调试时跳来跳去跳晕了,很难坚持下去读完很快就忘掉了,无法灵活运用

网上也会有一些讲某个具体开源代码的系列文章,通常比较冗长,传授的都是“鱼”而不是“渔”。
俗话说:授人以鱼不如授人以渔。

我相信大多数同学希望得到方法论级别、更加系统化介绍如何更好地阅读源码的文章。
为此,在这里我打算将自己的读源码经验传授给大家,相信会让很多人理解问题的症结所在,给出一些“意料之外”的实用建议,让饱受读源码困惑的同学能够找到方向。

如何阅读源代码(建议收藏)(源码函数学习设计项目) 如何阅读源代码(建议收藏)(源码函数学习设计项目) 软件开发
(图片来自网络侵删)

文章重点讲到如下内容:

为什么很多人读源码收获不大?读源码究竟读什么?有哪些读源码重要的思想?有哪些好的读源码切入点?有哪些读源码非常实用的技巧?

整体概览:

如何阅读源代码(建议收藏)(源码函数学习设计项目) 如何阅读源代码(建议收藏)(源码函数学习设计项目) 软件开发
(图片来自网络侵删)

为什么很多人读源码收获不大?

在我看来,大多数人读源码收获不大的主要原因如下:

缺乏整体思维,迷失在细节中(如调试源码时跳来跳去,最后跳晕了)缺乏思考(学而不思则罔,思而不学则殆!
)不知道读源码究竟读什么(如源码的设计思想)角度单一(如从解决问题角度、性能优化角度、设计模式角度、每次提交、单元测试、注释等)方法单一(如不懂的高级的调试技巧,不懂的时序图插件)缺乏输出(不会输出成文章,不能讲给别人听)读源码究竟读什么?

做事要“以终为始”,只有搞清楚读源码我们究竟想得到什么,我们才能避免“走马观花” 最终将收获无多的尴尬场景。

那么读源码读的是什么?我们要关注哪些方面呢?

读目的:该框架是为了解决什么问题?比同类框架相比的优劣是什么?这对理解框架非常重要。

读注释:很多人读源码会忽略注释。
建议大家读源码时一定要重视注释。
因为优秀的开源项目,通常某个类、某个函数的目的、核心逻辑、核心参数的解释,异常的发生场景等都会写到注释中,这对我们学习源码,分析问题有极大的帮助。

读逻辑:这里所谓的逻辑是指语句或者子函数的顺序问题。
我们要重视作者编码的顺序,了解为什么先写 A 再写 B,背后的原因是什么。

读思想:所谓思想是指源码背后体现出了哪些设计原则,比如是不是和设计模式的六大原则相符?是不是符合高内聚低耦合?是不是体现某种性能优化思想?

读原理:读核心实现步骤,而不是记忆每行代码。
核心原理和步骤最重要。

读编码风格:一般来说优秀的源码的代码风格都比较优雅。
我们可以通过源码来学习编码规范。

读编程技巧:作者是否采用了某种设计模式,某种编程技巧实现了意料之外的效果。

读设计方案:读源码不仅包含具体的代码,更重要的是设计方案。
比如我们下载一个秒杀系统 / 商城系统的代码,我们可以学习密码加密的方案,学习分布式事务处理的方案,学习幂等的设计方案,超卖问题的解决方案等。
因为掌握这些方案之后对提升我们自己的工作经验非常有帮助,我们工作中做技术方案时可以参考这些优秀项目的方案。

读源码的误区

很多人读源码不顺利,效果不好,通常都会有些共性。

那么读源码通常会有哪些误区呢?

开局打 Boss

经常打游戏的朋友都知道,开局直接打 Boss 无异于送人头。

一般开局先打野,练就了经验再去挑战 Boss。

如果开始尝试学习源码就直接拿大型开源框架入手容易自信心受挫,导致放弃。

佛系青年

经常打游戏的朋友也都知道,打游戏要讲究策略,随便瞎打很容易失败。

有些朋友决定读源码,但又缺乏规划,随心所欲,往往效果不太好。

对着答案做题

我们知道很多小学生、初高中生,甚至很多大学生学习会出现眼高手低的情况。

有些人做题时并不是先思考,而是先看答案,然后对着答案的思路来理解题目。
在这种模式下,大多数题目都理所当然地这么做,会误认为自己真正懂了。
但是即使是原题,也会做错,想不出思路。

同样地,很多人读源码也会走到这个误区中。
直接看源码的解析,直接看源码的写法,缺乏关键的前置步骤,即先自己思考再对照源码。

读源码的思想先会用再读源码

学习某个源码之前一定要对源码的基本用法有一个初步了解。

如果对框架没有基本的了解就直接读源码,效果通常不会太好。

一般优秀的开源项目,都会给出一些简单的官方示例代码,大家可以将官方示例代码跑起来,了解基本用法。

大家也可以去 GitHub 上搜索并拉取某个技术的 Demo,某个技术的 hello world 项目,快速用起来。

如 Dubbo 官方文档就给出了快速上手示例代码 ;轻量级的分布式服务框架 jupiter README.md 就给出了简单的调用示例。
一些开源项目给出了多个框架的示例代码,如 tutorials。

先易后难

循序渐进是学习的一大规律。

一方面,可以先尝试阅读较为简单的开源项目源码,比如 commons-lang、commons-collection、guava、mapstruct 等工具性质的源码。

另外还可以尝试寻找某个框架的简单版,先从简单版学起,看透了再学大型的开源项目就容易很多。

可能很多人会说不好找,其实大多数知名开源的项目都会有简单版,用心找大多数都可以找到, 比如 Spring 的简易版、Dubbo 简易版。

先整体后局部

先整体后局部是非常重要的一个认知规则,体现了“整体思维”。

如果对框架缺乏整体认识,很容易陷入局部细节之中。

先整体后局部包括多种含义,下面会介绍几种核心的含义。

先看架构再读源码

大家可以通过框架的官方文档了解其整体架构,了解其核心原理,然后再去看具体的源代码。

但是很多人总会忽视这个步骤。

如轻量级分布式服务框架 jupiter 框架 的 README.md 给出了框架的整体架构:

(图片来自:jupiter 项目 README.md 文档)

对框架有了一个整体了解之后,再去看具体的实现就会容易很多。

先看项目结构再读源码

先整体后局部,还包括先看项目的分包,再具体看源码。

(图片来自:jupiter 项目结构)

通过项目的报名,如 monitor、registry、serialization、example、common 等就可以明白该包下的代码意图。

先看类的函数列表再读源码

通过 IDEA 的函数列表功能,可以快速了解某个类包含的函数,可以对这个类的核心功能有一个初步的认识。

这种方式在读某些源码时效果非常棒。

更重要的是,如果能够养成查看函数列表的习惯,可以发现很多重要但是被忽略的函数,在未来的项目开发中很可能会用到。

下图为 commons-lang3 的 3.9 版本中 StringUtils 类的函数列表示意图:

先看整体逻辑再看某个步骤

比如一个大函数可能分为多个步骤,我们先要理解某个步骤的意图,了解为什么先执行子函数 1, 再执行子函数 2 等。

然后再去观察某个子函数的细节。

以 spring-context 的 5.1.0.RELEASE 版本的 IOC 容器的核心 org.springframework.context.support.AbstractApplicationContext 的核心函数 refresh 为例:

@Overridepublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. // 1 初始化前的预处理 prepareRefresh(); // Tell the subclass to refresh the internal bean factory. // 2 告诉子类去 refresh 内部的 bean Factory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. // 3 BeanFactory 的预处理配置 prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. // 4 准备 BeanFactory 完成后进行后置处理 postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. // 5 执行 BeanFactory 创建后的后置处理器 invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. // 6 注册 Bean 的后置处理器 registerBeanPostProcessors(beanFactory); // Initialize message source for this context. // 7 初始化 MessageSource initMessageSource(); // Initialize event multicaster for this context. // 8 初始化事件派发器 initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. // 9 子类的多态 onRefresh onRefresh(); // Check for listener beans and register them. // 10 检查监听器并注册 registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. // 11 实例化所有剩下的单例 Bean (非

比如再去了解第 7 步的具体编码实现。

/ Initialize the MessageSource. Use parent's if none defined in this context. /protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); // Make MessageSource aware of parent MessageSource. if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; if (hms.getParentMessageSource() == null) { // Only set parent context as parent MessageSource if no parent MessageSource // registered already. hms.setParentMessageSource(getInternalParentMessageSource()); } } if (logger.isTraceEnabled()) { logger.trace("Using MessageSource [" + this.messageSource + "]"); } } else { // Use empty MessageSource to be able to accept getMessage calls. DelegatingMessageSource dms = new DelegatingMessageSource(); dms.setParentMessageSource(getInternalParentMessageSource()); this.messageSource = dms; beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); if (logger.isTraceEnabled()) { logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]"); } }}

从该子函数的角度,“整体”为 if 和 else 两个代码块,“部分”为 if 和 else 的代码块的具体步骤。

从设计者的角度学源码

从设计者的角度读源码是一条极其重要的思想。
体现了“先猜想后验证”的思想。

这样就可以走出“对着答案做题”的误区。

学习源码时不管是框架的整体架构、某个具体的类还是某个函数都要设想如果自己是作者,该怎么设计框架、如何编写某个类、某个函数的代码。

然后再和最终的源码进行对比,发现自己的设想和对方的差异,这样对源码的印象更加深刻,对作者的意图领会的会更加到位。

比如我们封装 HTTP 请求工具,获取响应后根据响应码判断是否成功,我们可能会这么写:

public boolean isSuccessful(Integer code) { return 200 == code;}

我们查看 okhttp 4.3.0 版本的源码,依赖:

<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp --><dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.3.0</version></dependency>

okhttp3.Response 类的 isSuccessful 函数源码注释和代码 (kotlin):

/ Returns true if the code is in [200..300), which means the request was successfully received, understood, and accepted. / val isSuccessful: Boolean get() = code in 200..299

发现和自己设想的不同,响应码的范围是 [200..300)。

通过这个简单的例子,我们发现自己对 HTTP 响应码的理解不够全面。

另外通过这个源码我们也了解到了源码注释的重要性,通过源码注释可以清楚明白的理解该函数的意图。

从设计模式的角度学源码

很多优秀的开源项目都会用到各种设计模式,尤其是学习 Spring 源码。

因此,强烈建议要了解常见的设计模式。

了解常见设计模式的目的、核心场景、优势和劣势等。

要理解设计模式的六大原则:单一职责原则、开闭原则、依赖倒置原则、接口隔离原则、迪米特法则等。

在读源码时注意体会设计模式的六大原则在源码中的体现。

如 jupiter 1.3.1 版本的 org.jupiter.serialization.SerializerFactory 类就体现了工厂模式。
该类通过在静态代码块中使用 SPI 机制加载序列化方式并存储到 serializers map 中,获取时从该 map 中直接取,实现了已有对象的重用。

大家可以通过《设计模式之禅》、《Java 设计模式及实践》、《Head first 设计模式》等来学习设计模式。

从设计模式角度阅读源码,可以加深对设计模式应用场景的理解,自己编码时更容易选择适合的设计模式来应对项目中的变化。

读源码的粒度问题

很多开源项目代码行数非常多,几十万甚至上百万行,想都读完并且都能记下来不太现实。

前面也讲到读源码读什么的问题,个人建议大家读核心的原理,关键特性的实现,高抽象层的几个关键步骤。

不要追求读每一行代码,甚至“背诵”代码,因为工作之后学习的目的更多地是为了运用,而不是为了考试。

读源码的技巧通过注释学习源码

我们以 Guava 源码 commit id 为 5a8f19bd3556 的提交版的 CacheBuilder 源码为例。

如果我们想了解 expireAfterWrite 函数的的用法。

可以通过读其注释了解该函数的功能,每个参数的含义,异常发生的原因等。
对我们学习源码和实际工作中的使用帮助极大。

/ Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry's creation, or the most recent replacement of its value. // 省略其他 @param duration the length of time after an entry is created that it should be automatically removed @param unit the unit that {@code duration} is expressed in @return this {@code CacheBuilder} instance (for chaining) @throws IllegalArgumentException if {@code duration} is negative @throws IllegalStateException if the time to live or time to idle was already set / @SuppressWarnings("GoodTime") // should accept a java.time.Duration public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) { checkState( expireAfterWriteNanos == UNSET_INT, "expireAfterWrite was already set to %s ns", expireAfterWriteNanos); checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); this.expireAfterWriteNanos = unit.toNanos(duration); return this; }通过单元测试学源码

同样以学习 6.1 的函数为例,可以通过 find usages 找到对应的单元测试。

com.google.common.cache.CacheExpirationTest#testExpiration_expireAfterWrite

可以执行在源码中断点,然后执行单元测试,了解源码细节。

public void testExpiration_expireAfterWrite() { FakeTicker ticker = new FakeTicker(); CountingRemovalListener<String, Integer> removalListener = countingRemovalListener(); WatchedCreatorLoader loader = new WatchedCreatorLoader(); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder() .expireAfterWrite(EXPIRING_TIME, MILLISECONDS) .removalListener(removalListener) .ticker(ticker) .build(loader); checkExpiration(cache, loader, ticker, removalListener);}从入口开始学源码

如下面是常见的 springboot 的应用启动主函数:

@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}

我们可以从 SpringApplication 的 run 函数一直跟下去。

有些朋友可能会说,跟着跟丢了怎么办?

大家可以在源码中打断点,然后通过左下角的调用栈实现源码的跳转,可以通过“drop frame”实现。

利用插件来学源码类图插件

可以使用 IDEA 自带的类图了解核心类的源码的关系。

如下图为 fastjson 的核心类的类图:

时序图插件

可以使用 Stack trace to UML IDEA 插件绘制错误堆栈的时序图,了解源码的执行流程。

推荐大家安装 SequenceDiagram IDEA 插件,读源码时可以查看调用的时序图,对理解源码调用关系帮助很大。

codota

强烈推荐大家安装 codota 插件(支持 Eclipse、IDEA、Android Studio) 通过该插件或对应的 Java 代码搜索网站

如下图所示,我们安装好 codota 插件后,想了解 org.springframework.beans.factory.support.BeanDefinitionRegistry 的 registerBeanDefinition 函数用法。

直接在该函数上右键然后选择“Get relevant examples”,即可查看其他知名开源项目中的相关用法。

这对我们了解该源码的功能和用法有极大的帮助,我们实际开发中也可以多用 codota 来快速学习如何使用一个函数。

通过提交记录学源码

比如我们想研究某段源码的变动,可以拉取源代码,查看 Git 提交记录。

比如我们想研究某个感兴趣类的演进,直接选取该类,查看提交记录即可。

下图为 commons-lang 项目的,StringUtils 工具类的一个变更记录:

通过变更记录我们可以学习到早期版本有哪些问题,如何进行优化。

根据 issue 学源码

issues 是学习源码的重要途径,是我们提高开发经验的一个重要途径。

如果我们想深入学习某个开源项目,可以翻阅历史 issues 。

针对具体的 issue 中涉及的具体的问题入手了解大家对该问题的看法,学习问题的原因和解决办法。

着重了解有多种方案时作者进行了何种考量,做出了什么取舍。

如 Add ImmutableArray.reverse() #3965:

搜索引擎大法

当我们对某些源码设计感到困惑时,可以在 Google 或者 Stack Overflow 上搜索问题的原因,往往会有些意外收获。

反编译大法

我们在读源码时经常会遇到类似下面的这种写法:

org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer

private WebServer startWebServer() { WebServer webServer = this.webServer; if (webServer != null) { webServer.start(); } return webServer; }

在函数中声明一个和成员变量同名的局部变量,然后将成员变量赋值给局部变量,再去使用。

看似很小的细节,隐含着一个优化思想。
这就需要借助反编译大法,在字节码层面去分析。

详细解读参见《为什么要推荐大家学习字节码?》。

总结

总之,读源码要着重思考,思考为什么这么设计?可能的原因是什么?然后去验证。

学习代码在平时,工作时如果项目开发工期不紧,编码过程中进入源码分析学习,积少成多;在开发过程中,如果遇到问题,可以选择进入源码调试,这样印象更深刻;此外,我们既要埋头苦干也要“仰望星空”(巩固专业基础),有些核心的软件设计原则,操作系统、计算机网络的设计原理,都是源码设计思想的重要来源,如果专业基础不扎实,往往很难了解问题的本质。
标签:

相关文章