Dubbo服務暴露源碼解析②

​ 先放一張官網的服務暴露時序圖,對我們梳理源碼有很大的幫助。注:不論是暴露還是導出或者是其他翻譯,都是描述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方法就是根據名字獲取對應的實現類,這樣就實現了按需加載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章