如何去閱讀原始碼,我總結了18條心法

2023-02-07 15:02:37

大家好,我是三友~~

這篇文章我準備來聊一聊如何去閱讀開源專案的原始碼。

在聊如何去閱讀原始碼之前,先來簡單說一下為什麼要去閱讀原始碼,大致可分為以下幾點原因:

  • 最直接的原因,就是面試需要,面試喜歡問原始碼,讀完原始碼才可以跟面試官battle
  • 提升自己的程式設計水平,學習程式設計思想和和程式碼技巧
  • 熟悉技術實現細節,提高設計能力
  • ...

那麼到底該如何去閱讀原始碼呢?這裡我總結了18條心法,助你修煉神功

學好JDK

身為一個Javaer,不論要不要閱讀開源專案原始碼,都要學好JDK相關的技術。

所有的Java類開源專案,本質上其實就是利用JDK已有的類庫和關鍵字實現一種業務功能,所以學會了JDK相關的類庫是看其它的原始碼基礎。

如果你不懂JDK,你去閱讀原始碼會發現有太多看不懂的地方,會影響讀原始碼的心情和信心。

學習JDK主要包括使用和原理兩部分。內容大致包括以下幾部分:

  • 集合相關,比如常見的Map,List,Queue的實現,包括執行緒安全與不安全
  • 並行相關,比如synchronized、volatile、CAS、AQS、鎖、執行緒池、原子類等等
  • io相關,包括bio和nio等等
  • 反射相關
  • 網路程式設計相關
  • ...

瞭解設計模式

在一個優秀的開源專案中,設計模式處處存在,所以在你開始閱讀原始碼之前最好先了解一下常見的一些設計模式。當你瞭解了一些設計模式以後,在原始碼中遇到了相關的設計模式,你就可以快速明白程式碼結構的設計,從而以整體的視角去閱讀相關程式碼。

同時,學習設計模式不僅可以幫助我們閱讀原始碼,在日常開發中也可以幫助我們設計出更易於擴充套件的程式。

學習設計模式的話可以看看《大話設計模式》這本書,如果不想看書也可以找一些視訊或者專欄。

之前我也寫過一篇關於開源專案中常用的設計模式文章 兩萬字盤點那些被玩爛了的設計模式 ,有興趣的小夥伴可以看看。

先從官網入手

官網是介紹開源專案的地方,同時也是學習一個開源專案最開始的地方,通過官網我們可以快速的瞭解專案,比如:

  • 專案的定位
  • 一些核心概念
  • 功能
  • 使用教學
  • 整體的架構和設計
  • 常見的問題及解答
  • ...
RokcetMQ官網
RokcetMQ官網

當你瞭解了專案的一些概念、功能等資訊之後,如果你在讀原始碼一旦發現了程式碼是實現這些概念或者功能的足跡,那麼能夠幫助你更好的理解程式碼。

熟悉原始碼模組結構

當你對專案有大致的瞭解之後,就可以從Github上把程式碼clone下來,官網有專案原始碼的Github地址。

當成功拉下來程式碼之後,就可以對專案原始碼模組進行簡單的分析,熟悉模組結構,分析模組功能,混個眼熟。

如上是RocketMQ原始碼,如果前面閱讀過官網相關的一些概念介紹,就大致可以知道這些模組有什麼功能。

RocketMQ概念介紹
RocketMQ概念介紹

比如說,原始碼中的broker模組,官網說broker主要是負責訊息儲存,那麼broker模組程式碼塊肯定就主要實現了訊息儲存的功能。

還有些模組可以根據單詞的意思進行判斷,比如common模組,一看就是儲存一些公共類的模組,example模組,就是RocketMQ使用程式碼範例的模組等等。

順著demo開始讀

有的小夥伴在讀原始碼的時候不知道從哪裡開始讀比較合適,最後隨便從原始碼中的某個模組就開始讀,讀讀越來越發現讀不下去。

讀原始碼正確的姿勢應該是從demo開始讀。

比如說,現在我想要閱讀一下RocketMQ生產者是如何傳送訊息的,整個過程是什麼樣的,那麼我首先至少得寫個傳送訊息的demo,看看程式碼是如何寫的。

demo一般可以從官網中檢視

RocketMQ官網傳送訊息程式碼示例
RocketMQ官網傳送訊息程式碼範例

除了官網,一般開源專案在原始碼中也會有相應的demo,程式碼放在範例模組,就比如上面提到的RocketMQ的example模組。

最後還可以通過谷歌搜尋一下demo。

DefaultMQProducer producer = new DefaultMQProducer("sanyouProducer");
//指定NameServer的地址
producer.setNamesrvAddr("localhost:9876");
//啟動生產者
producer.start();
//省略程式碼。。
Message msg = new Message("sanyouTopic""TagA""三友的java日記".getBytes(RemotingHelper.DEFAULT_CHARSET));
// 傳送訊息並得到訊息的傳送結果,然後列印
SendResult sendResult = producer.send(msg);

如上是RocketMQ生產者傳送訊息的一個demo,訊息傳送原始碼閱讀就從這塊程式碼開始入手,一步一步進入原始碼中,這就算開始閱讀原始碼了。

帶著目的去讀

帶著目的去讀其實很好理解,就拿上面生產者傳送訊息流程原始碼來說,讀原始碼的第一個目的其實就是弄懂生產者傳送訊息的流程。

除了弄懂生產者傳送訊息,你還可以帶著其它目的去讀。

比如說,訊息傳送的核心邏輯是send方法實現的,那麼除了訊息傳送,是不是可以去弄懂生產者在啟動的過程做了哪些事,也就是start方法的作用。

再比如生產者傳送訊息肯定涉及到網路通訊相關的內容,那麼瞭解RocketMQ底層網路通訊模型是不是也可以算一個目的。

當你帶著這些目的,你讀原始碼就有很強的目的性,讀完印象會很深刻。當然如果你最開始想不到這些目的,也沒有什麼關係,你可以先往下讀,在讀的過程中再去嘗試發現一些其它的目的。

先抓主線,再抓分支

有的小夥伴在讀原始碼的時候,每個方法都使勁一直往下點,最後都不知道程式碼進入到哪了,這其實是非常不可取的。

正確的方法應該是先抓住主線流程,分支流程先大致看看,知道大概是什麼作用,等讀完主線之後,再回過頭仔細讀一下分支程式碼。

舉個例子來說,在Spring中,ApplicationContext在使用之前需要呼叫一下refresh方法,而refresh方法就定義了整個容器重新整理的執行流程程式碼。

refresh方法部分截圖
refresh方法部分截圖

當在讀這段程式碼,你可以先讀一讀refresh中各個方法大致都做了什麼,等讀完之後,你可以具體的去讀每個程式碼的具體實現,比如說prepareRefresh幹了什麼,obtainFreshBeanFactory是如何獲取到BeanFactory的,prepareBeanFactory又在對BeanFactory做了什麼事等等。

不要過度摳實現細節

有的小夥伴在閱讀的時候特別喜歡深究,想要弄清每行程式碼是如何實現的,這不僅非常難而且也是不可取的。

就比如說,我們都知道,在Spring Bean的生命週期中,當存在基於xml的方式來宣告Bean的方式,Spring會去解析xml,生成BeanDefinition。當你想要了解Bean的生命週期過程的時候,其實是沒有太大的必要去過度扣Spring是如何解析xml生成BeanDefinition的細節,這對你整體瞭解Bean的生命週期沒有太大的意義,只需要知道最終會轉換成BeanDefinition就可以了。

那什麼時候去扣實現細節呢?

  • 當你需要使用到的時候,比如說你遇到了一個bug或者是需要擴充套件
  • 阻礙你理解功能實現的時候

大膽猜

讀原始碼的時候也需要我們發揮一點想象力,去猜一猜功能是如何實現的。猜不是瞎猜,而是基於目前瞭解的一些知識、技術或者是思想合理地去猜。

就比如說,當你已經知道了OpenFeign最終會對每一個FeignClient介面生成動態代理物件,之後注入的物件都是代理物件,代理物件中實現了RPC的請求之後,那麼當你在學習dubbo的時候,是不是就可以去猜測注入的dubbo介面最終也是一個動態代理物件,並且這個代理物件也實現了RPC的請求?

之後你在讀程式碼的時候就需要著重注意發現是否有動態代理生成的程式碼,這就算是一個目的,一旦發現了動態代理相關的程式碼,那麼這塊程式碼很可能就是dubbo RPC實現的核心。

學會看類名

不要小看類名,優秀的程式碼命名都是見名知意的,所以從類名也可能窺探出這個類的一些蛛絲馬跡。

如下列舉了幾個比較常用的命名習慣

  • 以Registry結尾的一般都是儲存功能,比如Spring中的SingletonBeanRegistry就是用來儲存單例Bean的;Mybatis中的MapperRegistry就是用來儲存Mapper介面的
  • 以Support、Helper、s、Util(s)結尾的一般都是工具類
  • 以Filter,Interceptor結尾的一般都是攔截作用,一般會配合責任鏈模式(Chain)使用
  • 以Event、Listener結尾的一般都是基於觀察者模式實現的事件釋出訂閱模型
  • ...

除了一些比較通用的命名習慣,也有一些專案獨有的一些命名習慣。

比如說Spring中常見的以PostProcessor結尾的都是擴充套件介面,實現這些介面可以拿到某個比較核心的元件,從而實現對Spring的擴充套件。

其實很多開源專案的命名都比較偏向Spring的命名風格,當你遇到了跟Spring的命名比較像的時候,那麼可以大膽猜測類的作用。

學會看類結構

類結構也非常重要,他也能夠幫助我們窺探類的大致功能。

ApplicationContext
ApplicationContext

如上圖,是Spring中ApplicationContext的繼承體系,當你需要了解ApplicationContext的時候,可以先去熟悉一下它的父介面的作用,當你大致弄明白了每個介面的作用,那麼ApplicationContext有啥作用就大致就清楚了。

除了可以看類繼承體系,還可以瀏覽一下類大致提供了哪些方法,瞭解對外提供的功能。

類方法通過快捷鍵 ctrl+F12(mac:fn+command+F12)檢視,並且還支援模糊搜尋方法名,我本人就非常喜歡這個快捷鍵

ApplicationContext
ApplicationContext

總結類的職責

當我們在讀完一個類的程式碼的時候,一定要總結這個類的職責,明白這個類存在的意義。一般情況下一個類核心職責只有一個,遵循單一職責的設計原則。

舉個例子,在RocketMQ中有一個類MQClientAPIImpl

MQClientAPIImpl
MQClientAPIImpl

其實從名字大概看不出這個類主要是有什麼功能,但是當我讀程式碼的時候發現每個方法最終都呼叫RemotingClient方法,而RemotingClient只有一個實現NettyRemotingClient,所以從這個實現和類名可以猜出來RemotingClient是傳送網路請求的使用者端,所以當讀完MQClientAPIImpl原始碼之後,我就知道了MQClientAPIImpl這個類的職責大致是封裝引數,然後通過RemotingClient向MQ傳送訊息的。

當知道這個類的職責的時候,那麼其它地方在呼叫這個類的方法的時候,就知道大概在做什麼事了。

習慣閱讀註釋

當你在讀原始碼的時候,如果有註釋,最好能先讀一下注釋,這樣能幫助你釐清類或者方法的功能,先知道功能,再去讀原始碼就容易多了。

註釋一般都是英文,如果看不懂,可以裝個外掛

寫好註釋

俗話說的好記性不如爛筆頭,寫好註釋也是閱讀原始碼中很重要的一個環節,好的註釋可以幫助快速回憶起實現細節和功能。

註釋並不需要對每行程式碼都註釋,當然如果你願意也沒多大問題,但是註釋應包括以下幾點內容:

  • 核心類和方法實現的核心功能
  • 核心功能大致的實現邏輯
  • 核心的成員變數的作用
  • 方法中不易讀懂的程式碼實現細節
DefaultMessageStore
DefaultMessageStore

如圖,是我讀RocketMQ中對於DefaultMessageStore類閱讀的註釋,這個類是RocketMQ中一個非常核心的類,從名字可以看出來跟訊息的儲存有關。這個類的功能非常多,所以我寫了很多註釋,列舉了這個類主要有哪些功能和這些功能實現的一些細節。

總結思想,及時輸出

當你讀完某個功能模組的時候,就可以嘗試對這塊功能實現邏輯或者思想進行總結。

比如說,當你瞭解了CAS思想的時候,你會發現,原來保證執行緒安全不僅僅可以通過加鎖的方式,還可以基於樂觀鎖的方式來實現。

在總結之後可以輸出成一個檔案,又或者是流程圖。我個人比較喜歡畫圖,這裡推薦兩個線上畫圖工具:

  • processon
  • draw.io

processon我平時就在用,功能多,但是需要收費;draw.io的話免費,圖示和顏色感覺比processon好看,平時文章中的貼圖就是用draw.io畫的。

這裡多說一句,總結思想還是非常重要的,在我閱讀了很多原始碼之後,我發現很多技術或者功能的實現原理最終都是殊途同歸。

提前瞭解依賴的技術

一般一個開源專案不是所有的技術都是自己實現的,它也會依賴一些其它的框架或者是思想,提前瞭解這些框架或者是思想,可以幫助你更好地閱讀和理清程式碼。

比如說,RocketMQ底層是基於Netty框架實現網路通訊的,當你對Netty有所瞭解,知道Netty在啟動的時候需要註冊一堆ChannelHandler用來處理網路請求,那麼在讀RocketMQ底層網路通訊功能的時候你就可以去找一下Netty啟動的程式碼,看看都註冊了哪些ChannelHandler,然後就知道RocketMQ是如何處理和傳送請求的。

查閱相關資料

當在閱讀原始碼的時候,對某一塊程式碼功能實現不太清楚的時候,可以通過查閱相關資料來輔助閱讀,包括但不限於以下幾種通道:

  • 官網
  • 書籍
  • Github
  • 文章
  • 視訊

堅持

最後一點也是最核心的一點就是堅持。只有你長期堅持讀原始碼,不停地思考,總結,不斷提升自身技術的廣度和深度,找到適合自己的閱讀方式,閱讀原始碼才會是越來越容易的一件事。