为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

再学经典:《Effective Java》独家解析

原价 ¥ 68.00

立即订阅
02 Java 源码学习指南
更新时间:2020-05-27 18:38:20
构成我们学习最大障碍的是已知的东西,而不是未知的东西。 —— 贝尔纳

1. 前言

在正式解读《Effective Java》之前,我们需要先了解读源码究竟要读什么,了解怎么读源码更有效。

结合源码才能更好地理解《Effective Java》一书中一些建议的原因,更清楚地了解规则如何落地。

另外,贯穿整个专栏的大多数章节会涉及到 Java 源码,因此能够快速读懂源码对我们专栏后续的学习有极大的帮助。

读源码也是我们学习进阶的必经之路。读源码可以了解设计模式的场景,可以了解并熟悉框架的底层实现,可以学习某个不熟悉类的用法,可以学习优秀的编程技巧等,这些对我们编程能力的提高也有极大的帮助。

然而很多人读源码会感到无从下手;读源码很容易遗忘;读源码不知有何用;读源码花费时间很多但是效果不佳,很难坚持下去。

本节将为大家讲解 Java 源码学习的一些经验。
图片描述

2. 读源码究竟读什么

我们做事情要遵循 “以终为始” 的思想,即需要明确我们的目标,这样才能避免跑偏,才能避免走弯路。

如果读源码毫无目标,毫无重点,“走马观花” 最终将收获无多。

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

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

注释: 很多人可能会奇怪,读源码怎么还需要关注注释呢?其实这正是我要特别强调的地方,很多人读源码会忽略注释。这里建议大家读源码时一定要重视注释。因为某个类、某个函数的目的、核心逻辑、核心参数的解释,异常的发生场景等都会写到注释中,这对我们学习源码,分析问题有极大的帮助。因此建议大家一定要重视源码注释。

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

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

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

风格: 一般来说优秀的源码的代码风格都比较优雅。我们可以通过源码来学习编码规范。比如类、函数、变量命名,注释的规范等。

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

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

3. 读源码的误区

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

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

3.1 开局打 boss

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

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

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

3.2 佛系青年

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

同样地,有些朋友决定读源码,但是没有规划,看到哪是哪,随心所欲,往往效果不太好。

3.3 “对着答案做题”

“对着答案做题” 和读源码误区有啥关系呢?

莫急,且听我慢慢分解。

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

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

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

4. 读源码的思想

4.1 先会用再读源码

所谓 “心急吃不到热豆腐”。

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

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

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

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

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

4.2 先易后难

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

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

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

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

4.3 先整体后局部

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

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

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

4.3.1 先看架构再读源码

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

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

如轻量级分布式服务框架 jupiter 框架README.md 给出了框架的整体架构:
图片描述(图片来自: jupiter 项目 README.md 文档)

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

4.3.2 先看项目结构再读源码

先整体后局部,还包括先看项目的分包,再具体看源码
图片描述(图片来自: jupiter 项目结构)

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

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

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

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

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

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

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

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

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

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

@Override
public 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 的代码块的具体步骤。

4.4 从设计者的角度学源码

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

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

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

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

比如我们封装 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 响应码的理解不够全面。

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

4.5 从设计模式的角度学源码

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

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

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

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

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

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

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

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

4.6 读源码的粒度问题

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

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

5. 读源码的技巧

5.1 通过注释学习源码

我们以 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;
  }

5.2 通过单元测试学源码

同样以学习 5.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);
}

5.3 从入口开始学源码

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

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

我们可以从 SpringApplicationrun 函数一直跟下去。

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

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

5.4 利用插件来学源码

5.4.1 类图插件

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

如下图为 fastjson 的核心类的类图:
图片描述

5.4.2 时序图插件

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

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

5.4.3 codota

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

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

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

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

5.5 通过提交记录学源码

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

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

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

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

当然,还有很多其他读源码的技巧,比如通过反编译、反编译那学源码(在后续章节会讲到),这里就不一一列举了。

6. 总结

本文重点讲解了 Java 源码学习的重要性,讲解学习源码的常见误区,然后通过讲解阅读源码的思想和阅读源码的技巧来阐述如何阅读源码更有效。通过本节的学习为后面深入解析《Effective Java》知识点做准备,同时希望本节介绍的方法和经验能够对大家在未来的学习和工作的进阶中有帮助。

下一节将讲述 Java 反编译,讲述学习反编译的好处,学习反编译对我们后面学习和研究的意义。

7. 思考与练习

1、 使用 IDEA 自带的类图插件,绘制 Java Collection 类图。

2 安装体验 SequenceDiagram IDEA 插件。

3、 Stack trace to UML IDEA 插件只支持根据错误堆栈绘制时序图,那么如何在调试时用上该插件绘制出当前调试的时序图呢?

欢迎在留言区进行评论和探讨。

}
立即订阅 ¥ 68.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

再学经典:《Effective Java》独家解析
立即订阅 ¥ 68.00

举报

0/150
提交
取消