1. 程式人生 > >Dubbo服務暴露原始碼解析②

Dubbo服務暴露原始碼解析②

[TOC] ​ 先放一張官網的服務暴露時序圖,對我們梳理原始碼有很大的幫助。注:不論是暴露還是匯出或者是其他翻譯,都是描述export的,只是翻譯不同。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226134935598-1389747515.png) ## 0.配置解析 ​ 在Spring的配置檔案中,Dubbo指明瞭DubboNamespaceHandler類作為標籤解析。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135016685-1372145440.png) ​ 與服務相關的顯然就是service,找到對應的ServiceBean類,進入這個類,開始服務暴露的原始碼分析。這個類位於Dubbo原始碼config模組-spring模組下的根目錄。 ## 1.開始export ​ export也是上面時序圖中最開始的一個方法,從這個方法名也知道,這就是服務暴露或者叫出口最關鍵的方法。進入ServiceBean類,在這個類中一共有兩處呼叫了此方法。即**onApplicationEvent**和**afterPropertiesSet**,瞭解過Spring Bean生命週期的朋友看到這兩個方法肯定眼熟,果然,這個類實現了相關的介面: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135053561-980597506.png) ​ 看一下onApplicationEvent方法: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135115833-57431279.png) ​ 從它的if判斷條件呼叫的幾個方法名可以看出,如果是延遲暴露、還未暴露過且支援暴露就可以執行export方法了。這裡說一下,這個isDelay方法有點迷惑,字面意思應該為是否延遲,返回ture代表延遲。**但是實際意思卻為返回true代表不延遲**,因為這個判斷條件是delay==null || delay==-1,代表沒有設定延遲。所以這個方法中的export才是第一個觸發的。 ​ 接著進入到export方法。這個方法會跳轉到ServiceConfig類,是ServiceBean的父類,也正好符合時序圖。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135145103-1854929643.png) ​ 這幾個if的作用就是判斷是否需要暴露和延遲暴露。如果不需要暴露就返回,否則都會執行doExport方法的。進入這個方法,這個方法程式碼很多,前面一堆if都是檢測配置資訊的,關注的重點在doExportUrls方法。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135216344-1943425155.png) ​ Dubbo是支援多註冊中心和多協議的,在這裡就表現出來了。獲取到的註冊中心URL放到一個list裡面。其中loadRegistries方法就是根據配置組裝成相關的URL並返回,如載入註冊中心地址、檢查地址是否合法、新增配置資訊等。咱們先關注重點,這個方法就不跟下去了,不然沒完沒了。至於組裝後的URL可以debug自己看看,大概樣子如下: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135239160-1248805170.png) ## 2.組裝URL ​ 進入到doExportUrlsFor1Protocol方法,這個比較重要。從它的名字可以看出,它的作用是組裝暴露URL。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135314714-1815106254.png) ​ 這個方法很長,主要就是建立一個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 中 } } ``` ​ 當然,如果你沒有配置相關的資訊,如,在debug原始碼時,壓根就不會進入到這些分支裡面。現在我們看一下URL長啥樣: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135350464-692833411.png) ​ 可以看到協議已經變成了dubbo,具體的服務介面也顯示了出來。而map的值就存在parameters當中。 ## 3.服務暴露 ​ 依舊在doExportUrlsFor1Protocol方法裡,具體的服務URL已經組裝好了,接下來就是服務暴露了。先看這麼一段程式碼: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135528938-415304408.png) ​ 這段程式碼有兩個關鍵點,已經在圖中標註。**第一處是先進行本地暴露。第二處判斷如果有註冊中心,就會進行遠端暴露。**註冊中心的URL在doExportUrls中已經獲取了。 ​ 先看**本地暴露**,進入到exportLocal方法: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135459167-1043089034.png) ​ exportLocal方法比較簡單,根據協議頭判斷是否需要暴露服務,如果需要,就建立一個新的URL ​ 我們看一下這個URL長啥樣: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135604233-648680814.png) ​ 協議變成了injvm,從這個協議名稱就可以猜測到,這個在一個jvm內的協議。IP地址也從遠端註冊中心的IP地址變成了本機地址。 ​ 本地URL組裝好後,會建立一個exporter物件。這個物件是由protocol的export方法生成,我們點進這個抽象方法,會發現它有一個@Adaptive註解。這個註解修飾方法時會生成一個代理類。主要配合SPI機制使用,SPI的作用簡單的說就是提供一個標準化的介面,可能有不同的實現,而這個實現類的路徑我們就放在一個固定的位置,讓框架去讀取。同樣的用法也在proxyFactory.getInvoker()中。關於SPI的解析放在最後。這個export的具體實現方法如下圖: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135640893-568644948.png) ​ 所在類為InjvmProtocol。這個實現方法就不說了,主要就是根據傳入的引數進行封裝,我們直接看最終的exporter: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135706582-946634675.png) ​ 可以看到,已經找到了服務介面的實現類了。最後就是將exporter新增到exporters中,這個exporters是本地的一個集合,專門快取exporter。 ​ 接著就是**遠端暴露**了,其實和本地暴露的目的一樣,都要封裝成invoker——>exporter,最後新增到exporters中,還多了一步註冊。首先依舊是通過getInvoker封裝成invoker。(這裡說句題外話,可以根據引數的協議型別找到這些抽象方法的實現類。Dubbo命名很嚴謹,比如引數中,URL的協議為registry,那麼其實現類就是RegistryProtocol。至於為什麼要封裝成invoker我們最後再分析,現在只需理解這麼做是為了遮蔽細節,統一暴露)。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135735428-515980849.png) ​ 封裝成invoker後又弄了一層wrapperInvoker,點進這個類,可以發現其實就給invoker額外封裝一層,可以提供更多資訊以及一些工具方法,比如ServiceConfig、檢測是否有效。 ​ 接著主要區別在export方法當中,其實現方法在RegistryProtocol類中(因為引數wrapperInvoker的url協議為registry)。實現方法部分截圖如下: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135856238-1900910749.png) ​ 這個方法主要做了如下工作: ​ 1.呼叫doLocalExport匯出服務 ​ 2.向註冊中心註冊 ​ 3.向註冊中心訂閱override資料 ​ 4.建立並返回DestroyableExporter ​ 首先進入到doLocalExport方法,這個方法主要就是會呼叫DubboProtocol的export方法,為了避免過多的程式碼截圖把自己弄昏了,就不貼這個方法了。這個方法開頭同樣的,根據invoker獲取URL,關鍵在於它呼叫了一個openServer。看到這個方法名應該知道是啥意思了,即開啟服務。好傢伙,終於要結束了麼。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135826604-1877736942.png) ​ 這個方法很清晰,獲取註冊中心的IP和埠號、檢查快取、建立server。接著跟進原始碼,bind過程,主要關注Transports的bind方法。這裡Dubbo也是用Adaptive註解和SPI機制,實現了拓展功能。它會根據傳入的引數選擇不同型別的Transport,預設是NettyTransporter。接下來就是Netty服務啟動的相關過程了,以前寫過相關部落格,就不跟進了。 ​ 接著,我們看上上張截圖,有一個if會判斷是否需要註冊,如果需要註冊就會向註冊中心註冊。我們接著跟蹤原始碼,一直到如下方法: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135932095-1400132419.png) ​ 看到了Zookeeper客戶端,到這裡就明白了,是向Zookeeper新增資訊。我們最後看一下Zookeeper裡面的內容。我們開啟Zookeeper客戶端,檢視一下服務: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226135957121-1926337132.png) ​ 可以發現,已經有我們註冊的服務了。最好下個視覺化的Zookeeper客戶端,可以進入到這些目錄,可以找到Provider的IP地址。 ## 疑問解析 - **為什麼要本地暴露?** - - 呼叫本地服務時,避免網路通訊。 - **為什麼要封裝成invoker和export?** - - 前面的原始碼分析中,本地和遠端都經過了封裝invoker和export兩個步驟。export是服務暴露的最終形態,其包含invoker以及其他更多資訊,比如註冊中心、服務介面、實現類等等資訊。下面是官網的一張截圖: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226140303266-355423937.png) - - 官網是這麼說的:由於 Invoker 是 Dubbo 領域模型中非常重要的一個概念,很多設計思路都是向它靠攏、或轉換為它。這個所謂的靠攏就如圖中顯示的那樣,不管在消費者方還是服務提供方,均會出現Invoker,它代表一個可執行體,並遮蔽了內部細節。既然它這麼重要,我們就看一下它是如果建立的。 - 其是由*proxyFactory*.getInvoker建立而來,通過debug找到它的實現類: ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226140432700-967668828.png) - - 上面的方法在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](http://dubbo.apache.org/zh/docs/v2.7/dev/source/export-service/#221-invoker-建立過程)。 - **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目錄下。我們從原始碼的該路徑下找個檔案看看。 ![](https://img2020.cnblogs.com/blog/1383122/202012/1383122-20201226140502177-1920940779.png) ​ 可以看到Dubbo SPI的配置檔案內容是鍵值對的形式,這樣就可以實現按需載入。根據key值,獲取全路徑名,然後載入。 如果需要自己自定義,就直接在MEATA-INF/dubbo/目錄下建立配置檔案即可。同樣的,類似Java SPI中的ServiceLoader,Dubbo中叫**ExtensionLoader**。這個類的幾個方法,作用很明確,也不復雜,這裡就不跟蹤了。其中**getExtensionLoader**方法,入參是需要載入的介面,這個方法會檢查是否有對應型別的ExtensionLoader物件,如果沒有就新建一個。**createExtension**方法就是根據名字獲取對應的實現類,這樣就實現了按需