前言
讀原始碼是大多數程式設計師進階的重要途徑,最近了解到很多朋友反饋讀了一些原始碼但是收穫不是很大,看了一些原始碼總是半途而廢,有很多困惑。
主要表現為:
讀原始碼的時候並不知道該讀啥很容易迷失在細節中,除錯時跳來跳去跳暈了,很難堅持下去讀完很快就忘掉了,無法靈活運用網上也會有一些講某個具體開原始碼的系列文章,通常比較冗長,傳授的都是“魚”而不是“漁”。 俗話說:授人以魚不如授人以漁。
我相信大多數同學希望得到方法論級別、更加系統化介紹如何更好地閱讀原始碼的文章。 為此,在這裡我打算將自己的讀原始碼經驗傳授給大家,相信會讓很多人理解問題的癥結所在,給出一些“意料之外”的實用建議,讓飽受讀原始碼困惑的同學能夠找到方向。
文章重點講到如下內容:
為什麼很多人讀原始碼收穫不大?讀原始碼究竟讀什麼?有哪些讀原始碼重要的思想?有哪些好的讀原始碼切入點?有哪些讀原始碼非常實用的技巧?整體概覽:
為什麼很多人讀原始碼收穫不大?在我看來,大多數人讀原始碼收穫不大的主要原因如下:
缺乏整體思維,迷失在細節中(如除錯原始碼時跳來跳去,最後跳暈了)缺乏思考(學而不思則罔,思而不學則殆!)不知道讀原始碼究竟讀什麼(如原始碼的設計思想)角度單一(如從解決問題角度、效能最佳化角度、設計模式角度、每次提交、單元測試、註釋等)方法單一(如不懂的高階的除錯技巧,不懂的時序圖外掛)缺乏輸出(不會輸出成文章,不能講給別人聽)讀原始碼究竟讀什麼?做事要“以終為始”,只有搞清楚讀原始碼我們究竟想得到什麼,我們才能避免“走馬觀花” 最終將收穫無多的尷尬場景。
那麼讀原始碼讀的是什麼?我們要關注哪些方面呢?
讀目的:該框架是為了解決什麼問題?比同類框架相比的優劣是什麼?這對理解框架非常重要。
讀註釋:很多人讀原始碼會忽略註釋。建議大家讀原始碼時一定要重視註釋。因為優秀的開源專案,通常某個類、某個函式的目的、核心邏輯、核心引數的解釋,異常的發生場景等都會寫到註釋中,這對我們學習原始碼,分析問題有極大的幫助。
讀邏輯:這裡所謂的邏輯是指語句或者子函式的順序問題。我們要重視作者編碼的順序,瞭解為什麼先寫 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 版本的原始碼,依賴:
/** * 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 Immutable*Array.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; }
在函式中宣告一個和成員變數同名的區域性變數,然後將成員變數賦值給區域性變數,再去使用。
看似很小的細節,隱含著一個最佳化思想。這就需要藉助反編譯大法,在位元組碼層面去分析。
詳細解讀參見《為什麼要推薦大家學習位元組碼?》。
總結總之,讀原始碼要著重思考,思考為什麼這麼設計?可能的原因是什麼?然後去驗證。
學習程式碼在平時,工作時如果專案開發工期不緊,編碼過程中進入原始碼分析學習,積少成多;在開發過程中,如果遇到問題,可以選擇進入原始碼除錯,這樣印象更深刻;此外,我們既要埋頭苦幹也要“仰望星空”(鞏固專業基礎),有些核心的軟體設計原則,作業系統、計算機網路的設計原理,都是原始碼設計思想的重要來源,如果專業基礎不紮實,往往很難了解問題的本質。