前言
忙的時候,會埋怨學習的時間太少,缺少個人的空間,於是會爭分奪秒的工作、學習。而一旦繁忙的時候過去,有時間了之後,整個人又會不自覺的陷入一種懶散的狀態中,時間也顯得不那麼重要了,隨便就可以浪費掉幾個小時。可見普通人的學習之路要主動地去克服掉很多阻礙,最主要的阻礙還是來自於自身,週期性的不想學習、不自覺的懶散、淺嘗輒止的態度、好高騖遠貪多的盲目...哎,學習之路,還是要時刻提醒自己,需勤勉致知。
閒話少敘,今天的學習目標是要儘量的瞭解清楚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了,感覺繁瑣且陌生。原始碼閱讀,還是要找到一個比較好的框架慢慢研讀下去,勿好高騖遠,勿心急氣躁。一時看不懂的地方,就多研究一下,實在不行則先放放,過兩天重看的時候就會有不一樣的靈感,一般都很容易就領悟了,如若還不行,上網看看大神們的解讀也能豁然開朗。
總之技術的提升沒有什麼捷徑,需要多學習多積累多思考。與各位共勉!