1. 前言
Java 學習和進階離不開閱讀源碼,但是很多人只知道閱讀源碼卻不知道如何閱讀源碼更有效始衅。
很多人面對源碼無從下手,也有很多人閱讀源碼剛開始就陷入細節(jié),看著看著就暈了粱侣,很難堅持下去。
也有很多人看了很多源碼蓖宦,最終都 “忘了”齐婴,沒留下什么印象。
我自己也遇到過類似的問題稠茂,通過探索和交流總結(jié)了一些經(jīng)驗柠偶,在此分享給大家。
2. 讀源碼究竟讀什么主慰?
很多人只是知道閱讀源碼是進階的一個重要步驟嚣州,但是在閱讀之前并不是很清楚到底要通過源碼學到什么。
如果讀源碼之前想不清楚這件事共螺,很容易 “走馬觀花”该肴,收獲無多。
通過閱讀源碼可以學習到很多知識藐不,如:編碼規(guī)范匀哄,包括類、函數(shù)雏蛮、屬性的命名涎嚼,注釋的規(guī)范等;優(yōu)秀程序員的編程思想挑秉;學習一些高級的編程技巧法梯;某些功能或特性的核心原理;可以學習到一些好的設計原則、設計模式如何落地立哑。
3. 閱讀源碼的思路
閱讀源碼的方法和心態(tài)很重要夜惭,很多人想一口氣吃個大胖子,急于求成最后適得其反铛绰。
很多人急躁的心情是可以理解的诈茧,想早點攻克某個框架源碼,但是大家可以回想一下打游戲的場景捂掰,想打好游戲渤闷,通常需要學習各種通關技巧袖瞻,需要先 “打野”迷扇。
下面介紹幾個閱讀源碼的思路瑟蜈。
3.1 從設計者的角度看源碼
從設計者的角度看源碼是最有效的方式。
源碼也是人寫出來的疤苹,源碼的作者編寫代碼之前也是在頭腦中思考過的互广。
源碼,尤其是復雜源碼卧土,都是符合 “任務拆分” 的原則的惫皱,即一個大的功能分為幾個核心的步驟,分別編寫代碼尤莺。
這也符合羅伯特?C?馬堵梅蟆(Robert Cecil Martin)所提出的面向?qū)ο笪宕蠡驹瓌t之一的:單一職責原則。
單一職責原則:一個類或者模塊應該有且只有一個改變的原因
因此我們學習源碼要想好編寫這個功能應該有哪些步驟颤霎,再去和源碼對比媳谁。
這樣才能驗證自己思考問題的角度是否正確,是否有遺漏友酱。
通過對比能夠清楚地知道作者為什么要這么設計晴音,作者的源碼比自己所設想的好在哪里,這樣才不容易遺忘缔杉。
這就像學生時代做數(shù)學題一樣锤躁,很多人會發(fā)現(xiàn)如果我們不做題就直接看答案,我們會認為問題都很簡單或详,自己都會系羞。但是真正脫離答案去做題時,往往并不會做霸琴。這也像我們拿著復雜迷宮的答案圖紙去看迷宮時椒振,會認為迷宮并不難,但是沒有提前看答案時梧乘,破解迷宮的難度是要大很多的澎迎。
下面舉一個非常簡單的例子:
在開發(fā)時,需要借助 okhttp 封裝一個 HTTP 請求工具類,其中涉及到編寫一個判斷請求是否成功的函數(shù)夹供。
正如很多人認為地那樣辑莫,在封裝地函數(shù)中直接判斷響應碼是否等于 200 即可。
public boolean isSuccessful(Integer code) {
return 200 == code;
}
但是當我們?nèi)ゲ榭?okhttp3.Response
的 isSuccessful
的寫法:
/**
* 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
突然發(fā)現(xiàn)我們的想法不夠嚴謹罩引,響應碼從 200 到 299 都應該算請求成功。
這樣我們對源碼的某個細節(jié)的印象就會非常深刻枝笨,更加清晰地了解到自己思路的不嚴謹性袁铐。
如果我們的 “猜想” 核心的步驟和最終和源碼比對,如果和作者的邏輯非常一致時横浑,我們就很開心剔桨,這也是看源碼的樂趣之一。如果不一致徙融,通過對比完善自己的思路洒缀。
通過這種方式去讀源碼能夠不斷糾正我們的思路,不斷發(fā)現(xiàn)我們的問題欺冀,這是閱讀源碼非常重要的一個目的树绩。
3.2 先整體后局部
俗話說 “磨刀不誤砍柴工”,這幾乎是盡人皆知的道理隐轩,但是學習編程時很多人依然會著急看源碼饺饭,不重視背景知識,不重視框架的整體思想职车,導致后面浪費更多地時間瘫俊。
為了避免過早陷于局部而缺乏全局觀念,應該先從整體了解一個技術(shù)的核心模塊再去學習每個具體模塊的源碼悴灵。
比如我們學習 dubbo 源碼之前必須想了解該框架的主要模塊以及之間的關系扛芽。
先了解架構(gòu)的核心角色以及調(diào)用關系,再去學習源碼會更容易一些积瞒。
先仔細閱讀官方手冊再去學習源碼川尖,很多人不重視官方手冊,學習很久甚至工作很久赡鲜,連核心技術(shù)棧的官方手冊都沒認真看過一遍空厌,這是一件非常可怕的事情银酬。
對要學習的技術(shù)有一個整體地了解之后嘲更,可以去拉取源碼,去看源碼包含哪些模塊揩瞪,每個模塊的大體功能是什么赋朦,各個模塊之間有什么關系等,然后再去看代碼的細節(jié)。而大多數(shù)人讀源碼宠哄,會認為這些不重要壹将,會急于讀源碼,導致效果不好毛嫉。
3.3 由易到難
尤其是對于很多新手來說诽俯,連核心技術(shù)棧使用都不熟悉的情況下,直接看其源碼很容易遭受很大打擊承粤。
因此要根據(jù)自己的階段去選擇適合自己的框架來閱讀暴区。
這一點和打游戲是非常一致的,一般開局都先 “打野”辛臊,通過 “打野” 來提升等級獲取裝備等仙粱,再去和高級的敵人對抗。
對于初學者而言彻舰,可以先從開發(fā)中常用的簡單的框架入手伐割,如 commons-lang 、commons-collections刃唤、 guava 等隔心。從看這些簡單的源碼積累經(jīng)驗,然后再去學習 spring 尚胞、spring boot济炎、dubbo 等框架的源碼。
另外要先保證能夠熟練使用辐真,再去學習源碼效果會更好一些须尚。如果連使用都不會就直接去學習源碼,是一種非常不理智的行為侍咱。
另外學習從來不是勻速的耐床,大家也明白 “欲速則不達” 的道理,建議可以先從簡單的框架入手楔脯,積累經(jīng)驗后快速將這種學習的能力遷移到自己想研究的框架中去撩轰。
3.4 帶著問題看源碼
3.4.1 通用的問題
看源碼和學某個技術(shù)之前,要重點思考幾個能從整體理解該項目的問題:
- 這個項目主要核心功能是什么昧廷?
- 這個框架能解決什么問題堪嫂?
- 有沒有同類的框架,有啥異同木柬?
很多人會忽略這些問題皆串,認為這些問題不重要,導致雖然能用起來眉枕,卻對框架的使用場景恶复、解決的本質(zhì)問題都不清楚怜森。
3.4.2 工作中遇到的問題
對于很多人而言,大多數(shù)時間都花費在工作上谤牡,業(yè)余時間并沒那么多副硅,那么如何去學源碼呢?
其實未必需要有大量完整地時間才可以去學習源碼翅萤,我們在開發(fā)過程中遇到問題時可以順便進入源碼來研究問題套么。
在工作任務不是特別忙的時候违诗,可以通過自己項目引用的 jar 包進入源碼中看一下平時常用的注解是如何解析的诸迟,常用的函數(shù)具體實現(xiàn)是怎樣的,常用的類中還有哪些其它函數(shù)等绅项。
當學習和工作中遇到問題時,如果能夠借機深挖,就可以借助這個問題帶動源碼的部分內(nèi)容的學習搪花。
如在 《虛擬機退出時機問題研究》小節(jié)所舉的例子,下面代碼打印語句還沒來得及執(zhí)行就結(jié)束了:
public class CompletableFutureDemo {
public static void main(String[] args) {
CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException ignore) {
}
System.out.println("異步任務");
});
}
}
最終我們通過進入 runAsync
函數(shù)的源碼跟蹤到 ForkJoinPool#registerWorker
函數(shù)發(fā)現(xiàn), ForkJoinPool
的工作線程類型為守護者線程授账。
我們就借著這個機會,學習了 CompletableFuture
源碼的部分知識點。
下面介紹另外一個問題攻臀,比如某同學使用 MyBatis 時,運行項目測試時發(fā)現(xiàn)找不到自定定義好的 Mapper,咋回事呢离例?
此時你要想出各種可能性:
- 包名是不是寫錯了掃描不到的猛?
- 接口是不是寫成類了?
等等最有可能的問題胡诗,然后依次排查瑰抵。
最終發(fā)現(xiàn)是因為自己誤將 Mapper 接口定義為類導致的,將類 (class) 改成接口 (interface) 就好了氓栈。
那么為啥會這樣呢提完?大家千萬不要就此打住蜗字。
了解 MyBatis 會通過 MapperRegistry 來注冊和獲取 Mapper 對象的代理,我們進入添加 mapper 的核心代碼:
可以看到該函數(shù)會先判斷 Mapper 是否為接口類型戏羽,如果是接口類型才會注冊此映射的代理對象始花,因此問題就非常明確了酷宵。
每一個問題都是我們學習源碼的好機會朴摊,希望大家能夠有這種意識鹃操。
很多人恰恰是平時不用心,臨近找工作突擊春哨,才導致學啥都不深入臭胜,結(jié)果可想而知。
<u>隨著遇到的問題越來越多癞尚,看過常見的類的源碼的函數(shù)越來越多耸三,對源碼的理解就越來越深刻,越來越熟悉浇揩。</u>
3.4.3 看 issues
通過看開源項目的 issues仪壮, 你可以發(fā)現(xiàn)該項目的潛在 BUG。
可以了解同一個問題胳徽,不同人的解決思路积锅,以及官方最終采用的是哪種方案。
很多人的解決方案养盗,對你實際的業(yè)務開發(fā)也有很大幫助缚陷。
3.4.4 看時序圖
有些人可能會想到,可以根據(jù)源碼畫出時序圖來理解源碼往核,但是畫圖非常耗時箫爷,怎么辦呢?
IDEA 插件 SequenceDiagram 就派上用場了聂儒,這個插件非常贊虎锚,可以根據(jù)源碼繪制出調(diào)用時序圖,對學習源碼幫助極大衩婚。
3.4.5 看錯誤堆棧信息
程序運行出錯時窜护,是我們學習的最佳時機。
大家可以通過 [Stack trace to UML 插件](Stack trace to UML ) 繪制出出錯的調(diào)用時序圖非春,了解調(diào)用的順序柱徙。
如下面出錯信息:
安裝好插件后,通過菜單:Analyze > Open Stack trace to UML plugin + Generate UML diagrams from stacktrace from debug 奇昙,將繪制出下面的時序圖:
當異常堆棧信息非常多時坐搔,通過該插件繪制出的時序圖將非常有助于幫助我們了解調(diào)用鏈,理解源碼敬矩。
3.5 帶著場景學源碼
比如從設計模式的角度去學習源碼概行。
可以從設計模式的六大原則來思考源碼的設計,思考源碼是如何體現(xiàn)這幾種原則的弧岳。
設計模式六大原則:單一職責原則凳忙、里氏替換原則业踏、依賴倒置原則、接口隔離原則涧卵、迪米特法則勤家、開放封閉原則。
還可以結(jié)合《設計模式之禪》這本書或者菜鳥教程中設計模式的教程柳恐,了解具體某些設計模式的特點伐脖、使用場景、優(yōu)點乐设、缺點等讼庇。
然后從 JDK 或者 Spring 等自己想學習的框架源碼中去尋找這些設計模式的身影。
通過這種方式可以更清楚設計模式該如何落地近尚,從更多角度去了解源碼蠕啄。
這里不詳細展開,希望大家自行學習戈锻。
3.6 通過源碼的單元測試來學習源碼
正如前面一些章節(jié)所提到的歼跟,大多數(shù)知名的 Java 開源項目都會有非常完善的單元測試,這是我們學習源碼的一個非常重要的突破口格遭。我們可以運行單元測試來調(diào)試源碼哈街,熟悉核心類的功能。
可以直接根據(jù)類名搜索拒迅,也可以通過找到該類骚秦,使用 “find usages” 功能來找到其單元測試代碼。
3.7 通過 DEMO 學源碼
大家可以使用官方的例子或者自己寫例子運行坪它,來體會某個項目的用法骤竹,研究其特性帝牡。
在這里推薦一個高質(zhì)量的英文技術(shù)文章網(wǎng)站 baeldung, 幾乎所有的文章都有<pangu> </pangu>配套代碼 , 我們可以直接通過該網(wǎng)站的代碼運行學習某些知識點往毡,某些框架。
大家學習某個框架靶溜,還可以自行去 github 找到相關的范例开瞭,運行學習。
另外罩息,超級推薦大家通過自己開發(fā)的項目來學習 Spring 源碼嗤详。大家可以對照著官方文檔、對照著 Spring 的源碼教程等瓷炮,觀察自己項目中某個 Spring 類的使用葱色,還可以在項目測試時偶爾進到源碼中斷點,通過調(diào)試自己的項目來學習源碼娘香。
4. 閱讀源碼的技巧
4.1 實現(xiàn) “簡易版” 是學習的重要途徑
比如學習 Spring 源碼之前苍狰,可以根據(jù)自己平時使用 Spring 的方式办龄,自己實現(xiàn)簡易版的 Spring,記錄自己編寫代碼的核心步驟淋昭,以及核心步驟的缺點和遇到的問題俐填。待真正去閱讀源碼時,很多問題豁然開朗翔忽。
可能很多人會認為英融,不是所有的代碼都有簡易版。的確如此歇式,但是只要思維靈活驶悟,方法總比困難多。
如可以購買或者尋找《Spring5 核心原理與 30 個類手寫實戰(zhàn)》書本所配套的簡單版 Spring 代碼贬丛,并且自己嘗試從最簡單版去改編撩银,梳理清楚核心邏輯,并記錄這個過程中遇到的困難豺憔。再去讀 Spring 源碼就會容易很多额获。
比如想讀 dubbo 源碼,可以在 github 上找一些簡單的 Java RPC 框架看會后再去看 dubbo 的源碼恭应。
4.2 尋找程序入口是一個學習源碼的切入點
通過尋找程序啟動的入口抄邀,對入口斷點調(diào)試,可以從源頭了解框架的啟動流程和運行原理昼榛。
可以通過打斷點境肾,然后通過調(diào)用棧逆向?qū)ふ胰肟冢豢梢哉揖W(wǎng)上的博客的源碼分析找到入口打斷點胆屿。
4.3 閱讀源碼時要重視函數(shù)的命名
往往優(yōu)秀的源碼函數(shù)命名都非常貼切奥喻。可以通過 IDEA 的 structure非迹,來了解源碼中某個類的核心函數(shù)环鲤。
核心類有哪些核心的函數(shù),這些函數(shù)的功能又是什么憎兽,對學習源碼幫助很大冷离。
通過單個函數(shù)快速了解其意圖,對學習源碼幫助很大纯命。
4.4 多看函數(shù)列表
在看源碼時建議打開函數(shù)列表西剥,進入某個類時優(yōu)先看該類有哪些公有函數(shù)。
這樣做有助于幫助你從整體了解該類亿汞,更全面地了解一個類的功能瞭空。
4.5 閱讀源碼時要重視源碼的注釋
優(yōu)秀的開源項目的類、函數(shù)甚至成員變量都會有非常詳盡的注釋。
注釋可以快速幫助我們理解源碼咆畏,幫助我們了解一些重要細節(jié)图甜。
比如很多代碼會給出其核心步驟,此時一定要先閱讀函數(shù)上面的注釋和內(nèi)部給出的核心步驟再去讀源碼鳖眼。
比較典型的一個案例 java.util.concurrent.ThreadPoolExecutor#execute
:
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1\. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2\. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3\. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
4.6 關注目標類繼承的類或者實現(xiàn)的接口
目標類的父類和實現(xiàn)的接口是研究該類功能和特征的重要突破口黑毅。
借助前面章節(jié)講到的調(diào)試技巧,查看調(diào)用棧钦讳,運行表達式等可以極大地幫助我們理解源碼矿瘦。通過 IDEA 提供的類圖功能,可以幫助我們理解不同類之間的關系愿卒。
如下圖所示缚去,通過 IDEA 自帶的類圖工具繪制出 fastjson 核心類之的類圖,通過類圖的選項來控制顯示的內(nèi)容和可見性琼开。
4.7 其它
大家可以使用前面調(diào)試章節(jié)所學到的查看調(diào)用棧易结、設置條件斷點、查看加載的對象等調(diào)試功能來幫助大家學習源碼柜候。
大家還可以跟著某個框架的專欄作者的思路去深入學習某個具體框架的源碼搞动。
5. 總結(jié)
本節(jié)主要講述如何閱讀源碼,講到了閱讀源碼的思路和一些技巧渣刷。希望通過本文的介紹大家可以更高效地閱讀源碼鹦肿,提高進階的速度。
推薦閱讀
- 源碼閱讀的三種境界 - 碼農(nóng)翻身
- 新手也能看懂的源碼閱讀技巧
- [Java并發(fā)編程之美 源碼閱讀技巧]