Dubbo的服務暴露是一個重要的特性,瞭解其機制很重要。之前有很多人寫了有關的源代碼分析,在本文中不再重新分析。官方文檔中的一篇寫的就很好,本文主要是有關內容進行補充與總結。 傳送門: "服務導出" 為什麼要服務暴露 服務暴露分為遠程暴露和本地暴露。在遠程服務暴露中會將服務信息上傳到註冊中心。這時客 ...
Dubbo的服務暴露是一個重要的特性,瞭解其機制很重要。之前有很多人寫了有關的源代碼分析,在本文中不再重新分析。官方文檔中的一篇寫的就很好,本文主要是有關內容進行補充與總結。
傳送門:服務導出
為什麼要服務暴露
服務暴露分為遠程暴露和本地暴露。在遠程服務暴露中會將服務信息上傳到註冊中心。這時客戶端要調用某個服務時會從註冊中心找到該服務的遠程地址等信息。然後客戶端根據這個地址進行遠程調用。服務端接收到遠程調用請求後會最終調用getInvoker()
方法進行查找對用的invoker
。在getInvoker()
方法中會從一個HashMap中進行查找,如果在這個Map中查找不到就會拋出異常。在遠程服務暴露中,會按照規則將實例Invoker存儲在HashMap中,其中Key名包含埠、介面名、介面版本和介面分組。所以進行服務暴露很重要。
本地服務暴露是暴露在JVM中,不需要遠程通信。Dubbo會預設把遠程服務用injvm協議再暴露一份。
為什麼會有本地服務暴露
在Dubbo中,一個服務可以即是provider,又是Consumer,因此就存在它自己調用自己服務的時候,如果再通過網路去訪問,那麼就是捨近求遠,因此有了本地暴露服務這個設計。消費者可以直接消費同一個JVM內部的服務,避免了跨網路進行遠程通信。
服務暴露起點
我們會通過XML或註解的方式來指定要暴露的服務。l例子如下:
<bean id=“xxxService” class=“com.xxx.XxxServiceImpl” />
<!-- 增加暴露遠程服務配置 -->
<dubbo:service interface=“com.xxx.XxxService” ref=“xxxService” />
這時會創建出一個ServiceBean
對象。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, BeanNameAware,
ApplicationEventPublisherAware
public class ServiceConfig<T> extends ServiceConfigBase<T>
public abstract class ServiceConfigBase<T> extends AbstractServiceConfig {
private static final long serialVersionUID = 3033787999037024738L;
protected String interfaceName;
//要暴露服務類的介面類
protected Class<?> interfaceClass;
//實現類引用
protected T ref;
//服務名
protected String path;
protected ProviderConfig provider;
protected String providerIds;
protected volatile String generic;
protected ServiceMetadata serviceMetadata;
ServiceBean和Spring有關,它繼承了InitializingBean和ApplicationEvent。在Bean初始化完成後會調用InitializingBean.afterPropertiesSet方法來執行服務暴露的準備工作。在Spring的context完成初始化後,會觸發ApplicationEventListener事件進行服務暴露,會執行onApplicationEvent
方法。這時服務服務暴露就開始了。
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否有延遲導出 && 是否已導出 && 是不是已被取消導出
if (isDelay() && !isExported() && !isUnexported()) {
// 導出服務
export();
}
}
整體流程
Dubbo真正的服務暴露入口是ServiceConfig#doExport()
方法。首先ServiceConfig類拿到對外提供服務的實際類ref,然後通過ProxyFactory
類的getInvoker
方法使用ref生成一個AbstractProxyInvoker
實例,到這一步就完成具體服務到Invoker
的轉化。然後就是把Invoker
通過具體的協議(比如Bubbo)轉化成Exporter
。
參考了簡書肥朝的一篇文章: dubbo源碼解析-服務暴露原理
- 調用ServiceConfig中的
export
方法,對配置進行檢查與更新,例如provider是否為空,註冊中心是否為空,protocols是否為空。然後檢測是否應該暴露,如果不應該暴露,則直接結束;然後檢測是否配置了延遲載入,如果是,則使用定時器來實現延遲載入的目的。 - 調用
expoer
方法裡面的doExport()
方法。通過exported
變數判斷是否暴露,如果為true,直接返回;否則先設置為true,然後執行doExportUrls()
方法。 - 調用
doExportUrls()
方法,先通過loadRegistries
載入註冊中心鏈接,然後遍歷ProtocolConfig集合,它是用戶指定的協議集合(比如Dubbo、REST),在裡面執行doExportUrlsFor1Protocol(protocolConfig, registryURLs)
方法。 - 執行
doExportUrlsFor1Protocol(protocolConfig, registryURLs)
方法,剛開始是組裝URL。(1)它把metrices、application、module、provider、protocol等所有配置都放入到map中,(2) 通過反射獲取interfaceClass的方法列表,先做簽名校驗,判斷該服務是否有配置的方法存在,然後該方法簽名是否有這個參數存在,都核對成功才將method的配置加入到map中;(3)將範化調用、版本號、method或者methods、token等信息加入到map;(4)獲取服務暴露地址和埠號,利用map內數據組裝成URL。 - 然後根據url中的scope參數決定服務導出方式。
scope=none
,不導出服務;scope!=remote
,導出到本地;scope!=local
,導出到遠程。 - 執行
proxyFactory.getInvoker(ref, (Class)interfaceClass, url)
方法,來獲取invoker,Dubbo預設的ProxyFactory實現類是JavasistProxyFacoty
方法。
這時,第一大步服務轉化成Invoker已經完成,然後執行第二大步Invoker轉化成Exporter
。
先介紹本地服務的暴露機制
- 調用
exportLocal(url)
。首先判斷URL的協議頭如果等於injvm,說明已經導出到本地了,無需再次導出。如果不是,則創建一個新的URL並將協議頭設置為injvm,並另外設置主機名和埠。然後創建invoker,並調用InjvmProtocal
的export()
方法暴露服務。 InjvmProtocol
的export
方法僅創建一個InjvmExporter
,無其他邏輯。
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
super(invoker);
this.key = key;
this.exporterMap = exporterMap;
exporterMap.put(key, this);
}
然後介紹遠程服務暴露
如果有註冊中心,服務暴露後需要向註冊中心註冊服務信息
如果沒有註冊中心,直接暴露服務。
有註冊中心的暴露
- 調用
doLocalExport()
方法暴露服務。 - 向註冊中心註冊服務
- 向註冊中心進行訂閱ovrride數據
- 創建並返回
DestroyableExporter
。
調用doLocalExport()
方法暴露服務
- 首先根據
invoker
得到key,從bounds
緩存變數中嘗試獲取exporter
,如果獲取不到,則調用Protocal的export
方法,預設的協議是dubbo,所以調用的DubboProtocol的export方法。 - 調用DubboProtocol的export方法,首先從invoker中獲取對用的URL,然後根據服務組名、服務名、服務版本號和埠組成key,然後創建一個DubboExporter。
// demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880
String key = serviceKey(url);
// 創建 DubboExporter
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
// 將 <key, exporter> 鍵值對放入緩存中
exporterMap.put(key, exporter);
- 調用
openServer(url)
方法。首先根據url獲取host:port
,用於標識當前的伺服器實例。在同一臺機器上,同一個埠僅允許啟動一個伺服器實例。若某個埠上已有伺服器實例,此時調用reset
方法重置一些伺服器的配置;如果沒有則調用createServer(url)
方法創建一個伺服器實例。 - 調用
createServer(url)
方法,有三個核心的邏輯,首先檢測是否存在server參數所代表的Transporter拓展,即網路傳輸方式(如Netty, Mina),如果沒有,則拋出異常;然後通過Exchangers.bind(url, requestHandler)
方法創建伺服器實例;最後是檢測是否支持client參數所表示的Transporter拓展,不存在也是拋出異常。 - 調用
Exchangers.bind(url, requestHandler)
方法,裡面又調用了下麵幾個方法。只保留了主要邏輯部分。
getExchanger(url).bind(url, handler);
public static Exchanger getExchanger(URL url) {
String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);
return getExchanger(type);
}
public static Exchanger getExchanger(String type) {
return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
}
Exchange層是為了封裝請求/響應模式,例如:把同步請求轉化為非同步請求。預設的擴展點實現類是HeaderExchanger
。
- 調用HeaderExchanger的bind方法
public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handlers == null || handlers.length == 0) {
throw new IllegalArgumentException("handlers == null");
}
ChannelHandler handler;
if (handlers.length == 1) {
handler = handlers[0];
} else {
// 如果 handlers 元素數量大於1,則創建 ChannelHandler 分發器
handler = new ChannelHandlerDispatcher(handlers);
}
// 獲取自適應 Transporter 實例,並調用實例方法
return getTransporter().bind(url, handler);
}
- 根據自適應擴展機制動態創建一個Transporter,Dubbo預設是NettyTransporter。
- 調用
NettyTransporter.bind(URL, ChannelHandler)
方法。創建一個NettyServer
實例。 - 調用
NettyServer.doOPen()
方法,伺服器被開啟,服務也被暴露出來了。