Dubbo 服務導出過程始於 Spring 容器發佈刷新事件,Dubbo 在接收到事件後,會立即執行服務導出邏輯。整個邏輯大致可分爲三個部分,第一部分是前置工作,主要用於檢查參數,組裝 URL。第二部分是導出服務,包含導出服務到本地 (JVM),和導出服務到遠程兩個過程。第三部分是向註冊中心註冊服務,用於服務發現。本篇文章將會對這三個部分代碼進行詳細的分析。
實現細節概覽
解析服務
基於 dubbo.jar 內的 META-INF/spring.handlers
配置,Spring 在遇到 dubbo 名稱空間時,會回調 DubboNamespaceHandler
。
所有 dubbo 的標籤,都統一用 DubboBeanDefinitionParser
進行解析,基於一對一屬性映射,將 XML 標籤解析爲 Bean 對象。
在 ServiceConfig.export()
或 ReferenceConfig.get()
初始化時,將 Bean 對象轉換 URL 格式,所有 Bean 屬性轉成 URL 的參數。
然後將 URL 傳給 協議擴展點,基於擴展點的 擴展點自適應機制,根據 URL 的協議頭,進行不同協議的服務暴露或引用。
暴露服務
1. 只暴露服務端口:
在沒有註冊中心,直接暴露提供者的情況下 [1],ServiceConfig
解析出的 URL 的格式爲: dubbo://service-host/com.foo.FooService?version=1.0.0
。
基於擴展點自適應機制,通過 URL 的 dubbo://
協議頭識別,直接調用 DubboProtocol
的 export()
方法,打開服務端口。
2. 向註冊中心暴露服務:
在有註冊中心,需要註冊提供者地址的情況下 [2],ServiceConfig
解析出的 URL 的格式爲: registry://registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0")
,
基於擴展點自適應機制,通過 URL 的 registry://
協議頭識別,就會調用 RegistryProtocol
的 export()
方法,將 export
參數中的提供者 URL,先註冊到註冊中心。
再重新傳給 Protocol
擴展點進行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0
,然後基於擴展點自適應機制,通過提供者 URL 的 dubbo://
協議頭識別,就會調用 DubboProtocol
的 export()
方法,打開服務端口。
服務提供者暴露一個服務的詳細過程
上圖是服務提供者暴露服務的主過程:
首先 ServiceConfig
類拿到對外提供服務的實際類 ref(如:HelloWorldImpl),然後通過 ProxyFactory
類的 getInvoker
方法使用 ref 生成一個 AbstractProxyInvoker
實例,到這一步就完成具體服務到 Invoker
的轉化。接下來就是 Invoker
轉換到 Exporter
的過程。
Dubbo 處理服務暴露的關鍵就在 Invoker
轉換到 Exporter
的過程,上圖中的紅色部分。下面我們以 Dubbo 和 RMI 這兩種典型協議的實現來進行說明:
Dubbo 的實現
Dubbo 協議的 Invoker
轉爲 Exporter
發生在 DubboProtocol
類的 export
方法,它主要是打開 socket 偵聽服務,並接收客戶端發來的各種請求,通訊細節由 Dubbo 自己實現。
RMI 的實現
RMI 協議的 Invoker
轉爲 Exporter
發生在 RmiProtocol
類的 export
方法,它通過 Spring 或 Dubbo 或 JDK 來實現 RMI 服務,通訊細節這一塊由 JDK 底層來實現,這就省了不少工作量。
滿眼都是 Invoker
由於 Invoker
是 Dubbo 領域模型中非常重要的一個概念,很多設計思路都是向它靠攏。這就使得 Invoker
滲透在整個實現代碼裏,對於剛開始接觸 Dubbo 的人,確實容易給搞混了。 下面我們用一個精簡的圖來說明最重要的兩種 Invoker
:服務提供 Invoker
和服務消費 Invoker
:
爲了更好的解釋上面這張圖,我們結合服務消費和提供者的代碼示例來進行說明:
服務消費者代碼:
public class DemoClientAction {
private DemoService demoService;
public void setDemoService(DemoService demoService) {
this.demoService = demoService;
}
public void start() {
String hello = demoService.sayHello("world" + i);
}
}
上面代碼中的 DemoService
就是上圖中服務消費端的 proxy,用戶代碼通過這個 proxy 調用其對應的 Invoker
[5],而該 Invoker
實現了真正的遠程服務調用。
服務提供者代碼:
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}
上面這個類會被封裝成爲一個 AbstractProxyInvoker
實例,並新生成一個 Exporter
實例。這樣當網絡通訊層收到一個請求後,會找到對應的 Exporter
實例,並調用它所對應的 AbstractProxyInvoker
實例,從而真正調用了服務提供者的代碼。Dubbo 裏還有一些其他的 Invoker
類,但上面兩種是最重要的。
導出服務入口
服務導出的入口方法是 ServiceBean 的 onApplicationEvent。onApplicationEvent 是一個事件響應方法,該方法會在收到 Spring 上下文刷新事件後執行服務導出操作。方法代碼如下:
ServiceBean 是 Dubbo 與 Spring 框架進行整合的關鍵,可以看做是兩個框架之間的橋樑。具有同樣作用的類還有 ReferenceBean。
檢查參數略
加載註冊中心URL列表
Dubbo 允許我們使用不同的協議導出服務,也允許我們向多個註冊中心註冊服務。Dubbo 在 doExportUrls 方法中對多協議,多註冊中心進行了支持。相關代碼如下:
private void doExportUrls() {
// 加載註冊中心鏈接
List<URL> registryURLs = loadRegistries(true);
// 遍歷 protocols,並在每個協議下導出服務
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
組裝provider URL
導出服務到本地
exportLocal()方法內部會把url協議替換爲injvm協議,然後通過自適應擴展機制調用InjvmProtocol的export方法在本地暴露服務。
InjvmProtocol 的 export 方法僅創建了一個 InjvmExporter,無其他邏輯
導出服務到遠程
生成Invoker實例
注意第518行 registryURL上加了一個export參數,參數的值就是providerURL,以便可以從registryURL中獲取providerURL
導出服務
例子中是調用DubboProtocol的export方法暴露服務
跟蹤方法最核心的一步就是創建服務端,開啓監聽端口,默認是使用nettyserver
註冊服務
registryFactory是一個SPI接口的代理類
根據registryURL的協議zookeeper,實際執行ZookeeperRegistryFactory的方法,ZookeeperTransporter也是一個SPI接口。默認是使用curator的客戶端。
通過curator客戶端創建zookeeper目錄