前言
本文 Dubbo 使用版本 2.7.5
Dubbo 通過使用 dubbo:service配置或 @service在解析完配置後進行服務暴露,供服務消費者消費。
Dubbo 的服務暴露有兩種:
- 遠程暴露
- 本地暴露
可以通過 scope顯式指定暴露方式:
- none 不暴露
- remote 遠程暴露
- local 本地暴露
服務暴露流程
下面是一個服務暴露的流程圖:
ProxyFactory 是動態代理,用來創建 Invoker 對象,實現代理使用 JavassistProxyFactory和 JdkProxyFactory。
Invoker 是一個服務對象實例,Dubbo 框架的實體域。它可以是一個本地的實現,一個遠程的實現或一個集群的實現,可以向它發起 Invoker 調用。
Protocol 是服務域,負責 Invoker 的生命週期管理,是 Invoker 暴露和引用的主要功能入口,對應該類的 export和 refer方法。
Exporter 是根據不同協議暴露 Invoker 進行封裝的類,它會根據不同的協議頭進行識別(比如:registry://和 dubbo://),調用對應 XXXProtocol的 export()方法。
從上圖中可以看到,Dubbo 中服務暴露分為兩個大步驟:第一步通過代理將服務實例轉換成 Invoker,這就是通過我們常用的反射實現。第二步將 Invoker 根據具體的協議轉換成 Exporter,這是就是我們要分析的核心。從這裡可以看到 Dubbo 服務對象都是圍繞 Invoker 進行工作。
遠程暴露
服務遠程暴露從字面上理解,就是將服務跨網絡進行遠程通信,並非同一 JVM 中的服務進行調用。服務最後都是轉換成 org.apache.dubbo.config.spring.ServiceBean,它的UML類圖:
ServiceBean繼承自 ServiceConfig,服務在 ServiceConfig#doExportUrls根據不同協議進行暴露。
通過獲取所有註冊中心實例(registryURLs)後,進行依次暴露,暴露操作在 doExportUrlsFor1Protocol中。
<code>private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, ListregistryURLs) { Map<string> map = new HashMap<string>(); // 配置信息存入 map ..... // 獲取服務URL String host = findConfigedHosts(protocolConfig, registryURLs, map); Integer port = findConfigedPorts(protocolConfig, name, map); URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map); ..... String scope = url.getParameter(SCOPE_KEY); // 如果 scope 配置為 none,則服務不進行暴露 if (!SCOPE_NONE.equalsIgnoreCase(scope)) { // 本地暴露 if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) { exportLocal(url); } // 遠程暴露 if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) { // 判斷是否有註冊中心 if (CollectionUtils.isNotEmpty(registryURLs)) { for (URL registryURL : registryURLs) { //if protocol is only injvm ,not register if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { continue; } url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY)); // 獲取監控URL URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL); if (monitorUrl != null) { // 追加監控上報地址,在攔截器上報數據 url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString()); } // 日誌打印 if (logger.isInfoEnabled()) { if (url.getParameter(REGISTER_KEY, true)) { logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); } else { logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); } } // For providers, this is used to enable custom proxy to generate invoker String proxy = url.getParameter(PROXY_KEY); if (StringUtils.isNotEmpty(proxy)) { registryURL = registryURL.addParameter(PROXY_KEY, proxy); } // 將服務對象轉換成 Invoker Invoker> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString())); DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); // 暴露服務,向註冊中心註冊服務,進入對應的 RegistryProtocol Exporter> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } else { // 沒有註冊中心時 if (logger.isInfoEnabled()) { logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); } Invoker> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url); DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); // 直接暴露服務 Exporter> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } /** * 存儲Dubbo服務的元數據,元數據可以存儲在遠端配置中心和本地,默認是存儲在本地 * @since 2.7.0 * ServiceData Store */ WritableMetadataService metadataService = WritableMetadataService.getExtension(url.getParameter(METADATA_KEY, DEFAULT_METADATA_STORAGE_TYPE)); if (metadataService != null) { metadataService.publishServiceDefinition(url); } } } this.urls.add(url);}/<string>/<string> /<code>
上面是代碼片段為暴露服務的核心,可以看到 scope 由三個值控制是否暴露和遠程或本地暴露,默認遠程和本地都暴露。在遠程調用中,分為使用註冊中心暴露和直接暴露(默認dubbo協議),它們之間的區別在url上:
- 無註冊中心:dubbo://192.168.3.19:20880/xxxx
- 有註冊中心:registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=provider&dubbo=2.0.2&export=dubbo://192.168.3.19:20880/xxxx
無註冊中心的直接暴露服務。有註冊中心的先創建註冊中心,再得到 export 的服務地址,然後暴露服務,當服務暴露成功後把服務元數據註冊到註冊中心。
代碼中 protocol#export會根據服務 url 的請求頭進行區分不同 XXXProtocol#export的邏輯,比如。目前 Dubbo 中有以下幾種:
本地暴露
同一個應用中,可能既要提供服務遠程暴露給其他應用引用,也要給自身提供引用。如果只提供遠程暴露的話,當自身應用需要引用自身的服務時,需要通過遠程通信訪問,那麼這大大浪費網絡資源。這是就需要用 injvm 協議暴露,就是我們所說的本地暴露,無需跨網絡遠程通信,可以更好的節省資源。通過上面代碼中,我們知道本地暴露調用的是 ServiceConfig#exportLocal方法。
本地暴露會指定 injvm 協議,並且 host 指定為本地 127.0.0.1和端口號為0。protocol.export 調用 InjvmProtocol#export 實現:
<code>@Override publicExporter /<code>export(Invoker invoker) throws RpcException { return new InjvmExporter (invoker, invoker.getUrl().getServiceKey(), exporterMap); }
export 中返回了 InjvmExporter 實例化對象。
<code>class InjvmExporterextends AbstractExporter /<code>{ private final String key; private final Map<string>> exporterMap; InjvmExporter(Invoker invoker, String key, Map<string>> exporterMap) { super(invoker); this.key = key; this.exporterMap = exporterMap; exporterMap.put(key, this); } @Override public void unexport() { super.unexport(); exporterMap.remove(key); }}/<string> /<string>
本地暴露就比較簡單,將 Invoker 直接保存在 InjvmExporter 的 exporterMap 中。
最後
本篇對 Dubbo 的服務暴露流程進行了分析,核心點就是開篇圖中的得到 Invoker 後轉化到 Export。
閱讀更多 Java技術虎 的文章