Dubbo服務暴露原始碼解析②

耶low發表於2020-12-26

​ 先放一張官網的服務暴露時序圖,對我們梳理原始碼有很大的幫助。注:不論是暴露還是匯出或者是其他翻譯,都是描述export的,只是翻譯不同。

0.配置解析

​ 在Spring的配置檔案中,Dubbo指明瞭DubboNamespaceHandler類作為標籤解析。

​ 與服務相關的顯然就是service,找到對應的ServiceBean類,進入這個類,開始服務暴露的原始碼分析。這個類位於Dubbo原始碼config模組-spring模組下的根目錄。

1.開始export

​ export也是上面時序圖中最開始的一個方法,從這個方法名也知道,這就是服務暴露或者叫出口最關鍵的方法。進入ServiceBean類,在這個類中一共有兩處呼叫了此方法。即onApplicationEventafterPropertiesSet,瞭解過Spring Bean生命週期的朋友看到這兩個方法肯定眼熟,果然,這個類實現了相關的介面:

​ 看一下onApplicationEvent方法:

​ 從它的if判斷條件呼叫的幾個方法名可以看出,如果是延遲暴露、還未暴露過且支援暴露就可以執行export方法了。這裡說一下,這個isDelay方法有點迷惑,字面意思應該為是否延遲,返回ture代表延遲。但是實際意思卻為返回true代表不延遲,因為這個判斷條件是delaynull || delay-1,代表沒有設定延遲。所以這個方法中的export才是第一個觸發的。

​ 接著進入到export方法。這個方法會跳轉到ServiceConfig類,是ServiceBean的父類,也正好符合時序圖。

​ 這幾個if的作用就是判斷是否需要暴露和延遲暴露。如果不需要暴露就返回,否則都會執行doExport方法的。進入這個方法,這個方法程式碼很多,前面一堆if都是檢測配置資訊的,關注的重點在doExportUrls方法。

​ Dubbo是支援多註冊中心和多協議的,在這裡就表現出來了。獲取到的註冊中心URL放到一個list裡面。其中loadRegistries方法就是根據配置組裝成相關的URL並返回,如載入註冊中心地址、檢查地址是否合法、新增配置資訊等。我們們先關注重點,這個方法就不跟下去了,不然沒完沒了。至於組裝後的URL可以debug自己看看,大概樣子如下:

2.組裝URL

​ 進入到doExportUrlsFor1Protocol方法,這個比較重要。從它的名字可以看出,它的作用是組裝暴露URL。

​ 這個方法很長,主要就是建立一個map然後新增各種值,包括配置資訊、提供的服務等等。由於這個方法分支非常多,官網給了各個分支含義的解釋,配合原始碼能很好理解其意思:

// 獲取 ArgumentConfig 列表
for (遍歷 ArgumentConfig 列表) {
    if (type 不為 null,也不為空串) {    // 分支1
        1. 通過反射獲取 interfaceClass 的方法列表
        for (遍歷方法列表) {
            1. 比對方法名,查詢目標方法
        	2. 通過反射獲取目標方法的引數型別陣列 argtypes
            if (index != -1) {    // 分支2
                1. 從 argtypes 陣列中獲取下標 index 處的元素 argType
                2. 檢測 argType 的名稱與 ArgumentConfig 中的 type 屬性是否一致
                3. 新增 ArgumentConfig 欄位資訊到 map 中,或丟擲異常
            } else {    // 分支3
                1. 遍歷引數型別陣列 argtypes,查詢 argument.type 型別的引數
                2. 新增 ArgumentConfig 欄位資訊到 map 中
            }
        }
    } else if (index != -1) {    // 分支4
		1. 新增 ArgumentConfig 欄位資訊到 map 中
    }
}

​ 當然,如果你沒有配置相關的資訊,如dubbo:method,在debug原始碼時,壓根就不會進入到這些分支裡面。現在我們看一下URL長啥樣:

​ 可以看到協議已經變成了dubbo,具體的服務介面也顯示了出來。而map的值就存在parameters當中。

3.服務暴露

​ 依舊在doExportUrlsFor1Protocol方法裡,具體的服務URL已經組裝好了,接下來就是服務暴露了。先看這麼一段程式碼:

​ 這段程式碼有兩個關鍵點,已經在圖中標註。第一處是先進行本地暴露。第二處判斷如果有註冊中心,就會進行遠端暴露。註冊中心的URL在doExportUrls中已經獲取了。

​ 先看本地暴露,進入到exportLocal方法:

​ exportLocal方法比較簡單,根據協議頭判斷是否需要暴露服務,如果需要,就建立一個新的URL

​ 我們看一下這個URL長啥樣:

​ 協議變成了injvm,從這個協議名稱就可以猜測到,這個在一個jvm內的協議。IP地址也從遠端註冊中心的IP地址變成了本機地址。

​ 本地URL組裝好後,會建立一個exporter物件。這個物件是由protocol的export方法生成,我們點進這個抽象方法,會發現它有一個@Adaptive註解。這個註解修飾方法時會生成一個代理類。主要配合SPI機制使用,SPI的作用簡單的說就是提供一個標準化的介面,可能有不同的實現,而這個實現類的路徑我們就放在一個固定的位置,讓框架去讀取。同樣的用法也在proxyFactory.getInvoker()中。關於SPI的解析放在最後。這個export的具體實現方法如下圖:

​ 所在類為InjvmProtocol。這個實現方法就不說了,主要就是根據傳入的引數進行封裝,我們直接看最終的exporter:

​ 可以看到,已經找到了服務介面的實現類了。最後就是將exporter新增到exporters中,這個exporters是本地的一個集合,專門快取exporter。

​ 接著就是遠端暴露了,其實和本地暴露的目的一樣,都要封裝成invoker——>exporter,最後新增到exporters中,還多了一步註冊。首先依舊是通過getInvoker封裝成invoker。(這裡說句題外話,可以根據引數的協議型別找到這些抽象方法的實現類。Dubbo命名很嚴謹,比如引數中,URL的協議為registry,那麼其實現類就是RegistryProtocol。至於為什麼要封裝成invoker我們最後再分析,現在只需理解這麼做是為了遮蔽細節,統一暴露)。

​ 封裝成invoker後又弄了一層wrapperInvoker,點進這個類,可以發現其實就給invoker額外封裝一層,可以提供更多資訊以及一些工具方法,比如ServiceConfig、檢測是否有效。

​ 接著主要區別在export方法當中,其實現方法在RegistryProtocol類中(因為引數wrapperInvoker的url協議為registry)。實現方法部分截圖如下:

​ 這個方法主要做了如下工作:

​ 1.呼叫doLocalExport匯出服務

​ 2.向註冊中心註冊

​ 3.向註冊中心訂閱override資料

​ 4.建立並返回DestroyableExporter

​ 首先進入到doLocalExport方法,這個方法主要就是會呼叫DubboProtocol的export方法,為了避免過多的程式碼截圖把自己弄昏了,就不貼這個方法了。這個方法開頭同樣的,根據invoker獲取URL,關鍵在於它呼叫了一個openServer。看到這個方法名應該知道是啥意思了,即開啟服務。好傢伙,終於要結束了麼。

​ 這個方法很清晰,獲取註冊中心的IP和埠號、檢查快取、建立server。接著跟進原始碼,bind過程,主要關注Transports的bind方法。這裡Dubbo也是用Adaptive註解和SPI機制,實現了擴充功能。它會根據傳入的引數選擇不同型別的Transport,預設是NettyTransporter。接下來就是Netty服務啟動的相關過程了,以前寫過相關部落格,就不跟進了。

​ 接著,我們看上上張截圖,有一個if會判斷是否需要註冊,如果需要註冊就會向註冊中心註冊。我們接著跟蹤原始碼,一直到如下方法:

​ 看到了Zookeeper客戶端,到這裡就明白了,是向Zookeeper新增資訊。我們最後看一下Zookeeper裡面的內容。我們開啟Zookeeper客戶端,檢視一下服務:

​ 可以發現,已經有我們註冊的服務了。最好下個視覺化的Zookeeper客戶端,可以進入到這些目錄,可以找到Provider的IP地址。

疑問解析

  • 為什麼要本地暴露?

    • 呼叫本地服務時,避免網路通訊。
  • 為什麼要封裝成invoker和export?

    • 前面的原始碼分析中,本地和遠端都經過了封裝invoker和export兩個步驟。export是服務暴露的最終形態,其包含invoker以及其他更多資訊,比如註冊中心、服務介面、實現類等等資訊。下面是官網的一張截圖:

    • 官網是這麼說的:由於 Invoker 是 Dubbo 領域模型中非常重要的一個概念,很多設計思路都是向它靠攏、或轉換為它。這個所謂的靠攏就如圖中顯示的那樣,不管在消費者方還是服務提供方,均會出現Invoker,它代表一個可執行體,並遮蔽了內部細節。既然它這麼重要,我們就看一下它是如果建立的。
    • 其是由proxyFactory.getInvoker建立而來,通過debug找到它的實現類:

    • 上面的方法在JavassistProxyFactory類中,其重寫了doInvoke方法,比較簡單,只是轉發了invokeMethod。其中AbstractProxyInvoker是一個抽象類,實現了Invoker介面。而這個Wrapper的作用是包裹目標類,僅可通過getWrapper(Classs)建立子類。子類可以對入參Class進行解析,拿到類方法、成員變數等資訊。在這裡,目標類就是暴露服務的實現類。
    • 關於Wrapper的分析內容非常多,這裡記錄一下官網的解析:http://dubbo.apache.org/zh/docs/v2.7/dev/source/export-service/#221-invoker-%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B
  • SPI是什麼?

    • SPI(Service Provider Interface),其作用前面也說了,就是定義一個標準介面,這個介面的實現由使用者決定。這樣做的好處就是提高了框架的擴充性。但是這個介面的實現放在哪,得讓框架知道。在Java SPI中,規定在META-INF/services/ 目錄下,建立一個以介面全路徑名命名的檔案,檔案中寫出介面實現類的全路徑名。然後Java就會去遍歷載入這些實現類並建立例項。
    • 前面說了Java SPI,但是Dubbo並沒有用Java規定的方法,而是自己實現了SPI機制。可以從ServiceLoader.load()方法跟蹤原始碼看一下,Java SPI機制是遍歷了所有的實現類,而不是按需載入,造成了不必要的浪費。說到Dubbo SPI,那麼它的規定目錄在哪?在META-INF/dubbo/internal目錄下。我們從原始碼的該路徑下找個檔案看看。

​ 可以看到Dubbo SPI的配置檔案內容是鍵值對的形式,這樣就可以實現按需載入。根據key值,獲取全路徑名,然後載入。 如果需要自己自定義,就直接在MEATA-INF/dubbo/目錄下建立配置檔案即可。同樣的,類似Java SPI中的ServiceLoader,Dubbo中叫ExtensionLoader。這個類的幾個方法,作用很明確,也不復雜,這裡就不跟蹤了。其中getExtensionLoader方法,入參是需要載入的介面,這個方法會檢查是否有對應型別的ExtensionLoader物件,如果沒有就新建一個。createExtension方法就是根據名字獲取對應的實現類,這樣就實現了按需載入。

相關文章