Dubbo原始碼學習之-服務匯出

張曾經發表於2019-08-04

前言

        忙的時候,會埋怨學習的時間太少,缺少個人的空間,於是會爭分奪秒的工作、學習。而一旦繁忙的時候過去,有時間了之後,整個人又會不自覺的陷入一種懶散的狀態中,時間也顯得不那麼重要了,隨便就可以浪費掉幾個小時。可見普通人的學習之路要主動地去克服掉很多阻礙,最主要的阻礙還是來自於自身,週期性的不想學習、不自覺的懶散、淺嘗輒止的態度、好高騖遠貪多的盲目...哎,學習之路,還是要時刻提醒自己,需勤勉致知。

        閒話少敘,今天的學習目標是要儘量的瞭解清楚Dubbo框架中的服務匯出功能,先附上Dubbo官網上的一個框架圖

本文要說的服務匯出,指的就是上圖中的動作1-register,即將配置檔案/類中服務提供者的資訊在註冊中心完成註冊,以便後續的消費者能發現服務,此外,開放NettyClient用於與消費者通訊。

 下面會從服務匯出的觸發時機、配置項準備、開放通訊client並註冊三部分進行服務匯出流程的瞭解。

一、服務匯出的觸發時機

        Dubbo是一個可以完全相容Spring的服務治理框架,相容性體現在何處?一是Dubbo的配置可以做到對Spring配置無侵入;二是Dubbo預設是隨著Spring容器的啟動而啟動的,不需要人為的控制。幫助Dubbo實現第二項的類有兩個:ReferenceBean和ServiceBean,這兩個類在Dubbo中擔任了連線Spring的橋樑的作用。與本文相關的類是ServiceBean,此類實現了ApplicationListener介面,這樣Spring框架在執行refresh方法完成容器初始化的時候,就會發布容器重新整理事件,呼叫ServiceBean的重寫方法onApplicationEvent,觸發服務匯出。

1 public void onApplicationEvent(ContextRefreshedEvent event) {
2         // 判斷是否已經匯出 或者是否不應該匯出
3         if (!isExported() && !isUnexported()) {
4             if (logger.isInfoEnabled()) {
5                 logger.info("The service ready on spring started. service: " + getInterface());
6             }
7             export();
8         }
9     }

二、配置項準備

        既然有了入口,就順著一路追溯下去。真正的export匯出方法,在ServiceConfig類中。此時需要說明一下Dubbo中的配置類與實際配置項之間的對應關係。此處以Dubbo的xml配置檔案為例。

<dubbo:service interface = "com.test.dubboservice.ITestService" ref = "testService" version="1.0"/>  對應 ServiceConfig類,

<dubbo:provider delay="-1" timeout="${DUBBO_TIMEOUT}" retries="0" token="test_demo"/>  對應 ProviderConfig類,此類與ServiceConfig同屬於AbstractServiceConfig的實現類

<dubbo:application name = "dubbo_provider"/> 對應ApplicationConfig類,

<dubbo:registry address = "zookeeper://192/168.16.218:10110" check="false" subscribe="false" register=""/> 對應RegistryConfig類,

<dubbo:protocol name = "dubbo"/>  對應ProtocolConfig類,此外監控中心對應的類是 MonitorConfig

播放完插曲,繼續往下追溯export方法 ServiceConfig中的 doExportUrls方法:

 1 private void doExportUrls() {
 2         // 1、Dubbo支援多協議多註冊中心
 3         List<URL> registryURLs = loadRegistries(true);
 4         ///2、多協議匯出服務,向多註冊中心註冊服務
 5         for (ProtocolConfig protocolConfig : protocols) {
 6             String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
 7             ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
 8             ApplicationModel.initProviderModel(pathKey, providerModel);
 9             doExportUrlsFor1Protocol(protocolConfig, registryURLs);
10         }
11     }

此方法分為兩步,第一步是獲取配置的註冊中心生成的URL,第二步是通過不同協議進行註冊。一步一步的看,先看loadRegistries方法。

1、loadRegistries(true)方法

 1 protected List<URL> loadRegistries(boolean provider) {
 2         // check && override if necessary
 3         List<URL> registryList = new ArrayList<URL>();
 4         // 此處registries型別為List<RegistryConfig>,是解析配置檔案時完成的注入,類似於Spring對配置檔案的解析
 5         if (CollectionUtils.isNotEmpty(registries)) {
 6             for (RegistryConfig config : registries) {
 7                 String address = config.getAddress();
 8                 if (StringUtils.isEmpty(address)) {
 9                     address = ANYHOST_VALUE;
10                 }
11                 // 判斷當前註冊中心的地址是否為可用地址,不可用地址格式為 N/A
12                 if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
13                     Map<String, String> map = new HashMap<String, String>();
14                     // !!!此方法極為重要,在拼接URL時經常會看到,是重點需要理解的方法
15                     appendParameters(map, application);
16                     appendParameters(map, config);
17                     map.put(PATH_KEY, RegistryService.class.getName());
18                     appendRuntimeParameters(map);
19                     // 如果註冊中心配置中沒有配置protocol,則預設用dubbo
20                     if (!map.containsKey(PROTOCOL_KEY)) {
21                         map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
22                     }
23                     List<URL> urls = UrlUtils.parseURLs(address, map);
24                     // 重新構建URL
25                     for (URL url : urls) {
26                         url = URLBuilder.from(url)
27                                 .addParameter(REGISTRY_KEY, url.getProtocol())
28                                 .setProtocol(REGISTRY_PROTOCOL)
29                                 .build();
30                         // 滿足兩個條件會往裡新增:1、是提供者且有配置註冊中心;2、不是提供者但是配置了訂閱
31                         if ((provider && url.getParameter(REGISTER_KEY, true))
32                                 || (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
33                             registryList.add(url);
34                         }
35                     }
36                 }
37             }
38         }
39         return registryList;
40     }

 此方法邏輯不難理解,先是判斷如果為空則設定address的預設值為127的本地地址,再是組裝URL引數,最後重新build一下,判斷是否新增,最終返回。只是有三個地方需要搞清楚,一個是appendParameters方法的原理及作用,一個是UrlUtils.parseURLs方法的原理,最後是26-29行程式碼的作用以及為什麼還要重新建立(直接將23行的urls返回不行嗎?)。下面一個個的來。

1.1 appendParameters方法

原始碼如下所示:

 1 // 第二個引數就是前面傳入的配置類 ApplicationConfig、RegistryConfig等
 2     protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
 3         if (config == null) {
 4             return;
 5         }
 6         // 通過反射得到所有的方法
 7         Method[] methods = config.getClass().getMethods();
 8         for (Method method : methods) {
 9             try {
10                 String name = method.getName();
11                 // 是get方法或is方法才進行賦值
12                 if (MethodUtils.isGetter(method)) {
13                     Parameter parameter = method.getAnnotation(Parameter.class);
14                     // 返回值是當前配置類,或者此方法有Parameter註解且excluded引數為true,此時不往map中賦值
15                     if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
16                         continue;
17                     }
18                     String key;
19                     if (parameter != null && parameter.key().length() > 0) {
20                         // 如果Parameter註解中的key不為空,則將它作為往map中put的key
21                         key = parameter.key();
22                     } else {
23                         // 否則擷取get/is後面的字串,轉成application.version格式的key
24                         key = calculatePropertyFromGetter(name);
25                     }
26                     // 通過反射獲取value
27                     Object value = method.invoke(config);
28                     String str = String.valueOf(value).trim();
29                     if (value != null && str.length() > 0) {
30                         if (parameter != null && parameter.escaped()) {
31                             // 轉成utf-8格式
32                             str = URL.encode(str);
33                         }
34                         // 註解中有可拼接配置,則在value前面拼接預設值
35                         if (parameter != null && parameter.append()) {
36                             String pre = parameters.get(DEFAULT_KEY + "." + key);
37                             if (pre != null && pre.length() > 0) {
38                                 str = pre + "," + str;
39                             }
40                             pre = parameters.get(key);
41                             if (pre != null && pre.length() > 0) {
42                                 str = pre + "," + str;
43                             }
44                         }
45                         // 如果有字首則在key上加字首
46                         if (prefix != null && prefix.length() > 0) {
47                             key = prefix + "." + key;
48                         }
49                         // 放入map中
50                         parameters.put(key, str);
51                     } else if (parameter != null && parameter.required()) {
52                         throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
53                     }
54                     // 如果是getParameters方法
55                 } else if ("getParameters".equals(name)
56                         && Modifier.isPublic(method.getModifiers())
57                         && method.getParameterTypes().length == 0
58                         && method.getReturnType() == Map.class) {
59                     Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
60                     if (map != null && map.size() > 0) {
61                         String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
62                         // 重新放入parameters中
63                         for (Map.Entry<String, String> entry : map.entrySet()) {
64                             parameters.put(pre + entry.getKey().replace('-', '.'), entry.getValue());
65                         }
66                     }
67                 }
68             } catch (Exception e) {
69                 throw new IllegalStateException(e.getMessage(), e);
70             }
71         }
72     }

此方法的作用就是獲取傳入Object的get/is方法,將get/is後面的屬性名跟返回的value值作為key-value放入map中,此外該方法還相容了Dubbo本身的配置項功能。

 1.2 UrlUtils.parseURLs方法

此方法最終呼叫了org.apache.dubbo.common.utils.UrlUtils#parseURL方法,由於程式碼較長,就不貼出來了。

方法中做的事情為:拆分address,如果配置了多個註冊中心地址,則每個地址對應一個URL,然後組裝URL的七大基本引數 protocol, username, password, host, port, path, parameters,最後new一個URL並返回。

1.3 26-29行程式碼的作用

將registry、protocol的值作為key、value放入parameters中,將protocol設定為registry,然後用新七項資訊建立一個新的URL,此時的URL就被標記為registry的URL了。

2、遍歷協議配置,用每個協議配置往註冊中心進行服務匯出

其中pathkey欄位,獲取到的就是要匯出的服務的介面全路徑名,然後將ProviderModel放入map中快取起來,最後再呼叫匯出方法。

三、服務匯出並註冊

此部分程式碼較長,邏輯複雜,故只貼一下關鍵地方的程式碼,需結合程式碼一起看。

1、服務匯出的觸發

在ServiceConfig類的上述doExportUrlsFor1Protocol方法的後面,進行了匯出的觸發。分析如下:

1 String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
2 Integer port = this.findConfigedPorts(protocolConfig, name, map);

上面2.1中獲取到的註冊中心URL用於提供host地址,在獲取到String protocol, String host, int port, String path, Map<String, String> parameters這五項資訊後,new一個URL。然後通過proxyFactory(通過SPI機制生成的代理類)來生成invoker物件,包裝成一個包裝類,然後呼叫通過SPI機制生成的protocol代理類的export方法進行匯出。

 1 if (CollectionUtils.isNotEmpty(registryURLs)) {
 2     for (URL registryURL : registryURLs) {
 3         // 若干無關程式碼
 4         // 通過代理工廠生成invoker
 5         Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
 6         // 用於持有invoker和ServiceConfig
 7         DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 8         // 進行服務匯出
 9         Exporter<?> exporter = protocol.export(wrapperInvoker);
10         exporters.add(exporter);
11     }
12 } else {
13     Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
14     DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
15 
16     Exporter<?> exporter = protocol.export(wrapperInvoker);
17     exporters.add(exporter);
18}

 

 Dubbo中一般都是這個套路,先將URL等準備好的物件轉換成一個Invoker,再將Invoker物件傳入目標方法完成特定功能。方便理解,可顧名思義,將Invoker看成一個呼叫體的抽象。

此處需要說明一點的是上述的if-else分支,if中的邏輯是當存在註冊中心時的處理方式,即向註冊中心註冊,並暴露服務供呼叫;而else中的邏輯,是不存在註冊中心時,只暴露服務。他們二者功能區別的實現,在於第5行跟第13行,getInvoker方法的第三個引數不同,一個是註冊中心的URL中帶有服務的URL,一個只是服務的URL。前者通過SPI的代理工廠類呼叫的是RegistryProtocol類,因為RegistryURL的protocol屬性為registry;而後者通過代理工廠類呼叫的是DubboProtocol等具體的協議類,因為此URL的protocol屬性是取得配置屬性,預設dubbo協議。由此可見,以URL為媒介的配置類與SPI機制結合時的靈活性是很大的。

2、RegistryProtocol中的export方法

 1 public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
 2         URL registryUrl = getRegistryUrl(originInvoker);
 3         // url to export locally
 4         URL providerUrl = getProviderUrl(originInvoker);
 5         //  subscription information to cover.
 6         final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
 7         final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
 8         overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
 9 
10         providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
11         //export invoker
12         // *** 匯出是指,建立可供客戶端通訊的nettyClient,並啟動
13         final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
14 
15         // url to registry
16         final Registry registry = getRegistry(originInvoker);
17         final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
18         ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
19                 registryUrl, registeredProviderUrl);
20         //to judge if we need to delay publish
21         boolean register = registeredProviderUrl.getParameter("register", true);
22         if (register) {
23             // *** 將服務註冊到註冊中心
24             register(registryUrl, registeredProviderUrl);
25             providerInvokerWrapper.setReg(true);
26         }
27 
28         // Deprecated! Subscribe to override rules in 2.6.x or before.
29         registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
30 
31         exporter.setRegisterUrl(registeredProviderUrl);
32         exporter.setSubscribeUrl(overrideSubscribeUrl);
33         //Ensure that a new exporter instance is returned every time export
34         return new DestroyableExporter<>(exporter);
35     }

此方法中最重要的兩個方法我用*號打了標記,其中前者最終執行的是DubboProtocol等類中的export方法,即啟動服務端的通訊Client,以進行後續的通訊呼叫,此方法下面再看。後者register方法的作用,即將服務註冊到註冊中心。不同的註冊中心,註冊的實現是不一樣的,比如如果用的是ZooKeeper註冊,則此處是呼叫ZooKeeper的相關API,在對應路徑上建立節點,一個節點對應一個服務。

3、DubboProtocol中的export方法

此處以預設協議DubboProtocol為例,看它的export方法。

 1  public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
 2         URL url = invoker.getUrl();
 3 
 4         // export service.
 5         String key = serviceKey(url);
 6         DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
 7         exporterMap.put(key, exporter);
 8 
 9         //export an stub service for dispatching event
10         Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
11         Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
12         if (isStubSupportEvent && !isCallbackservice) {
13             String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
14             if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
15                 if (logger.isWarnEnabled()) {
16                     logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
17                             "], has set stubproxy support event ,but no stub methods founded."));
18                 }
19 
20             } else {
21                 stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
22             }
23         }
24         // 開啟伺服器,為什麼是開啟伺服器?因為開啟了服務端Client,消費者就可以與其進行通訊的互動了
25         openServer(url);
26         optimizeSerialization(url);//序列化的優化
27 
28         return exporter;
29     }

此方法最主要的就是openServer方法,追蹤下去你會發現,最終建立了某Client並啟動(預設是NettyClient,此外還有MinaClient、GrizzlyClient),等待訊息的傳入。

總結

        Dubbo中服務匯出的流程,基本如上所述。3.2和3.3中由於有多層類呼叫程式碼量較多,所以未詳細跟進流程,不過相信以道友們紮實的原始碼閱讀功底,應該不難追溯下去,追溯到最後面就是封裝的與其他元件互動的api了,感覺繁瑣且陌生。原始碼閱讀,還是要找到一個比較好的框架慢慢研讀下去,勿好高騖遠,勿心急氣躁。一時看不懂的地方,就多研究一下,實在不行則先放放,過兩天重看的時候就會有不一樣的靈感,一般都很容易就領悟了,如若還不行,上網看看大神們的解讀也能豁然開朗。

        總之技術的提升沒有什麼捷徑,需要多學習多積累多思考。與各位共勉!

 

相關文章