)不知道读源码究竟读什么(如源码的设计思想)角度单一(如从解决问题角度、性能优化角度、设计模式角度、每次提交、单元测试、注释等)方法单一(如不懂的高级的调试技巧,不懂的时序图插件)缺乏输出(不会输出成文章,不能讲给别人听)读源码究竟读什么?做事要“以终为始”,只有搞清楚读源码我们究竟想得到什么,我们才能避免“走马观花” 最终将收获无多的尴尬场景那么读源码读的是什么?我们要关注哪些方面呢?读目的:该框架是为了解决什么问题?比同类框架相比的优劣是什么?这对理解框架非常重要读注释:很多人读源码会忽略注释建议大家读源码时一定要重视注释因为优秀的开源项目,通常某个类、某个函数的目的、核心逻辑、核心参数的解释,异常的发生场景等都会写到注释中,这对我们学习源码,分析问题有极大的帮助读逻辑:这里所谓的逻辑是指语句或者子函数的顺序问题我们要重视作者编码的顺序,了解为什么先写 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 (非懒初始化) finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. // 12 最后一步,完成容器的创建 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. // 销毁已经常见的单例 bean destroyBeans(); // Reset 'active' flag. // 重置 active 标志 cancelRefresh(ex); // Propagate exception to caller. // 将异常丢给调用者 throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... // 重置缓存 resetCommonCaches(); } }}
我们可以要特别重视每个步骤的含义,思考为什么这些要这么设计,然后再进入某个子函数中去了解具体的实现比如再去了解第 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; }
在函数中声明一个和成员变量同名的局部变量,然后将成员变量赋值给局部变量,再去使用看似很小的细节,隐含着一个优化思想这就需要借助反编译大法,在字节码层面去分析详细解读参见《为什么要推荐大家学习字节码?》总结总之,读源码要着重思考,思考为什么这么设计?可能的原因是什么?然后去验证学习代码在平时,工作时如果项目开发工期不紧,编码过程中进入源码分析学习,积少成多;在开发过程中,如果遇到问题,可以选择进入源码调试,这样印象更深刻;此外,我们既要埋头苦干也要“仰望星空”(巩固专业基础),有些核心的软件设计原则,操作系统、计算机网络的设计原理,都是源码设计思想的重要来源,如果专业基础不扎实,往往很难了解问题的本质(图片来源网络,侵删)
0 评论