Dubbo原始碼解析之服務匯出過程

Java勸退師發表於2019-04-03

1.簡介

本篇文章,我們來研究一下 Dubbo 匯出服務的過程。Dubbo 服務匯出過程始於 Spring 容器釋出重新整理事件,Dubbo 在接收到事件後,會立即執行服務匯出邏輯。整個邏輯大致可分為三個部分,第一部分是前置工作,主要用於檢查引數,組裝 URL。第二部分是匯出服務,包含匯出服務到本地 (JVM),和匯出服務到遠端兩個過程。第三部分是向註冊中心註冊服務,用於服務發現。本篇文章將會對這三個部分程式碼進行詳細的分析。

2.原始碼分析

服務匯出的入口方法是 ServiceBean 的 onApplicationEvent。onApplicationEvent 是一個事件響應方法,該方法會在收到 Spring 上下文重新整理事件後執行服務匯出操作。方法程式碼如下:

public void onApplicationEvent(ContextRefreshedEvent event) {
    // 是否有延遲匯出 && 是否已匯出 && 是不是已被取消匯出
    if (isDelay() && !isExported() && !isUnexported()) {
        // 匯出服務
        export();
    }
}
複製程式碼

這個方法首先會根據條件決定是否匯出服務,比如有些服務設定了延時匯出,那麼此時就不應該在此處匯出。還有一些服務已經被匯出了,或者當前服務被取消匯出了,此時也不能再次匯出相關服務。注意這裡的 isDelay 方法,這個方法字面意思是“是否延遲匯出服務”,返回 true 表示延遲匯出,false 表示不延遲匯出。但是該方法真實意思卻並非如此,當方法返回 true 時,表示無需延遲匯出。返回 false 時,表示需要延遲匯出。與字面意思恰恰相反,這個需要大家注意一下。下面我們來看一下這個方法的邏輯。

// -☆- 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。這個方法的返回值讓人有點困惑。

現在解釋一下 supportedApplicationListener 變數含義,該變數用於表示當前的 Spring 容器是否支援 ApplicationListener,這個值初始為 false。在 Spring 容器將自己設定到 ServiceBean 中時,ServiceBean 的 setApplicationContext 方法會檢測 Spring 容器是否支援 ApplicationListener。若支援,則將 supportedApplicationListener 置為 true。ServiceBean 是 Dubbo 與 Spring 框架進行整合的關鍵,可以看做是兩個框架之間的橋樑。具有同樣作用的類還有 ReferenceBean。

現在我們知道了 Dubbo 服務匯出過程的起點,接下來對服務匯出的前置邏輯進行分析。

2.1 前置工作

前置工作主要包含兩個部分,分別是配置檢查,以及 URL 裝配。在匯出服務之前,Dubbo 需要檢查使用者的配置是否合理,或者為使用者補充預設配置。配置檢查完成後,接下來需要根據這些配置組裝 URL。在 Dubbo 中,URL 的作用十分重要。Dubbo 使用 URL 作為配置載體,所有的擴充點都是通過 URL 獲取配置。這一點,官方文件中有所說明。

採用 URL 作為配置資訊的統一格式,所有擴充套件點都通過傳遞 URL 攜帶配置資訊。

接下來,我們先來分析配置檢查部分的原始碼,隨後再來分析 URL 組裝部分的原始碼。

2.1.1 檢查配置

本節我們接著前面的原始碼向下分析,前面說過 onApplicationEvent 方法在經過一些判斷後,會決定是否呼叫 export 方法匯出服務。那麼下面我們從 export 方法開始進行分析,如下:

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;
    }

    // delay > 0,延時匯出服務
    if (delay != null && delay > 0) {
        delayExportExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                doExport();
            }
        }, delay, TimeUnit.MILLISECONDS);
        
    // 立即匯出服務
    } else {
        doExport();
    }
}
複製程式碼

export 方法對兩項配置進行了檢查,並根據配置執行相應的動作。首先是 export 配置,這個配置決定了是否匯出服務。有時候我們只是想本地啟動服務進行一些除錯工作,我們並不希望把本地啟動的服務暴露出去給別人呼叫。此時,我們可通過配置 export 禁止服務匯出,比如:

<dubbo:provider export="false" />
複製程式碼

delay 配置顧名思義,用於延遲匯出服務,這個就不分析了。下面,我們繼續分析原始碼,這次要分析的是 doExport 方法。

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();
        }
        
    // ref 非 GenericService 型別
    } else {
        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 和 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);
        }
    }

    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 方法中對多協議,多註冊中心進行了支援。相關程式碼如下:

private void doExportUrls() {
    // 載入註冊中心連結
    List<URL> registryURLs = loadRegistries(true);
    // 遍歷 protocols,並在每個協議下匯出服務
    for (ProtocolConfig protocolConfig : protocols) {
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}
複製程式碼

上面程式碼首先是通過 loadRegistries 載入註冊中心連結,然後再遍歷 ProtocolConfig 集合匯出每個服務。並在匯出服務的過程中,將服務註冊到註冊中心。下面,我們先來看一下 loadRegistries 方法的邏輯。

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);
                
                // 新增 path、pid,protocol 等資訊到 map 中
                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;
}
複製程式碼

loadRegistries 方法主要包含如下的邏輯:

檢測是否存在註冊中心配置類,不存在則丟擲異常 構建引數對映集合,也就是 map 構建註冊中心連結列表 遍歷連結列表,並根據條件決定是否將其新增到 registryList 中 關於多協議多註冊中心匯出服務就先分析到這,程式碼不是很多,接下來分析 URL 組裝過程。

2.1.3 組裝 URL

配置檢查完畢後,緊接著要做的事情是根據配置,以及其他一些資訊組裝 URL。前面說過,URL 是 Dubbo 配置的載體,通過 URL 可讓 Dubbo 的各種配置在各個模組之間傳遞。URL 之於 Dubbo,猶如水之於魚,非常重要。大家在閱讀 Dubbo 服務匯出相關原始碼的過程中,要注意 URL 內容的變化。既然 URL 如此重要,那麼下面我們來了解一下 URL 組裝的過程。

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)) {
            // 隨機生成 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:method 標籤中的配置資訊,並將相關配置新增到 map 中。程式碼如下:

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個重要的分支,下面用虛擬碼解釋一下這幾個分支的含義。

// 獲取 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),和匯出到遠端。在深入分析服務匯出的原始碼前,我們先來從巨集觀層面上看一下服務匯出邏輯。如下:

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 的建立過程。如下:

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 例項。相關的程式碼如下:

 public static Wrapper getWrapper(Class<?> c) {	
    while (ClassGenerator.isDynamicClass(c))
        c = c.getSuperclass();

    if (c == Object.class)
        return OBJECT_WRAPPER;

    // 從快取中獲取 Wrapper 例項
    Wrapper ret = WRAPPER_MAP.get(c);
    if (ret == null) {
        // 快取未命中,建立 Wrapper
        ret = makeWrapper(c);
        // 寫入快取
        WRAPPER_MAP.put(c, ret);
    }
    return ret;
}
複製程式碼

getWrapper 方法僅包含一些快取操作邏輯,不難理解。下面我們看一下 makeWrapper 方法。

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 匯出服務到本地

本節我們來看一下服務匯出相關的程式碼,按照程式碼執行順序,本節先來分析匯出服務到本地的過程。相關程式碼如下:

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 方法都做了哪些事情。

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 方法上。

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 方法的邏輯,如下:

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;
}
複製程式碼

上面的程式碼是典型的雙重檢查鎖,大家在閱讀 Dubbo 的原始碼中,會多次見到。接下來,我們把重點放在 Protocol 的 export 方法上。假設執行時協議為 dubbo,此處的 protocol 變數會在執行時載入 DubboProtocol,並呼叫 DubboProtocol 的 export 方法。所以,接下來我們目光轉移到 DubboProtocol 的 export 方法上,相關分析如下:

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 方法。

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 方法重置伺服器的一些配置。考慮到篇幅問題,關於伺服器例項重置的程式碼就不分析了。接下來分析伺服器例項的建立過程。如下:

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(Constants.CODEC_KEY, DubboCodec.NAME);
    ExchangeServer server;
    try {
        // 建立 ExchangeServer
        server = Exchangers.bind(url, requestHandler);
    } catch (RemotingException e) {
        throw new RpcException("Fail to start server...");
    }
                                   
	// 獲取 client 引數,可指定 netty,mina
    str = url.getParameter(Constants.CLIENT_KEY);
    if (str != null && str.length() > 0) {
        // 獲取所有的 Transporter 實現類名稱集合,比如 supportedTypes = [netty, mina]
        Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
        // 檢測當前 Dubbo 所支援的 Transporter 實現類名稱列表中,
        // 是否包含 client 所表示的 Transporter,若不包含,則丟擲異常
        if (!supportedTypes.contains(str)) {
            throw new RpcException("Unsupported client type...");
        }
    }
    return server;
}
複製程式碼

如上,createServer 包含三個核心的邏輯。第一是檢測是否存在 server 引數所代表的 Transporter 擴充,不存在則丟擲異常。第二是建立伺服器例項。第三是檢測是否支援 client 引數所表示的 Transporter 擴充,不存在也是丟擲異常。兩次檢測操作所對應的程式碼比較直白了,無需多說。但建立伺服器的操作目前還不是很清晰,我們繼續往下看。

public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
    if (url == null) {
        throw new IllegalArgumentException("url == null");
    }
    if (handler == null) {
        throw new IllegalArgumentException("handler == null");
    }
    url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
    // 獲取 Exchanger,預設為 HeaderExchanger。
    // 緊接著呼叫 HeaderExchanger 的 bind 方法建立 ExchangeServer 例項
    return getExchanger(url).bind(url, handler);
}
複製程式碼

上面程式碼比較簡單,就不多說了。下面看一下 HeaderExchanger 的 bind 方法。

public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
	// 建立 HeaderExchangeServer 例項,該方法包含了多個邏輯,分別如下:
	//   1. new HeaderExchangeHandler(handler)
	//	 2. new DecodeHandler(new HeaderExchangeHandler(handler))
	//   3. Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))
    return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
}
複製程式碼

HeaderExchanger 的 bind 方法包含的邏輯比較多,但目前我們僅需關心 Transporters 的 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);
}
複製程式碼

如上,getTransporter() 方法獲取的 Transporter 是在執行時動態建立的,類名為 TransporterAdaptive,也就是自適應擴充類。TransporterAdaptive 會在執行時根據傳入的 URL 引數決定載入什麼型別的 Transporter,預設為 NettyTransporter。下面我們繼續跟下去,這次分析的是 NettyTransporter 的 bind 方法。

public Server bind(URL url, ChannelHandler listener) throws RemotingException {
	// 建立 NettyServer
	return new NettyServer(url, listener);
}
複製程式碼

這裡僅有一句建立 NettyServer 的程式碼,無需多說,我們繼續向下看。

public class NettyServer extends AbstractServer implements Server {
    public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
        // 呼叫父類構造方法
        super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));
    }
}


public abstract class AbstractServer extends AbstractEndpoint implements Server {
    public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
        // 呼叫父類構造方法,這裡就不用跟進去了,沒什麼複雜邏輯
        super(url, handler);
        localAddress = getUrl().toInetSocketAddress();

        // 獲取 ip 和埠
        String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
        int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
        if (url.getParameter(Constants.ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
            // 設定 ip 為 0.0.0.0
            bindIp = NetUtils.ANYHOST;
        }
        bindAddress = new InetSocketAddress(bindIp, bindPort);
        // 獲取最大可接受連線數
        this.accepts = url.getParameter(Constants.ACCEPTS_KEY, Constants.DEFAULT_ACCEPTS);
        this.idleTimeout = url.getParameter(Constants.IDLE_TIMEOUT_KEY, Constants.DEFAULT_IDLE_TIMEOUT);
        try {
            // 呼叫模板方法 doOpen 啟動伺服器
            doOpen();
        } catch (Throwable t) {
            throw new RemotingException("Failed to bind ");
        }

        DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
        executor = (ExecutorService) dataStore.get(Constants.EXECUTOR_SERVICE_COMPONENT_KEY, Integer.toString(url.getPort()));
    }
    
    protected abstract void doOpen() throws Throwable;

    protected abstract void doClose() throws Throwable;
}
複製程式碼

上面程式碼多為賦值程式碼,不需要多講。我們重點關注 doOpen 抽象方法,該方法需要子類實現。下面回到 NettyServer 中。

protected void doOpen() throws Throwable {
    NettyHelper.setNettyLoggerFactory();
    // 建立 boss 和 worker 執行緒池
    ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));
    ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));
    ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));
    
    // 建立 ServerBootstrap
    bootstrap = new ServerBootstrap(channelFactory);

    final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
    channels = nettyHandler.getChannels();
    bootstrap.setOption("child.tcpNoDelay", true);
    // 設定 PipelineFactory
    bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
        @Override
        public ChannelPipeline getPipeline() {
            NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
            ChannelPipeline pipeline = Channels.pipeline();
            pipeline.addLast("decoder", adapter.getDecoder());
            pipeline.addLast("encoder", adapter.getEncoder());
            pipeline.addLast("handler", nettyHandler);
            return pipeline;
        }
    });
    // 繫結到指定的 ip 和埠上
    channel = bootstrap.bind(getBindAddress());
}
複製程式碼

以上就是 NettyServer 建立的過程,dubbo 預設使用的 NettyServer 是基於 netty 3.x 版本實現的,比較老了。因此 Dubbo 另外提供了 netty 4.x 版本的 NettyServer,大家可在使用 Dubbo 的過程中按需進行配置。

到此,關於服務匯出的過程就分析完了。整個過程比較複雜,大家在分析的過程中耐心一些。並且多寫 Demo 進行除錯,以便能夠更好的理解程式碼邏輯。

本節內容先到這裡,接下來分析服務匯出的另一塊邏輯 — 服務註冊。

2.2.4 服務註冊

本節我們來分析服務註冊過程,服務註冊操作對於 Dubbo 來說不是必需的,通過服務直連的方式就可以繞過註冊中心。但通常我們不會這麼做,直連方式不利於服務治理,僅推薦在測試服務時使用。對於 Dubbo 來說,註冊中心雖不是必需,但卻是必要的。因此,關於註冊中心以及服務註冊相關邏輯,我們也需要搞懂。

本節內容以 Zookeeper 註冊中心作為分析目標,其他型別註冊中心大家可自行分析。下面從服務註冊的入口方法開始分析,我們把目光再次移到 RegistryProtocol 的 export 方法上。如下:

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    
    // ${匯出服務}
    
    // 省略其他程式碼
    
    boolean register = registeredProviderUrl.getParameter("register", true);
    if (register) {
        // 註冊服務
        register(registryUrl, registeredProviderUrl);
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    }
    
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    // 訂閱 override 資料
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

    // 省略部分程式碼
}
複製程式碼

RegistryProtocol 的 export 方法包含了服務匯出,註冊,以及資料訂閱等邏輯。其中服務匯出邏輯上一節已經分析過了,本節將分析服務註冊邏輯,相關程式碼如下:

public void register(URL registryUrl, URL registedProviderUrl) {
    // 獲取 Registry
    Registry registry = registryFactory.getRegistry(registryUrl);
    // 註冊服務
    registry.register(registedProviderUrl);
}
複製程式碼

register 方法包含兩步操作,第一步是獲取註冊中心例項,第二步是向註冊中心註冊服務。接下來分兩節內容對這兩步操作進行分析。

2.2.4.1 建立註冊中心

本節內容以 Zookeeper 註冊中心為例進行分析。下面先來看一下 getRegistry 方法的原始碼,這個方法由 AbstractRegistryFactory 實現。如下:

public Registry getRegistry(URL url) {
    url = url.setPath(RegistryService.class.getName())
            .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
            .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
    String key = url.toServiceString();
    LOCK.lock();
    try {
    	// 訪問快取
        Registry registry = REGISTRIES.get(key);
        if (registry != null) {
            return registry;
        }
        
        // 快取未命中,建立 Registry 例項
        registry = createRegistry(url);
        if (registry == null) {
            throw new IllegalStateException("Can not create registry...");
        }
        
        // 寫入快取
        REGISTRIES.put(key, registry);
        return registry;
    } finally {
        LOCK.unlock();
    }
}
複製程式碼

protected abstract Registry createRegistry(URL url); 如上,getRegistry 方法先訪問快取,快取未命中則呼叫 createRegistry 建立 Registry,然後寫入快取。這裡的 createRegistry 是一個模板方法,由具體的子類實現。因此,下面我們到 ZookeeperRegistryFactory 中探究一番。

public class ZookeeperRegistryFactory extends AbstractRegistryFactory {

    // zookeeperTransporter 由 SPI 在執行時注入,型別為 ZookeeperTransporter$Adaptive
    private ZookeeperTransporter zookeeperTransporter;

    public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
        this.zookeeperTransporter = zookeeperTransporter;
    }

    @Override
    public Registry createRegistry(URL url) {
        // 建立 ZookeeperRegistry
        return new ZookeeperRegistry(url, zookeeperTransporter);
    }
}
複製程式碼

ZookeeperRegistryFactory 的 createRegistry 方法僅包含一句程式碼,無需解釋,繼續跟下去。

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    if (url.isAnyHost()) {
        throw new IllegalStateException("registry address == null");
    }
    
    // 獲取組名,預設為 dubbo
    String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
    if (!group.startsWith(Constants.PATH_SEPARATOR)) {
        // group = "/" + group
        group = Constants.PATH_SEPARATOR + group;
    }
    this.root = group;
    // 建立 Zookeeper 客戶端,預設為 CuratorZookeeperTransporter
    zkClient = zookeeperTransporter.connect(url);
    // 新增狀態監聽器
    zkClient.addStateListener(new StateListener() {
        @Override
        public void stateChanged(int state) {
            if (state == RECONNECTED) {
                try {
                    recover();
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
    });
}
複製程式碼

在上面的程式碼程式碼中,我們重點關注 ZookeeperTransporter 的 connect 方法呼叫,這個方法用於建立 Zookeeper 客戶端。建立好 Zookeeper 客戶端,意味著註冊中心的建立過程就結束了。接下來,再來分析一下 Zookeeper 客戶端的建立過程。

前面說過,這裡的 zookeeperTransporter 型別為自適應擴充類,因此 connect 方法會在被呼叫時決定載入什麼型別的 ZookeeperTransporter 擴充,預設為 CuratorZookeeperTransporter。下面我們到 CuratorZookeeperTransporter 中看一看。

public ZookeeperClient connect(URL url) {
    // 建立 CuratorZookeeperClient
    return new CuratorZookeeperClient(url);
}
複製程式碼

繼續向下看。

public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {

    private final CuratorFramework client;
    
    public CuratorZookeeperClient(URL url) {
        super(url);
        try {
            // 建立 CuratorFramework 構造器
            CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                    .connectString(url.getBackupAddress())
                    .retryPolicy(new RetryNTimes(1, 1000))
                    .connectionTimeoutMs(5000);
            String authority = url.getAuthority();
            if (authority != null && authority.length() > 0) {
                builder = builder.authorization("digest", authority.getBytes());
            }
            // 構建 CuratorFramework 例項
            client = builder.build();
            // 新增監聽器
            client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
                @Override
                public void stateChanged(CuratorFramework client, ConnectionState state) {
                    if (state == ConnectionState.LOST) {
                        CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED);
                    } else if (state == ConnectionState.CONNECTED) {
                        CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED);
                    } else if (state == ConnectionState.RECONNECTED) {
                        CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED);
                    }
                }
            });
            
            // 啟動客戶端
            client.start();
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}
複製程式碼

CuratorZookeeperClient 構造方法主要用於建立和啟動 CuratorFramework 例項。以上基本上都是 Curator 框架的程式碼,大家如果對 Curator 框架不是很瞭解,可以參考 Curator 官方文件。

本節分析了 ZookeeperRegistry 例項的建立過程,整個過程並不是很複雜。大家在看完分析後,可以自行除錯,以加深理解。現在註冊中心例項建立好了,接下來要做的事情是向註冊中心註冊服務,我們繼續往下看。

2.2.4.2 節點建立

以 Zookeeper 為例,所謂的服務註冊,本質上是將服務配置資料寫入到 Zookeeper 的某個路徑的節點下。為了讓大家有一個直觀的瞭解,下面我們將 Dubbo 的 demo 跑起來,然後通過 Zookeeper 視覺化客戶端 ZooInspector 檢視節點資料。如下:

從上圖中可以看到 com.alibaba.dubbo.demo.DemoService 這個服務對應的配置資訊(儲存在 URL 中)最終被註冊到了 /dubbo/com.alibaba.dubbo.demo.DemoService/providers/ 節點下。搞懂了服務註冊的本質,那麼接下來我們就可以去閱讀服務註冊的程式碼了。服務註冊的介面為 register(URL),這個方法定義在 FailbackRegistry 抽象類中。程式碼如下:

public void register(URL url) {
    super.register(url);
    failedRegistered.remove(url);
    failedUnregistered.remove(url);
    try {
        // 模板方法,由子類實現
        doRegister(url);
    } catch (Exception e) {
        Throwable t = e;

        // 獲取 check 引數,若 check = true 將會直接丟擲異常
        boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                && url.getParameter(Constants.CHECK_KEY, true)
                && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
        boolean skipFailback = t instanceof SkipFailbackWrapperException;
        if (check || skipFailback) {
            if (skipFailback) {
                t = t.getCause();
            }
            throw new IllegalStateException("Failed to register");
        } else {
            logger.error("Failed to register");
        }

        // 記錄註冊失敗的連結
        failedRegistered.add(url);
    }
}
複製程式碼

protected abstract void doRegister(URL url); 如上,我們重點關注 doRegister 方法呼叫即可,其他的程式碼先忽略。doRegister 方法是一個模板方法,因此我們到 FailbackRegistry 子類 ZookeeperRegistry 中進行分析。如下:

protected void doRegister(URL url) {
    try {
        // 通過 Zookeeper 客戶端建立節點,節點路徑由 toUrlPath 方法生成,路徑格式如下:
        //   /${group}/${serviceInterface}/providers/${url}
        // 比如
        //   /dubbo/org.apache.dubbo.DemoService/providers/dubbo%3A%2F%2F127.0.0.1......
        zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
    } catch (Throwable e) {
        throw new RpcException("Failed to register...");
    }
}
複製程式碼

如上,ZookeeperRegistry 在 doRegister 中呼叫了 Zookeeper 客戶端建立服務節點。節點路徑由 toUrlPath 方法生成,該方法邏輯不難理解,就不分析了。接下來分析 create 方法,如下:

public void create(String path, boolean ephemeral) {
    if (!ephemeral) {
        // 如果要建立的節點型別非臨時節點,那麼這裡要檢測節點是否存在
        if (checkExists(path)) {
            return;
        }
    }
    int i = path.lastIndexOf('/');
    if (i > 0) {
        // 遞迴建立上一級路徑
        create(path.substring(0, i), false);
    }
    
    // 根據 ephemeral 的值建立臨時或持久節點
    if (ephemeral) {
        createEphemeral(path);
    } else {
        createPersistent(path);
    }
}
複製程式碼

上面方法先是通過遞迴建立當前節點的上一級路徑,然後再根據 ephemeral 的值決定建立臨時還是持久節點。createEphemeral 和 createPersistent 這兩個方法都比較簡單,這裡簡單分析其中的一個。如下:

public void createEphemeral(String path) {
    try {
        // 通過 Curator 框架建立節點
        client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
    } catch (NodeExistsException e) {
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}
複製程式碼

好了,到此關於服務註冊的過程就分析完了。整個過程可簡單總結為:先建立註冊中心例項,之後再通過註冊中心例項註冊服務。本節先到這,接下來分析資料訂閱過程。

3.總結

本篇文章詳細分析了 Dubbo 服務匯出過程,包括配置檢測,URL 組裝,Invoker 建立過程、匯出服務以及註冊服務等等。篇幅比較大,需要大家耐心閱讀。本篇文章先就到這,如果文章有不妥錯誤之處,希望大家能夠進行反饋或修正。

歡迎大家加入Java高階架構群 378461078

相關文章