模型的威力:基於模型,快速梳理原始碼

一瑜一琂發表於2022-04-30

上一篇我們將梳理的核心流程整合進了概念模型,得到了一個相對詳細的流程。
本篇是《如何高效閱讀原始碼》專題的第十篇,我們來通過閱讀原始碼來驗證上面得到的流程圖是否正確,同時進一步細化,從核心流程向外圍流程進行梳理,構建一個更完整的流程。

本節主要內容:

  • 從呼叫關係確定呼叫類

  • 梳理呼叫類結構

  • 梳理呼叫類核心流程

  • 完善流程圖

從呼叫關係查詢呼叫類

前文我們得到了下面這張圖:

 

 

我們猜測TestRunners會通過測試的Class來構建TestClass,現在我們通過原始碼來驗證這個猜測。
我們在TestClass的構造方法上按下ALT+F7,IDEA就會列出呼叫TestClass構造方法的類。

 

 

有很多的測試類,結合前面的核心包分析,我們可以直接定位到runners包,runners包中有個JUnit4類。這個類應該就是我們閱讀擴充套件模組的入口了!

梳理呼叫類結構

我們通過JUnit4這個類來構建TestRunners的UML結構。直接在JUnit4上右擊,選擇Diagrams即可。

 

 

從上圖我們可以看到JUnit4有三個父類,一共實現了四個介面。
這裡還有個小技巧,IDEA提供了一個功能,能區分各個類是否在同一個包下,只需要選中某個類,如果哪些類和選中的類不在同一個包下,則會被置灰。

例如,我們選中JUnit4,從下圖可以看到,除了ParentRunner和BlockJUnit4ClassRunner,其它類都被置灰了。說明其它類和JUnit4不在同一個包下,而ParentRunner和BlockJUnit4ClassRunner與JUnit4同屬於org.junit.runners包。結合前面的包關係圖,我們可以知道JUnit4與哪些包有關係。

 

 

梳理出了大致的呼叫類模型,我們可以基於這個模型來梳理流程,在梳理流程的同時,再反過來完善呼叫類模型。

梳理呼叫類核心流程

為了便於梳理,我們先忽略那四個介面,直接看類。
首先,我們注意到Runner和ParentRunner是兩個抽象類。好,這裡我們停下來回想一下,我們使用抽象類的作用是什麼?一般就三個作用:

  • 提供子類公用的方法

  • 定義流程,比如模板方法模式

  • 定義子類需要實現的抽象方法

所以我們可以從這三個角度來看這兩個抽象類。我們從最上層的Runner開始。

 

 

這個類非常的簡單,我們一眼就能看到那個最核心的方法---run方法。我們直接去找run方法的實現。假設你點選IDE右側的按鈕,展示子類,你會發現有很多的子類實現,想要找到具體的實現,是不是要瘋?回想一下,你使用debug來讀原始碼的時候,是不是經常遇到這樣的問題

 

 

我們在前面梳理出的模型,在這裡就起到了非常大的作用,限定了Runner的子類就是ParentRunner,所以我們直接到ParentRunner中去找run方法的實現。這也是先建模再梳理流程的優勢之一。

 

 

這裡的核心是通過classBlock方法構建了一個Statement,然後呼叫了evaluate方法並通過RunNotifier物件來監聽執行過程。從這裡我們知道Statement是個執行類,用於執行測試用,TestNotifier是個通知類,用於將執行資訊通知給對應的類,所以我們將其加入到呼叫模型中。

 

 

  • 為什麼使用Statement類?作用是什麼?

  • RunNotifier如何進行監聽的?

這裡我們先提出疑問,記錄下來,先梳理流程,後面再進行解答。
我們深入到classBlock方法中。

 

 

這裡通過childrenInvoker方法來構建了Statement。
if判斷裡的邏輯是幹什麼用的呢?看方法名好像和BeforeClass、AfterClass註解有關係,它是怎麼處理的呢?
我們先直接跳到childrenInvoker方法來將流程走完。

 

 

從這裡可以看到,這裡實際上建立了一個Statement的匿名類,呼叫的是ParentRunner中的runChildren方法。
為什麼要用Statement封裝一層?都在ParentRunner類裡面,直接呼叫不就好了嗎?

 

 

runChildren通過getFilteredChildren方法遍歷子元素,通過runChild來執行。
為什麼這裡要構建一個Runnable來執行呢?

 

 

getFilteredChildren方法使用了DCL來加鎖,實際呼叫getChildren來獲取子元素,而getChildren是個抽象方法,由子類來實現。具體是哪個子類實現的呢?這裡再一次提現了建模的優勢。想一想,如果是debug的話,這裡是不是又要迷失在一堆子類中了?而我們一開始就限定了需要閱讀的類,所以我們可以直接定位到BlockJUnit4ClassRunner這個類,看它的getChildren實現。

 

 

這裡可以看到和前面的TestClass關聯上了,去獲取的是TestClass中的所有包含了Test註解的方法,然後去執行。

完善流程圖

至此,我們梳理出了TestRunner呼叫TestClass的流程:

  • 某個類會建立一個Runner的例項,建立的可能是BlockJUnit4ClassRunner,也可能是JUnit4

  • 然後呼叫其run方法來執行測試

  • 此方法通過RunNotifier來通知對應的類,通過Statement類來執行

  • 執行方式是查詢測試類中,所有包含了Test註解的方法,一個個的去執行

我們將這個流程新增到流程圖中,進一步完善流程。

 

 

總結

本文通過呼叫關係,梳理出了TestRunner呼叫核心模型的流程。通過此方法不斷的向外梳理,你就能構建出完整的專案流程圖。
下節將對本節梳理出的問題做出解答,理解為什麼這麼設計。

相關文章