Dubbo 原始碼分析 - 服務匯出
1.服務匯出過程
本篇文章,我們來研究一下 Dubbo 匯出服務的過程。Dubbo 服務匯出過程始於 Spring 容器釋出重新整理事件,Dubbo 在接收到事件後,會立即執行服務匯出邏輯。整個邏輯大致可分為三個部分,第一是前置工作,主要用於檢查引數,組裝 URL。第二是匯出服務,包含匯出服務到本地 (JVM),和匯出服務到遠端兩個過程。第三是向註冊中心註冊服務,用於服務發現。本篇文章將會對這三個部分程式碼進行詳細的分析,在分析之前,我們先來了解一下服務的匯出過程。
Dubbo 支援兩種服務匯出方式,分別延遲匯出和立即匯出。延遲匯出的入口是 ServiceBean 的 afterPropertiesSet 方法,立即匯出的入口是 ServiceBean 的 onApplicationEvent 方法。本文打算分析服務延遲匯出過程,因此不會分析 afterPropertiesSet 方法。下面從 onApplicationEvent 方法說起,該方法收到 Spring 容器的重新整理事件後,會呼叫 export 方法執行服務匯出操作。服務匯出之前,要進行對一系列的配置進行檢查,以及生成 URL。準備工作做完,隨後開始匯出服務。首先匯出到本地,然後再匯出到遠端。匯出到本地就是將服務匯出到 JVM 中,此過程比較簡單。匯出到遠端的過程則要複雜的多,以 dubbo 協議為例,DubboProtocol 類的 export 方法將會被呼叫。該方法主要用於建立 Exporter 和 ExchangeServer。ExchangeServer 本身並不具備通訊能力,需要藉助更底層的 Server 實現通訊功能。因此,在建立 ExchangeServer 例項時,需要先建立 NettyServer 或者 MinaServer 例項,並將例項作為引數傳給 ExchangeServer 實現類的構造方法。ExchangeServer 例項建立完成後,匯出服務到遠端的過程也就接近尾聲了。服務匯出結束後,服務消費者即可通過直聯的方式消費服務。當然,一般我們不會使用直聯的方式消費服務。所以,在服務匯出結束後,緊接著要做的事情是向註冊中心註冊服務。此時,客戶端即可從註冊中心發現服務。
以上就是 Dubbo 服務匯出的過程,比較複雜。下面開始分析原始碼,從原始碼的角度展現整個過程。
2.原始碼分析
一場 Dubbo 原始碼分析的馬拉松比賽即將開始,現在我們站在賽道的起點進行熱身準備。本次比賽的起點位置位於 ServiceBean 的 onApplicationEvent 方法處。好了,發令槍響了,我將和一些朋友從 onApplicationEvent 方法處出發,探索 Dubbo 服務匯出的全過程。下面我們來看一下 onApplicationEvent 方法的原始碼。
1 2 3 4 5 6 7 |
public void onApplicationEvent(ContextRefreshedEvent event) { // 是否有延遲匯出 && 是否已匯出 && 是不是已被取消匯出 if (isDelay() && !isExported() && !isUnexported()) { // 匯出服務 export(); } } |
onApplicationEvent 是一個事件響應方法,該方法會在收到 Spring 上下文重新整理事件後執行。這個方法首先會根據條件決定是否匯出服務,比如有些服務設定了延時匯出,那麼此時就不應該在此處匯出。還有一些服務已經被匯出了,或者當前服務被取消匯出了,此時也不能再次匯出相關服務。注意這裡的 isDelay 方法,這個方法字面意思是“是否延遲匯出服務”,返回 true 表示延遲匯出,false 表示不延遲匯出。但是該方法真實意思卻並非如此,當方法返回 true 時,表示無需延遲匯出。返回 false 時,表示需要延遲匯出。與字面意思恰恰相反,讓人覺得很奇怪。下面我們來看一下這個方法的邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 |
// -☆- ServiceBean private boolean isDelay() { // 獲取 delay Integer delay = getDelay(); ProviderConfig provider = getProvider(); if (delay == null && provider != null) { // 如果前面獲取的 delay 為空,這裡繼續獲取 delay = provider.getDelay(); } // 判斷 delay 是否為空,或者等於 -1 return supportedApplicationListener && (delay == null || delay == -1); } |
暫時忽略 supportedApplicationListener 這個條件,當 delay 為空,或者等於-1時,該方法返回 true,而不是 false。這個方法的返回值讓人有點困惑,因此我重構了該方法的程式碼,並給 Dubbo 提了一個 Pull Request,最終這個 PR 被合到了 Dubbo 主分支中。詳細請參見 Dubbo #2686。
現在解釋一下 supportedApplicationListener 變數含義,該變數用於表示當前的 Spring 容器是否支援 ApplicationListener,這個值初始為 false。在 Spring 容器將自己設定到 ServiceBean 中時,ServiceBean 的 setApplicationContext 方法會檢測 Spring 容器是否支援 ApplicationListener。若支援,則將 supportedApplicationListener 置為 true。程式碼就不分析了,大家自行查閱瞭解。
ServiceBean 是 Dubbo 與 Spring 框架進行整合的關鍵,可以看做是兩個框架之間的橋樑。具有同樣作用的類還有 ReferenceBean。ServiceBean 實現了 Spring 的一些拓展介面,有 FactoryBean、ApplicationContextAware、ApplicationListener、DisposableBean 和 BeanNameAware。這些介面我在 Spring 原始碼分析系列文章中介紹過,大家可以參考一下,這裡就不贅述了。
現在我們知道了 Dubbo 服務匯出過程的起點。那麼接下來,我們快馬加鞭,繼續進行比賽。賽程預告,下一站是“服務匯出的前置工作”。
2.1 前置工作
前置工作主要包含兩個部分,分別是配置檢查,以及 URL 裝配。在匯出服務之前,Dubbo 需要檢查使用者的配置是否合理,或者為使用者補充預設配置。配置檢查完成後,接下來需要根據這些配置組裝 URL。在 Dubbo 中,URL 的作用十分重要。Dubbo 使用 URL 作為配置載體,所有的拓展點都是通過 URL 獲取配置。這一點,官方文件中有所說明。
採用 URL 作為配置資訊的統一格式,所有擴充套件點都通過傳遞 URL 攜帶配置資訊。
接下來,我們先來分析配置檢查部分的原始碼,隨後再來分析 URL 組裝部分的原始碼。
2.1.1 檢查配置
本節我們接著前面的原始碼向下分析,前面說過 onApplicationEvent 方法在經過一些判斷後,會決定是否呼叫 export 方法匯出服務。那麼下面我們從 export 方法開始進行分析,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public synchronized void export() { if (provider != null) { // 獲取 export 和 delay 配置 if (export == null) { export = provider.getExport(); } if (delay == null) { delay = provider.getDelay(); } } // 如果 export 為 false,則不匯出服務 if (export != null && !export) { return; } if (delay != null && delay > 0) { // delay > 0,延時匯出服務 delayExportExecutor.schedule(new Runnable() { @Override public void run() { doExport(); } }, delay, TimeUnit.MILLISECONDS); } else { // 立即匯出服務 doExport(); } } |
export 對兩個配置進行了檢查,並配置執行相應的動作。首先是 export,這個配置決定了是否匯出服務。有時候我們只是想本地啟動服務進行一些除錯工作,這個時候我們並不希望把本地啟動的服務暴露出去給別人呼叫。此時,我們就可以通過配置 export 禁止服務匯出,比如:
1 |
<dubbo:provider export="false" /> |
delay 見名知意了,用於延遲匯出服務。下面,我們繼續分析原始碼,這次要分析的是 doExport 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
protected synchronized void doExport() { if (unexported) { throw new IllegalStateException("Already unexported!"); } if (exported) { return; } exported = true; // 檢測 interfaceName 是否合法 if (interfaceName == null || interfaceName.length() == 0) { throw new IllegalStateException("interface not allow null!"); } // 檢測 provider 是否為空,為空則新建一個,並通過系統變數為其初始化 checkDefault(); // 下面幾個 if 語句用於檢測 provider、application 等核心配置類物件是否為空, // 若為空,則嘗試從其他配置類物件中獲取相應的例項。 if (provider != null) { if (application == null) { application = provider.getApplication(); } if (module == null) { module = provider.getModule(); } if (registries == null) {...} if (monitor == null) {...} if (protocols == null) {...} } if (module != null) { if (registries == null) { registries = module.getRegistries(); } if (monitor == null) {...} } if (application != null) { if (registries == null) { registries = application.getRegistries(); } if (monitor == null) {...} } // 檢測 ref 是否泛化服務型別 if (ref instanceof GenericService) { // 設定 interfaceClass 為 GenericService.class interfaceClass = GenericService.class; if (StringUtils.isEmpty(generic)) { // 設定 generic = "true" generic = Boolean.TRUE.toString(); } } else { // ref 非 GenericService 型別 try { interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() .getContextClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } // 對 interfaceClass,以及 <dubbo:method> 必要欄位進行檢查 checkInterfaceAndMethods(interfaceClass, methods); // 對 ref 合法性進行檢測 checkRef(); // 設定 generic = "false" generic = Boolean.FALSE.toString(); } // local 屬性 Dubbo 官方文件中沒有說明,不過 local 和 stub 在功能應該是一致的,用於配置本地存根 if (local != null) { if ("true".equals(local)) { local = interfaceName + "Local"; } Class<?> localClass; try { // 獲取本地存根類 localClass = ClassHelper.forNameWithThreadContextClassLoader(local); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } // 檢測本地存根類是否可賦值給介面類,若不可賦值則會丟擲異常,提醒使用者本地存根類型別不合法 if (!interfaceClass.isAssignableFrom(localClass)) { throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName); } } // stub 和 local 均用於配置本地存根 if (stub != null) { // 此處的程式碼和上一個 if 分支的程式碼基本一致,這裡省略了 } // 檢測各種物件是否為空,為空則新建,或者丟擲異常 checkApplication(); checkRegistry(); checkProtocol(); appendProperties(this); checkStubAndMock(interfaceClass); if (path == null || path.length() == 0) { path = interfaceName; } // 匯出服務 doExportUrls(); // ProviderModel 表示服務提供者模型,此物件中儲存了和服務提供者相關的資訊。 // 比如服務的配置資訊,服務例項等。每個被匯出的服務對應一個 ProviderModel。 // ApplicationModel 持有所有的 ProviderModel。 ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref); ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel); } |
以上就是配置檢查的相關分析,程式碼比較多,需要大家耐心看一下。下面對配置檢查的邏輯進行簡單的總結,如下:
- 檢測 <dubbo:service> 標籤的 interface 屬性合法性,不合法則丟擲異常
- 檢測 ProviderConfig、ApplicationConfig 等核心配置類物件是否為空,若為空,則嘗試從其他配置類物件中獲取相應的例項。
- 檢測並處理泛化服務和普通服務類
- 檢測本地存根配置,並進行相應的處理
- 對 ApplicationConfig、RegistryConfig 等配置類進行檢測,為空則嘗試建立,若無法建立則丟擲異常
配置檢查並非本文重點,因此我不打算對 doExport 方法所呼叫的方法進行分析(doExportUrls 方法除外)。在這些方法中,除了 appendProperties 方法稍微複雜一些,其他方法都還好。因此,大家可自行進行分析。好了,其他的就不多說了,繼續向下分析。
2.1.2 多協議多註冊中心匯出服務
Dubbo 允許我們使用不同的協議匯出服務,也允許我們向多個註冊中心註冊服務。Dubbo 在 doExportUrls 方法中對多協議,多註冊中心進行了支援。相關程式碼如下:
1 2 3 4 5 6 7 8 |
private void doExportUrls() { // 載入註冊中心連結 List<URL> registryURLs = loadRegistries(true); // 遍歷 protocols,匯出每個服務 for (ProtocolConfig protocolConfig : protocols) { doExportUrlsFor1Protocol(protocolConfig, registryURLs); } } |
上面程式碼比較簡單,首先是通過 loadRegistries 載入註冊中心連結,然後再遍歷 ProtocolConfig 集合匯出每個服務。並在匯出服務的過程中,將服務註冊到註冊中心處。下面,我們先來看一下 loadRegistries 方法的邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
protected List<URL> loadRegistries(boolean provider) { // 檢測是否存在註冊中心配置類,不存在則丟擲異常 checkRegistry(); List<URL> registryList = new ArrayList<URL>(); if (registries != null && !registries.isEmpty()) { for (RegistryConfig config : registries) { String address = config.getAddress(); if (address == null || address.length() == 0) { // 若 address 為空,則將其設為 0.0.0.0 address = Constants.ANYHOST_VALUE; } // 從系統屬性中載入註冊中心地址 String sysaddress = System.getProperty("dubbo.registry.address"); if (sysaddress != null && sysaddress.length() > 0) { address = sysaddress; } // 判斷 address 是否合法 if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) { Map<String, String> map = new HashMap<String, String>(); // 新增 ApplicationConfig 中的欄位資訊到 map 中 appendParameters(map, application); // 新增 RegistryConfig 欄位資訊到 map 中 appendParameters(map, config); map.put("path", RegistryService.class.getName()); map.put("dubbo", Version.getProtocolVersion()); map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); if (ConfigUtils.getPid() > 0) { map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); } if (!map.containsKey("protocol")) { if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) { map.put("protocol", "remote"); } else { map.put("protocol", "dubbo"); } } // 解析得到 URL 列表,address 可能包含多個註冊中心 ip, // 因此解析得到的是一個 URL 列表 List<URL> urls = UrlUtils.parseURLs(address, map); for (URL url : urls) { url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol()); // 將 URL 協議頭設定為 registry url = url.setProtocol(Constants.REGISTRY_PROTOCOL); // 通過判斷條件,決定是否新增 url 到 registryList 中,條件如下: // (服務提供者 && register = true 或 null) // || (非服務提供者 && subscribe = true 或 null) if ((provider && url.getParameter(Constants.REGISTER_KEY, true)) || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) { registryList.add(url); } } } } } return registryList; } |
上面程式碼不是很複雜,包含如下邏輯:
- 檢測是否存在註冊中心配置類,不存在則丟擲異常
- 構建引數對映集合,也就是 map
- 構建註冊中心連結列表
- 遍歷連結列表,並根據條件決定是否將其新增到 registryList 中
關於多協議多註冊中心匯出服務就先分析到這,程式碼不是很多,就不過多敘述了。接下來分析 URL 組裝過程。
2.1.3 組裝 URL
配置檢查完畢後,緊接著要做的事情是根據配置,以及其他一些資訊組裝 URL。前面說過,URL 是 Dubbo 配置的載體,通過 URL 可讓 Dubbo 的各種配置在各個模組之間傳遞。URL 之於 Dubbo,猶如水之於魚,非常重要。大家在閱讀 Dubbo 服務匯出相關原始碼的過程中,要注意 URL 內容的變化。既然 URL 如此重要,那麼下面我們來了解一下 URL 組裝的過程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { String name = protocolConfig.getName(); // 如果協議名為空,或空串,則將協議名變數設定為 dubbo if (name == null || name.length() == 0) { name = "dubbo"; } Map<String, String> map = new HashMap<String, String>(); // 新增 side、版本、時間戳以及程序號等資訊到 map 中 map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE); map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion()); map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); if (ConfigUtils.getPid() > 0) { map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); } // 通過反射將物件的欄位資訊到 map 中 appendParameters(map, application); appendParameters(map, module); appendParameters(map, provider, Constants.DEFAULT_KEY); appendParameters(map, protocolConfig); appendParameters(map, this); // methods 為 MethodConfig 集合,MethodConfig 中儲存了 <dubbo:method> 標籤的配置資訊 if (methods != null && !methods.isEmpty()) { // 這段程式碼用於新增 Callback 配置到 map 中,程式碼太長,待會單獨分析 } // 檢測 generic 是否為 "true",並根據檢測結果向 map 中新增不同的資訊 if (ProtocolUtils.isGeneric(generic)) { map.put(Constants.GENERIC_KEY, generic); map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); } else { String revision = Version.getVersion(interfaceClass, version); if (revision != null && revision.length() > 0) { map.put("revision", revision); } // 為介面生成包裹類 Wrapper,Wrapper 中包含了介面的詳細資訊,比如介面方法名陣列,欄位資訊等 String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); // 新增方法名到 map 中,如果包含多個方法名,則用逗號隔開,比如 method = init,destroy if (methods.length == 0) { logger.warn("NO method found in service interface ..."); map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); } else { // 將逗號作為分隔符連線方法名,並將連線後的字串放入 map 中 map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ",")); } } // 新增 token 到 map 中 if (!ConfigUtils.isEmpty(token)) { if (ConfigUtils.isDefault(token)) { map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString()); } else { map.put(Constants.TOKEN_KEY, token); } } // 判斷協議名是否為 injvm if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) { protocolConfig.setRegister(false); map.put("notify", "false"); } // 獲取上下文路徑 String contextPath = protocolConfig.getContextpath(); if ((contextPath == null || contextPath.length() == 0) && provider != null) { contextPath = provider.getContextpath(); } // 獲取 host 和 port String host = this.findConfigedHosts(protocolConfig, registryURLs, map); Integer port = this.findConfigedPorts(protocolConfig, name, map); // 組裝 URL URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map); // 省略無關程式碼 } |
上面的程式碼首先是將一些資訊,比如版本、時間戳、方法名以及各種配置物件的欄位資訊放入到 map 中,map 中的內容將作為 URL 的查詢字串。構建好 map 後,緊接著是獲取上下文路徑、主機名以及埠號等資訊。最後將 map 和主機名等資料傳給 URL 構造方法建立 URL 物件。需要注意的是,這裡出現的 URL 並非 java.net.URL,而是 com.alibaba.dubbo.common.URL。
上面省略了一段程式碼,這裡簡單分析一下。這段程式碼用於檢測 <dubbo:argument> 標籤中的配置資訊,並將相關配置新增到 map 中。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { // ... // methods 為 MethodConfig 集合,MethodConfig 中儲存了 <dubbo:method> 標籤的配置資訊 if (methods != null && !methods.isEmpty()) { for (MethodConfig method : methods) { // 新增 MethodConfig 物件的欄位資訊到 map 中,鍵 = 方法名.屬性名。 // 比如儲存 <dubbo:method name="sayHello" retries="2"> 對應的 MethodConfig, // 鍵 = sayHello.retries,map = {"sayHello.retries": 2, "xxx": "yyy"} appendParameters(map, method, method.getName()); String retryKey = method.getName() + ".retry"; if (map.containsKey(retryKey)) { String retryValue = map.remove(retryKey); // 檢測 MethodConfig retry 是否為 false,若是,則設定重試次數為0 if ("false".equals(retryValue)) { map.put(method.getName() + ".retries", "0"); } } // 獲取 ArgumentConfig 列表 List<ArgumentConfig> arguments = method.getArguments(); if (arguments != null && !arguments.isEmpty()) { for (ArgumentConfig argument : arguments) { // 檢測 type 屬性是否為空,或者空串(分支1 ⭐️) if (argument.getType() != null && argument.getType().length() > 0) { Method[] methods = interfaceClass.getMethods(); if (methods != null && methods.length > 0) { for (int i = 0; i < methods.length; i++) { String methodName = methods[i].getName(); // 比對方法名,查詢目標方法 if (methodName.equals(method.getName())) { Class<?>[] argtypes = methods[i].getParameterTypes(); if (argument.getIndex() != -1) { // 檢測 ArgumentConfig 中的 type 屬性與方法引數列表 // 中的引數名稱是否一致,不一致則丟擲異常(分支2 ⭐️) if (argtypes[argument.getIndex()].getName().equals(argument.getType())) { // 新增 ArgumentConfig 欄位資訊到 map 中, // 鍵字首 = 方法名.index,比如: // map = {"sayHello.3": true} appendParameters(map, argument, method.getName() + "." + argument.getIndex()); } else { throw new IllegalArgumentException("argument config error: ..."); } } else { // 分支3 ⭐️ for (int j = 0; j < argtypes.length; j++) { Class<?> argclazz = argtypes[j]; // 從引數型別列表中查詢型別名稱為 argument.type 的引數 if (argclazz.getName().equals(argument.getType())) { appendParameters(map, argument, method.getName() + "." + j); if (argument.getIndex() != -1 && argument.getIndex() != j) { throw new IllegalArgumentException("argument config error: ..."); } } } } } } } // 使用者未配置 type 屬性,但配置了 index 屬性,且 index != -1 } else if (argument.getIndex() != -1) { // 分支4 ⭐️ // 新增 ArgumentConfig 欄位資訊到 map 中 appendParameters(map, argument, method.getName() + "." + argument.getIndex()); } else { throw new IllegalArgumentException("argument config must set index or type"); } } } } } // ... } |
上面這段程式碼 for 迴圈和 if else 分支巢狀太多,導致層次太深,不利於閱讀,需要耐心看一下。大家在看這段程式碼時,注意把幾個重要的條件分支找出來。只要理解了這幾個分支的意圖,就可以弄懂這段程式碼。我在上面程式碼中用⭐️符號標識出了4個重要的分支,下面用虛擬碼解釋一下這幾個分支的含義。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 獲取 ArgumentConfig 列表 for (遍歷 ArgumentConfig 列表) { if (type 不為 null,也不為空串) { // 分支1 1. 通過反射獲取 interfaceClass 的方法列表 for (遍歷方法列表) { 1. 比對方法名,查詢目標方法 2. 通過反射獲取目標方法的引數型別陣列 argtypes if (index != -1) { // 分支2 1. 從 argtypes 陣列中獲取下標 index 處的元素 argType 2. 檢測 argType 的名稱與 ArgumentConfig 中的 type 屬性是否一致 3. 新增 ArgumentConfig 欄位資訊到 map 中,或丟擲異常 } else { // 分支3 1. 遍歷引數型別陣列 argtypes,查詢 argument.type 型別的引數 2. 新增 ArgumentConfig 欄位資訊到 map 中 } } } else if (index != -1) { // 分支4 1. 新增 ArgumentConfig 欄位資訊到 map 中 } } |
在本節分析的原始碼中,appendParameters 這個方法出現的次數比較多,該方法用於將物件欄位資訊新增到 map 中。實現上則是通過反射獲取目標物件的 getter 方法,並呼叫該方法獲取屬性值。然後再通過 getter 方法名解析出屬性名,比如從方法名 getName 中可解析出屬性 name。如果使用者傳入了屬性名字首,此時需要將屬性名加入字首內容。最後將 <屬性名,屬性值> 鍵值對存入到 map 中就行了。限於篇幅原因,這裡就不分析 appendParameters 方法的原始碼了,大家請自行分析。
2.2 匯出 Dubbo 服務
前置工作做完,接下來就可以進行服務匯出工作。服務匯出,分為匯出到本地 (JVM),和匯出到遠端。在深入分析服務匯出原始碼前,我們先來從巨集觀層面上看一下服務匯出邏輯。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { // 省略無關程式碼 if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) .hasExtension(url.getProtocol())) { // 載入 ConfiguratorFactory,並生成 Configurator 配置 url url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) .getExtension(url.getProtocol()).getConfigurator(url).configure(url); } String scope = url.getParameter(Constants.SCOPE_KEY); // 如果 scope = none,則什麼都不做 if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) { // scope != remote,匯出到本地 if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) { exportLocal(url); } // scope != local,匯出到遠端 if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) { if (registryURLs != null && !registryURLs.isEmpty()) { for (URL registryURL : registryURLs) { url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY)); // 載入監視器連結 URL monitorUrl = loadMonitor(registryURL); if (monitorUrl != null) { // 將監視器連結作為引數新增到 url 中 url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString()); } String proxy = url.getParameter(Constants.PROXY_KEY); if (StringUtils.isNotEmpty(proxy)) { registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy); } // 為服務提供類(ref)生成 Invoker Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); // DelegateProviderMetaDataInvoker 僅用於持有 Invoker 和 ServiceConfig DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); // 匯出服務,並生成 Exporter Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } else { // 不存在註冊中心,僅匯出服務 Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter); } } } this.urls.add(url); } |
上面程式碼根據 url 中的 scope 引數決定服務匯出方式,分別如下:
- scope = none,不匯出服務
- scope != remote,匯出到本地
- scope != local,匯出到遠端
不管是匯出到本地,還是遠端。進行服務匯出之前,均需要先建立 Invoker。這是一個很重要的步驟,因此接下來我會先分析 Invoker 的建立過程。
2.2.1 Invoker 建立過程
在 Dubbo 中,Invoker 是一個非常重要的模型。在服務提供端,以及服務引用端均會出現 Invoker。Dubbo 官方文件中對 Invoker 進行了說明,這裡引用一下。
Invoker 是實體域,它是 Dubbo 的核心模型,其它模型都向它靠擾,或轉換成它,它代表一個可執行體,可向它發起 invoke 呼叫,它有可能是一個本地的實現,也可能是一個遠端的實現,也可能一個叢集實現。
既然 Invoker 如此重要,那麼我們很有必要搞清楚 Invoker 的用途。Invoker 是由 ProxyFactory 建立而來,Dubbo 預設的 ProxyFactory 實現類是 JavassistProxyFactory。下面我們到 JavassistProxyFactory 程式碼中,探索 Invoker 的建立過程。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-- JavassistProxyFactory public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { // 為目標類建立 Wrapper final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); // 建立匿名 Invoker 類物件,並實現 doInvoke 方法。 return new AbstractProxyInvoker<T>(proxy, type, url) { @Override protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable { // 呼叫 Wrapper 的 invokeMethod 方法,invokeMethod 最終會呼叫目標方法 return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); } }; } |
如上,JavassistProxyFactory 建立了一個繼承自 AbstractProxyInvoker 類的匿名物件,並覆寫了抽象方法 doInvoke。覆寫後的 doInvoke 邏輯比較簡單,僅是將呼叫請求轉發給了 Wrapper 類的 invokeMethod 方法。Wrapper 用於“包裹”目標類,Wrapper 是一個抽象類,僅可通過 getWrapper(Class) 方法建立子類。在建立 Wrapper 子類的過程中,子類程式碼生成邏輯會對 getWrapper 方法傳入的 Class 物件進行解析,拿到諸如類方法,類成員變數等資訊。以及生成 invokeMethod 方法程式碼,和其他一些方法程式碼。程式碼生成完畢後,通過 Javassist 生成 Class 物件,最後再通過反射建立 Wrapper 例項。相關的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static Wrapper getWrapper(Class<?> c) { while (ClassGenerator.isDynamicClass(c)) c = c.getSuperclass(); if (c == Object.class) return OBJECT_WRAPPER; // 訪存 Wrapper ret = WRAPPER_MAP.get(c); if (ret == null) { // 快取未命中,建立 Wrapper ret = makeWrapper(c); // 寫入快取 WRAPPER_MAP.put(c, ret); } return ret; } |
getWrapper 方法只是包含了一些快取操作邏輯,非重點。下面我們重點關注 makeWrapper 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
private static Wrapper makeWrapper(Class<?> c) { // 檢測 c 是否為私有型別,若是則丟擲異常 if (c.isPrimitive()) throw new IllegalArgumentException("Can not create wrapper for primitive type: " + c); String name = c.getName(); ClassLoader cl = ClassHelper.getClassLoader(c); // c1 用於儲存 setPropertyValue 方法程式碼 StringBuilder c1 = new StringBuilder("public void setPropertyValue(Object o, String n, Object v){ "); // c2 用於儲存 getPropertyValue 方法程式碼 StringBuilder c2 = new StringBuilder("public Object getPropertyValue(Object o, String n){ "); // c3 用於儲存 invokeMethod 方法程式碼 StringBuilder c3 = new StringBuilder("public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws " + InvocationTargetException.class.getName() + "{ "); // 生成型別轉換程式碼及異常捕捉程式碼,比如: // DemoService w; try { w = ((DemoServcie) $1); }}catch(Throwable e){ throw new IllegalArgumentException(e); } c1.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); c2.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); c3.append(name).append(" w; try{ w = ((").append(name).append(")$1); }catch(Throwable e){ throw new IllegalArgumentException(e); }"); // pts 用於儲存成員變數名和型別 Map<String, Class<?>> pts = new HashMap<String, Class<?>>(); // ms 用於儲存方法描述資訊(可理解為方法簽名)及 Method 例項 Map<String, Method> ms = new LinkedHashMap<String, Method>(); // mns 為方法名列表 List<String> mns = new ArrayList<String>(); // dmns 用於儲存定義在當前類中的方法的名稱 List<String> dmns = new ArrayList<String>(); // --------------------------------✨ 分割線1 ✨------------------------------------- // 獲取 public 訪問級別的欄位,併為所有欄位生成條件判斷語句 for (Field f : c.getFields()) { String fn = f.getName(); Class<?> ft = f.getType(); if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) // 忽略關鍵字 static 或 transient 修飾的變數 continue; // 生成條件判斷及賦值語句,比如: // if( $2.equals("name") ) { w.name = (java.lang.String) $3; return;} // if( $2.equals("age") ) { w.age = ((Number) $3).intValue(); return;} c1.append(" if( $2.equals(\"").append(fn).append("\") ){ w.").append(fn).append("=").append(arg(ft, "$3")).append("; return; }"); // 生成條件判斷及返回語句,比如: // if( $2.equals("name") ) { return ($w)w.name; } c2.append(" if( $2.equals(\"").append(fn).append("\") ){ return ($w)w.").append(fn).append("; }"); // 儲存 <欄位名, 欄位型別> 鍵值對到 pts 中 pts.put(fn, ft); } // --------------------------------✨ 分割線2 ✨------------------------------------- Method[] methods = c.getMethods(); // 檢測 c 中是否包含在當前類中宣告的方法 boolean hasMethod = hasMethods(methods); if (hasMethod) { c3.append(" try{"); } for (Method m : methods) { if (m.getDeclaringClass() == Object.class) // 忽略 Object 中定義的方法 continue; String mn = m.getName(); // 生成方法名判斷語句,示例如下: // if ( "sayHello".equals( $2 ) c3.append(" if( \"").append(mn).append("\".equals( $2 ) "); int len = m.getParameterTypes().length; // 生成執行時傳入引數的數量與方法的引數列表長度判斷語句,示例如下: // && $3.length == 2 c3.append(" && ").append(" $3.length == ").append(len); boolean override = false; for (Method m2 : methods) { // 檢測方法是否存在過載情況,條件為:方法物件不同 && 方法名相同 if (m != m2 && m.getName().equals(m2.getName())) { override = true; break; } } // 對過載方法進行處理,考慮下面的方法: // 1. void sayHello(Integer, String) // 2. void sayHello(Integer, Integer) // 方法名相同,引數列表長度也相同,因此不能僅通過這兩項判斷兩個方法是否相等。 // 需要進一步判斷方法的引數型別 if (override) { if (len > 0) { for (int l = 0; l < len; l++) { // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String") c3.append(" && ").append(" $3[").append(l).append("].getName().equals(\"") .append(m.getParameterTypes()[l].getName()).append("\")"); } } } // 新增 ) {,完成方法判斷語句,此時生成的方法可能如下(已格式化): // if ("sayHello".equals($2) // && $3.length == 2 // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String")) { c3.append(" ) { "); // 根據返回值型別生成目標方法呼叫語句 if (m.getReturnType() == Void.TYPE) // w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); return null; c3.append(" w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");").append(" return null;"); else // return w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); c3.append(" return ($w)w.").append(mn).append('(').append(args(m.getParameterTypes(), "$4")).append(");"); // 新增 }, 當前”方法判斷條件“程式碼生成完畢,示例程式碼如下(已格式化): // if ("sayHello".equals($2) // && $3.length == 2 // && $3[0].getName().equals("java.lang.Integer") // && $3[1].getName().equals("java.lang.String")) { // // w.sayHello((java.lang.Integer)$4[0], (java.lang.String)$4[1]); // return null; // } c3.append(" }"); // 新增方法名到 mns 集合中 mns.add(mn); // 檢測當前方法是否在 c 中被宣告的 if (m.getDeclaringClass() == c) // 若是,則將當前方法名新增到 dmns 中 dmns.add(mn); ms.put(ReflectUtils.getDesc(m), m); } if (hasMethod) { // 新增異常捕捉語句 c3.append(" } catch(Throwable e) { "); c3.append(" throw new java.lang.reflect.InvocationTargetException(e); "); c3.append(" }"); } // 新增 NoSuchMethodException 異常丟擲程式碼 c3.append(" throw new " + NoSuchMethodException.class.getName() + "(\"Not found method \\\"\"+$2+\"\\\" in class " + c.getName() + ".\"); }"); // --------------------------------✨ 分割線3 ✨------------------------------------- Matcher matcher; // 處理 get/set 方法 for (Map.Entry<String, Method> entry : ms.entrySet()) { String md = entry.getKey(); Method method = (Method) entry.getValue(); // 匹配以 get 開頭的方法 if ((matcher = ReflectUtils.GETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) { // 獲取屬性名 String pn = propertyName(matcher.group(1)); // 生成屬性判斷以及返回語句,示例如下: // if( $2.equals("name") ) { return ($w).w.getName(); } c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }"); pts.put(pn, method.getReturnType()); // 匹配以 is/has/can 開頭的方法 } else if ((matcher = ReflectUtils.IS_HAS_CAN_METHOD_DESC_PATTERN.matcher(md)).matches()) { String pn = propertyName(matcher.group(1)); // 生成屬性判斷以及返回語句,示例如下: // if( $2.equals("dream") ) { return ($w).w.hasDream(); } c2.append(" if( $2.equals(\"").append(pn).append("\") ){ return ($w)w.").append(method.getName()).append("(); }"); pts.put(pn, method.getReturnType()); // 匹配以 set 開頭的方法 } else if ((matcher = ReflectUtils.SETTER_METHOD_DESC_PATTERN.matcher(md)).matches()) { Class<?> pt = method.getParameterTypes()[0]; String pn = propertyName(matcher.group(1)); // 生成屬性判斷以及 setter 呼叫語句,示例如下: // if( $2.equals("name") ) { w.setName((java.lang.String)$3); return; } c1.append(" if( $2.equals(\"").append(pn).append("\") ){ w.").append(method.getName()).append("(").append(arg(pt, "$3")).append("); return; }"); pts.put(pn, pt); } } // 新增 NoSuchPropertyException 異常丟擲程式碼 c1.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" filed or setter method in class " + c.getName() + ".\"); }"); c2.append(" throw new " + NoSuchPropertyException.class.getName() + "(\"Not found property \\\"\"+$2+\"\\\" filed or setter method in class " + c.getName() + ".\"); }"); // --------------------------------✨ 分割線4 ✨------------------------------------- long id = WRAPPER_CLASS_COUNTER.getAndIncrement(); // 建立類生成器 ClassGenerator cc = ClassGenerator.newInstance(cl); // 設定類名及超類 cc.setClassName((Modifier.isPublic(c.getModifiers()) ? Wrapper.class.getName() : c.getName() + "$sw") + id); cc.setSuperClass(Wrapper.class); // 新增預設構造方法 cc.addDefaultConstructor(); // 新增欄位 cc.addField("public static String[] pns;"); cc.addField("public static " + Map.class.getName() + " pts;"); cc.addField("public static String[] mns;"); cc.addField("public static String[] dmns;"); for (int i = 0, len = ms.size(); i < len; i++) cc.addField("public static Class[] mts" + i + ";"); // 新增方法程式碼 cc.addMethod("public String[] getPropertyNames(){ return pns; }"); cc.addMethod("public boolean hasProperty(String n){ return pts.containsKey($1); }"); cc.addMethod("public Class getPropertyType(String n){ return (Class)pts.get($1); }"); cc.addMethod("public String[] getMethodNames(){ return mns; }"); cc.addMethod("public String[] getDeclaredMethodNames(){ return dmns; }"); cc.addMethod(c1.toString()); cc.addMethod(c2.toString()); cc.addMethod(c3.toString()); try { // 生成類 Class<?> wc = cc.toClass(); // 設定欄位值 wc.getField("pts").set(null, pts); wc.getField("pns").set(null, pts.keySet().toArray(new String[0])); wc.getField("mns").set(null, mns.toArray(new String[0])); wc.getField("dmns").set(null, dmns.toArray(new String[0])); int ix = 0; for (Method m : ms.values()) wc.getField("mts" + ix++).set(null, m.getParameterTypes()); // 建立 Wrapper 例項 return (Wrapper) wc.newInstance(); } catch (RuntimeException e) { throw e; } catch (Throwable e) { throw new RuntimeException(e.getMessage(), e); } finally { cc.release(); ms.clear(); mns.clear(); dmns.clear(); } } |
上面程式碼很長,大家耐心看一下。我在上面程式碼中做了大量的註釋,並按功能對程式碼進行了分塊,以幫助大家理解程式碼邏輯。下面對這段程式碼進行講解。首先我們把目光移到分割線1之上的程式碼,這段程式碼主要用於進行一些初始化操作。比如建立 c1、c2、c3 以及 pts、ms、mns 等變數,以及向 c1、c2、c3 中新增方法定義和型別型別轉換程式碼。接下來是分割線1到分割線2之間的程式碼,這段程式碼用於為 public 級別的欄位生成條件判斷取值與賦值程式碼。這段程式碼不是很難看懂,就不多說了。繼續向下看,分割線2和分隔線3之間的程式碼用於為定義在當前類中的方法生成判斷語句,和方法呼叫語句。因為需要對方法過載進行校驗,因此到這這段程式碼看起來有點複雜。不過耐心開一下,也不是很難理解。接下來是分割線3和分隔線4之間的程式碼,這段程式碼用於處理 getter、setter 以及以 is/has/can 開頭的方法。處理方式是通過正則表示式獲取方法型別(get/set/is/…),以及屬性名。之後為屬性名生成判斷語句,然後為方法生成呼叫語句。最後我們再來看一下分隔線4以下的程式碼,這段程式碼通過 ClassGenerator 為剛剛生成的程式碼構建 Class 類,並通過反射建立物件。ClassGenerator 是 Dubbo 自己封裝的,該類的核心是 toClass() 的過載方法 toClass(ClassLoader, ProtectionDomain),該方法通過 javassist 構建 Class。這裡就不分析 toClass 方法了,大家請自行分析。
閱讀 Wrapper 類程式碼需要對 javassist 框架有所瞭解。關於 javassist,大家如果不熟悉,請自行查閱資料,本節不打算介紹 javassist 相關內容。
好了,關於 Wrapper 類生成過程就分析到這。如果大家看的不是很明白,可以單獨為 Wrapper 建立單元測試,然後單步除錯。並將生成的程式碼拷貝出來,格式化後再進行觀察和理解。好了,本節先到這。
2.2.2 匯出服務到本地
本節我們來看一下服務匯出相關的程式碼,按照程式碼執行順序,本節先來分析匯出服務到本地的過程。相關程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private void exportLocal(URL url) { // 如果 URL 的協議頭等於 injvm,說明已經匯出到本地了,無需再次匯出 if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { URL local = URL.valueOf(url.toFullString()) .setProtocol(Constants.LOCAL_PROTOCOL) // 設定協議頭為 injvm .setHost(LOCALHOST) .setPort(0); ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref)); // 建立 Invoker,並匯出服務,這裡的 protocol 會在執行時呼叫 InjvmProtocol 的 export 方法 Exporter<?> exporter = protocol.export( proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); exporters.add(exporter); } } |
exportLocal 方法比較簡單,首先根據 URL 協議頭決定是否匯出服務。若需匯出,則建立一個新的 URL 並將協議頭、主機名以及埠設定成新的值。然後建立 Invoker,並呼叫 InjvmProtocol 的 export 方法匯出服務。下面我們來看一下 InjvmProtocol 的 export 方法都做了哪些事情。
1 2 3 4 |
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { // 建立 InjvmExporter return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap); } |
如上,InjvmProtocol 的 export 方法僅建立了一個 InjvmExporter,無其他邏輯。到此匯出服務到本地就分析完了,接下來,我們繼續分析匯出服務到遠端的過程。
2.2.3 匯出服務到遠端
與匯出服務到本地相比,匯出服務到遠端的過程要複雜不少,其包含了服務匯出與服務註冊兩個過程。這兩個過程涉及到了大量的呼叫,因此比較複雜。不過不管再難,我們都要看一下,萬一看懂了呢。按照程式碼執行順序,本節先來分析服務匯出邏輯,服務註冊邏輯將在下一節進行分析。下面開始分析,我們把目光移動到 RegistryProtocol 的 export 方法上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { // 匯出服務 final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker); // 獲取註冊中心 URL,以 zookeeper 註冊中心為例,得到的示例 URL 如下: // zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F172.17.48.52%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider URL registryUrl = getRegistryUrl(originInvoker); // 根據 URL 載入 Registry 實現類,比如 ZookeeperRegistry final Registry registry = getRegistry(originInvoker); // 獲取已註冊的服務提供者 URL,比如: // dubbo://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker); // 獲取 register 引數 boolean register = registeredProviderUrl.getParameter("register", true); // 向服務提供者與消費者登錄檔中註冊服務提供者 ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl); // 根據 register 的值決定是否註冊服務 if (register) { // 向註冊中心註冊服務 register(registryUrl, registeredProviderUrl); ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true); } // 獲取訂閱 URL,比如: // provider://172.17.48.52:20880/com.alibaba.dubbo.demo.DemoService?category=configurators&check=false&anyhost=true&application=demo-provider&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl); // 建立監聽器 final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); // 向註冊中心進行訂閱 override 資料 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); // 建立並返回 DestroyableExporter return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl); } |
上面程式碼看起來比較複雜,主要做如下一些操作:
- 呼叫 doLocalExport 匯出服務
- 向註冊中心註冊服務
- 向註冊中心進行訂閱 override 資料
- 建立並返回 DestroyableExporter
在以上操作中,除了建立並返回 DestroyableExporter 沒啥難度外,其他幾步操作都不是很簡單。這其中,匯出服務和註冊服務是本章要重點分析的邏輯。 訂閱 override 資料這個是非重點內容,後面會簡單介紹一下。下面開始本節的分析,先來分析 doLocalExport 方法的邏輯,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) { String key = getCacheKey(originInvoker); // 訪問快取 ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key); if (exporter == null) { synchronized (bounds) { exporter = (ExporterChangeableWrapper<T>) bounds.get(key); if (exporter == null) { // 建立 Invoker 為委託類物件 final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker)); // 呼叫 protocol 的 export 方法匯出服務 exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker); // 寫快取 bounds.put(key, exporter); } } } return exporter; } |
上面的程式碼是典型的雙重檢查,這個大家應該都知道。接下來,我們把重點放在 Protocol 的 export 方法上。假設執行時協議為 dubbo,此處的 protocol 會在執行時載入 DubboProtocol,並呼叫 DubboProtocol 的 export 方法。我們目光轉移到 DubboProtocol 的 export 方法上,相關分析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { URL url = invoker.getUrl(); // 獲取服務標識,理解成服務座標也行。由服務組名,服務名,服務版本號以及埠組成。比如: // 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); // 以下程式碼應該和本地存根有關,程式碼不難看懂,但具體用途暫時不清楚,先忽略 Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT); Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false); if (isStubSupportEvent && !isCallbackservice) { String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY); if (stubServiceMethods == null || stubServiceMethods.length() == 0) { // 省略日誌列印程式碼 } else { stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods); } } // 啟動伺服器 openServer(url); // 優化序列化 optimizeSerialization(url); return exporter; } |
如上,我們重點關注 DubboExporter 的建立以及 openServer 方法,其他邏輯看不懂也沒關係,不影響理解服務匯出過程。另外,DubboExporter 的程式碼比較簡單,就不分析了。下面分析 openServer 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private void openServer(URL url) { // 獲取 host:port,並將其作為伺服器例項的 key,用於標識當前的伺服器例項 String key = url.getAddress(); boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true); if (isServer) { // 訪問快取 ExchangeServer server = serverMap.get(key); if (server == null) { // 建立伺服器例項 serverMap.put(key, createServer(url)); } else { // 伺服器已建立,則根據 url 中的配置重置伺服器 server.reset(url); } } } |
如上,在同一臺機器上(單網絡卡),同一個埠上僅允許啟動一個伺服器例項。若某個埠上已有伺服器例項,此時則呼叫 reset 方法重置伺服器的一些配置。考慮到篇幅問題,關於伺服器例項重置的程式碼就不分析了。接下來分析伺服器例項的建立過程。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
private ExchangeServer createServer(URL url) { url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, // 新增心跳檢測配置到 url 中 url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT)); // 獲取 server 引數,預設為 netty String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER); // 通過 SPI 檢測是否存在 server 引數所代表的 Transporter 拓展,不存在則丟擲異常 if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) throw new RpcException("Unsupported server type: " + str + ", url: " + url); // 新增編碼解碼器引數 url = url.addParameter(Consta |