Dubbo原始碼解析之服務引入過程

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

1. 簡介

在 Dubbo 中,我們可以通過兩種方式引用遠端服務。第一種是使用服務直連的方式引用服務,第二種方式是基於註冊中心進行引用。服務直連的方式僅適合在除錯或測試服務的場景下使用,不適合線上上環境使用。因此,本文我將重點分析通過註冊中心引用服務的過程。從註冊中心中獲取服務配置只是服務引用過程中的一環,除此之外,服務消費者還需要經歷 Invoker 建立、代理類建立等步驟。這些步驟,將在後續章節中一一進行分析。

2. 服務引用原理

Dubbo 服務引用的時機有兩個,第一個是在 Spring 容器呼叫 ReferenceBean 的 afterPropertiesSet 方法時引用服務,第二個是在 ReferenceBean 對應的服務被注入到其他類中時引用。這兩個引用服務的時機區別在於,第一個是餓漢式的,第二個是懶漢式的。預設情況下,Dubbo 使用懶漢式引用服務。如果需要使用餓漢式,可通過配置 dubbo:reference 的 init 屬性開啟。下面我們按照 Dubbo 預設配置進行分析,整個分析過程從 ReferenceBean 的 getObject 方法開始。當我們的服務被注入到其他類中時,Spring 會第一時間呼叫 getObject 方法,並由該方法執行服務引用邏輯。按照慣例,在進行具體工作之前,需先進行配置檢查與收集工作。接著根據收集到的資訊決定服務用的方式,有三種,第一種是引用本地 (JVM) 服務,第二是通過直連方式引用遠端服務,第三是通過註冊中心引用遠端服務。不管是哪種引用方式,最後都會得到一個 Invoker 例項。如果有多個註冊中心,多個服務提供者,這個時候會得到一組 Invoker 例項,此時需要通過叢集管理類 Cluster 將多個 Invoker 合併成一個例項。合併後的 Invoker 例項已經具備呼叫本地或遠端服務的能力了,但並不能將此例項暴露給使用者使用,這會對使用者業務程式碼造成侵入。此時框架還需要通過代理工廠類 (ProxyFactory) 為服務介面生成代理類,並讓代理類去呼叫 Invoker 邏輯。避免了 Dubbo 框架程式碼對業務程式碼的侵入,同時也讓框架更容易使用。

以上就是服務引用的大致原理,下面我們深入到程式碼中,詳細分析服務引用細節。

3. 原始碼分析

服務引用的入口方法為 ReferenceBean 的 getObject 方法,該方法定義在 Spring 的 FactoryBean 介面中,ReferenceBean 實現了這個方法。實現程式碼如下:

public Object getObject() throws Exception {
    return get();
}

public synchronized T get() {
    if (destroyed) {
        throw new IllegalStateException("Already destroyed!");
    }
    // 檢測 ref 是否為空,為空則通過 init 方法建立
    if (ref == null) {
        // init 方法主要用於處理配置,以及呼叫 createProxy 生成代理類
        init();
    }
    return ref;
}
複製程式碼

以上兩個方法的程式碼比較簡短,並不難理解。這裡需要特別說明一下,如果你對 2.6.4 及以下版本的 getObject 方法進行除錯時,會碰到比較奇怪的的問題。這裡假設你使用 IDEA,且保持了 IDEA 的預設配置。當你面除錯到 get 方法的if (ref == null)時,你會發現 ref 不為空,導致你無法進入到 init 方法中繼續除錯。導致這個現象的原因是 Dubbo 框架本身有一些小問題。該問題已經在 pull request #2754 修復了此問題,並跟隨 2.6.5 版本釋出了。如果你正在學習 2.6.4 及以下版本,可通過修改 IDEA 配置規避這個問題。首先 IDEA 配置彈窗中搜尋 toString,然後取消Enable 'toString' object view勾選。具體如下:

3.1 處理配置

Dubbo 提供了豐富的配置,用於調整和優化框架行為,效能等。Dubbo 在引用或匯出服務時,首先會對這些配置進行檢查和處理,以保證配置到正確性。配置解析邏輯封裝在 ReferenceConfig 的 init 方法中,下面進行分析。

private void init() {
    // 避免重複初始化
    if (initialized) {
        return;
    }
    initialized = true;
    // 檢測介面名合法性
    if (interfaceName == null || interfaceName.length() == 0) {
        throw new IllegalStateException("interface not allow null!");
    }

    // 檢測 consumer 變數是否為空,為空則建立
    checkDefault();
    appendProperties(this);
    if (getGeneric() == null && getConsumer() != null) {
        // 設定 generic
        setGeneric(getConsumer().getGeneric());
    }

    // 檢測是否為泛化介面
    if (ProtocolUtils.isGeneric(getGeneric())) {
        interfaceClass = GenericService.class;
    } else {
        try {
            // 載入類
            interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                    .getContextClassLoader());
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        checkInterfaceAndMethods(interfaceClass, methods);
    }
    
    // -------------------------------✨ 分割線1 ✨------------------------------

    // 從系統變數中獲取與介面名對應的屬性值
    String resolve = System.getProperty(interfaceName);
    String resolveFile = null;
    if (resolve == null || resolve.length() == 0) {
        // 從系統屬性中獲取解析檔案路徑
        resolveFile = System.getProperty("dubbo.resolve.file");
        if (resolveFile == null || resolveFile.length() == 0) {
            // 從指定位置載入配置檔案
            File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties");
            if (userResolveFile.exists()) {
                // 獲取檔案絕對路徑
                resolveFile = userResolveFile.getAbsolutePath();
            }
        }
        if (resolveFile != null && resolveFile.length() > 0) {
            Properties properties = new Properties();
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(new File(resolveFile));
                // 從檔案中載入配置
                properties.load(fis);
            } catch (IOException e) {
                throw new IllegalStateException("Unload ..., cause:...");
            } finally {
                try {
                    if (null != fis) fis.close();
                } catch (IOException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
            // 獲取與介面名對應的配置
            resolve = properties.getProperty(interfaceName);
        }
    }
    if (resolve != null && resolve.length() > 0) {
        // 將 resolve 賦值給 url
        url = resolve;
    }
    
    // -------------------------------✨ 分割線2 ✨------------------------------
    if (consumer != null) {
        if (application == null) {
            // 從 consumer 中獲取 Application 例項,下同
            application = consumer.getApplication();
        }
        if (module == null) {
            module = consumer.getModule();
        }
        if (registries == null) {
            registries = consumer.getRegistries();
        }
        if (monitor == null) {
            monitor = consumer.getMonitor();
        }
    }
    if (module != null) {
        if (registries == null) {
            registries = module.getRegistries();
        }
        if (monitor == null) {
            monitor = module.getMonitor();
        }
    }
    if (application != null) {
        if (registries == null) {
            registries = application.getRegistries();
        }
        if (monitor == null) {
            monitor = application.getMonitor();
        }
    }
    
    // 檢測 Application 合法性
    checkApplication();
    // 檢測本地存根配置合法性
    checkStubAndMock(interfaceClass);
    
	// -------------------------------✨ 分割線3 ✨------------------------------
    
    Map<String, String> map = new HashMap<String, String>();
    Map<Object, Object> attributes = new HashMap<Object, Object>();

    // 新增 side、協議版本資訊、時間戳和程式號等資訊到 map 中
    map.put(Constants.SIDE_KEY, Constants.CONSUMER_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()));
    }

    // 非泛化服務
    if (!isGeneric()) {
        // 獲取版本
        String revision = Version.getVersion(interfaceClass, version);
        if (revision != null && revision.length() > 0) {
            map.put("revision", revision);
        }

        // 獲取介面方法列表,並新增到 map 中
        String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
        if (methods.length == 0) {
            map.put("methods", Constants.ANY_VALUE);
        } else {
            map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
        }
    }
    map.put(Constants.INTERFACE_KEY, interfaceName);
    // 將 ApplicationConfig、ConsumerConfig、ReferenceConfig 等物件的欄位資訊新增到 map 中
    appendParameters(map, application);
    appendParameters(map, module);
    appendParameters(map, consumer, Constants.DEFAULT_KEY);
    appendParameters(map, this);
    
	// -------------------------------✨ 分割線4 ✨------------------------------
    
    String prefix = StringUtils.getServiceKey(map);
    if (methods != null && !methods.isEmpty()) {
        // 遍歷 MethodConfig 列表
        for (MethodConfig method : methods) {
            appendParameters(map, method, method.getName());
            String retryKey = method.getName() + ".retry";
            // 檢測 map 是否包含 methodName.retry
            if (map.containsKey(retryKey)) {
                String retryValue = map.remove(retryKey);
                if ("false".equals(retryValue)) {
                    // 新增重試次數配置 methodName.retries
                    map.put(method.getName() + ".retries", "0");
                }
            }
 
            // 新增 MethodConfig 中的“屬性”欄位到 attributes
            // 比如 onreturn、onthrow、oninvoke 等
            appendAttributes(attributes, method, prefix + "." + method.getName());
            checkAndConvertImplicitConfig(method, map, attributes);
        }
    }
    
	// -------------------------------✨ 分割線5 ✨------------------------------

    // 獲取服務消費者 ip 地址
    String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY);
    if (hostToRegistry == null || hostToRegistry.length() == 0) {
        hostToRegistry = NetUtils.getLocalHost();
    } else if (isInvalidLocalHost(hostToRegistry)) {
        throw new IllegalArgumentException("Specified invalid registry ip from property..." );
    }
    map.put(Constants.REGISTER_IP_KEY, hostToRegistry);

    // 儲存 attributes 到系統上下文中
    StaticContext.getSystemContext().putAll(attributes);

    // 建立代理類
    ref = createProxy(map);

    // 根據服務名,ReferenceConfig,代理類構建 ConsumerModel,
    // 並將 ConsumerModel 存入到 ApplicationModel 中
    ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
    ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}
複製程式碼

上面的程式碼很長,做的事情比較多。這裡根據程式碼邏輯,對程式碼進行了分塊,下面我們一起來看一下。

首先是方法開始到分割線1之間的程式碼。這段程式碼主要用於檢測 ConsumerConfig 例項是否存在,如不存在則建立一個新的例項,然後通過系統變數或 dubbo.properties 配置檔案填充 ConsumerConfig 的欄位。接著是檢測泛化配置,並根據配置設定 interfaceClass 的值。接著來看分割線1到分割線2之間的邏輯。這段邏輯用於從系統屬性或配置檔案中載入與介面名相對應的配置,並將解析結果賦值給 url 欄位。url 欄位的作用一般是用於點對點呼叫。繼續向下看,分割線2和分割線3之間的程式碼用於檢測幾個核心配置類是否為空,為空則嘗試從其他配置類中獲取。分割線3與分割線4之間的程式碼主要用於收集各種配置,並將配置儲存到 map 中。分割線4和分割線5之間的程式碼用於處理 MethodConfig 例項。該例項包含了事件通知配置,比如 onreturn、onthrow、oninvoke 等。分割線5到方法結尾的程式碼主要用於解析服務消費者 ip,以及呼叫 createProxy 建立代理物件。關於該方法的詳細分析,將會在接下來的章節中展開。

3.2 引用服務

本節我們要從 createProxy 開始看起。從字面意思上來看,createProxy 似乎只是用於建立代理物件的。但實際上並非如此,該方法還會呼叫其他方法構建以及合併 Invoker 例項。具體細節如下。

private T createProxy(Map<String, String> map) {
    URL tmpUrl = new URL("temp", "localhost", 0, map);
    final boolean isJvmRefer;
    if (isInjvm() == null) {
        // url 配置被指定,則不做本地引用
        if (url != null && url.length() > 0) {
            isJvmRefer = false;
        // 根據 url 的協議、scope 以及 injvm 等引數檢測是否需要本地引用
        // 比如如果使用者顯式配置了 scope=local,此時 isInjvmRefer 返回 true
        } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
            isJvmRefer = true;
        } else {
            isJvmRefer = false;
        }
    } else {
        // 獲取 injvm 配置值
        isJvmRefer = isInjvm().booleanValue();
    }

    // 本地引用
    if (isJvmRefer) {
        // 生成本地引用 URL,協議為 injvm
        URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
        // 呼叫 refer 方法構建 InjvmInvoker 例項
        invoker = refprotocol.refer(interfaceClass, url);
        
    // 遠端引用
    } else {
        // url 不為空,表明使用者可能想進行點對點呼叫
        if (url != null && url.length() > 0) {
            // 當需要配置多個 url 時,可用分號進行分割,這裡會進行切分
            String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
            if (us != null && us.length > 0) {
                for (String u : us) {
                    URL url = URL.valueOf(u);
                    if (url.getPath() == null || url.getPath().length() == 0) {
                        // 設定介面全限定名為 url 路徑
                        url = url.setPath(interfaceName);
                    }
                    
                    // 檢測 url 協議是否為 registry,若是,表明使用者想使用指定的註冊中心
                    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        // 將 map 轉換為查詢字串,並作為 refer 引數的值新增到 url 中
                        urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    } else {
                        // 合併 url,移除服務提供者的一些配置(這些配置來源於使用者配置的 url 屬性),
                        // 比如執行緒池相關配置。並保留服務提供者的部分配置,比如版本,group,時間戳等
                        // 最後將合併後的配置設定為 url 查詢字串中。
                        urls.add(ClusterUtils.mergeUrl(url, map));
                    }
                }
            }
        } else {
            // 載入註冊中心 url
            List<URL> us = loadRegistries(false);
            if (us != null && !us.isEmpty()) {
                for (URL u : us) {
                    URL monitorUrl = loadMonitor(u);
                    if (monitorUrl != null) {
                        map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                    }
                    // 新增 refer 引數到 url 中,並將 url 新增到 urls 中
                    urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                }
            }

            // 未配置註冊中心,丟擲異常
            if (urls.isEmpty()) {
                throw new IllegalStateException("No such any registry to reference...");
            }
        }

        // 單個註冊中心或服務提供者(服務直連,下同)
        if (urls.size() == 1) {
            // 呼叫 RegistryProtocol 的 refer 構建 Invoker 例項
            invoker = refprotocol.refer(interfaceClass, urls.get(0));
            
        // 多個註冊中心或多個服務提供者,或者兩者混合
        } else {
            List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
            URL registryURL = null;

            // 獲取所有的 Invoker
            for (URL url : urls) {
                // 通過 refprotocol 呼叫 refer 構建 Invoker,refprotocol 會在執行時
                // 根據 url 協議頭載入指定的 Protocol 例項,並呼叫例項的 refer 方法
                invokers.add(refprotocol.refer(interfaceClass, url));
                if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                    registryURL = url;
                }
            }
            if (registryURL != null) {
                // 如果註冊中心連結不為空,則將使用 AvailableCluster
                URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
                // 建立 StaticDirectory 例項,並由 Cluster 對多個 Invoker 進行合併
                invoker = cluster.join(new StaticDirectory(u, invokers));
            } else {
                invoker = cluster.join(new StaticDirectory(invokers));
            }
        }
    }

    Boolean c = check;
    if (c == null && consumer != null) {
        c = consumer.isCheck();
    }
    if (c == null) {
        c = true;
    }
    
    // invoker 可用性檢查
    if (c && !invoker.isAvailable()) {
        throw new IllegalStateException("No provider available for the service...");
    }

    // 生成代理類
    return (T) proxyFactory.getProxy(invoker);
}
複製程式碼

上面程式碼很多,不過邏輯比較清晰。首先根據配置檢查是否為本地呼叫,若是,則呼叫 InjvmProtocol 的 refer 方法生成 InjvmInvoker 例項。若不是,則讀取直連配置項,或註冊中心 url,並將讀取到的 url 儲存到 urls 中。然後根據 urls 元素數量進行後續操作。若 urls 元素數量為1,則直接通過 Protocol 自適應擴充類構建 Invoker 例項介面。若 urls 元素數量大於1,即存在多個註冊中心或服務直連 url,此時先根據 url 構建 Invoker。然後再通過 Cluster 合併多個 Invoker,最後呼叫 ProxyFactory 生成代理類。Invoker 的構建過程以及代理類的過程比較重要,因此接下來將分兩小節對這兩個過程進行分析。

3.2.1 建立 Invoker

Invoker 是 Dubbo 的核心模型,代表一個可執行體。在服務提供方,Invoker 用於呼叫服務提供類。在服務消費方,Invoker 用於執行遠端呼叫。Invoker 是由 Protocol 實現類構建而來。Protocol 實現類有很多,本節會分析最常用的兩個,分別是 RegistryProtocol 和 DubboProtocol,其他的大家自行分析。下面先來分析 DubboProtocol 的 refer 方法原始碼。如下:

public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
    optimizeSerialization(url);
    // 建立 DubboInvoker
    DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
    invokers.add(invoker);
    return invoker;
}
複製程式碼

上面方法看起來比較簡單,不過這裡有一個呼叫需要我們注意一下,即 getClients。這個方法用於獲取客戶端例項,例項型別為 ExchangeClient。ExchangeClient 實際上並不具備通訊能力,它需要基於更底層的客戶端例項進行通訊。比如 NettyClient、MinaClient 等,預設情況下,Dubbo 使用 NettyClient 進行通訊。接下來,我們簡單看一下 getClients 方法的邏輯。

private ExchangeClient[] getClients(URL url) {
    // 是否共享連線
    boolean service_share_connect = false;
  	// 獲取連線數,預設為0,表示未配置
    int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
    // 如果未配置 connections,則共享連線
    if (connections == 0) {
        service_share_connect = true;
        connections = 1;
    }

    ExchangeClient[] clients = new ExchangeClient[connections];
    for (int i = 0; i < clients.length; i++) {
        if (service_share_connect) {
            // 獲取共享客戶端
            clients[i] = getSharedClient(url);
        } else {
            // 初始化新的客戶端
            clients[i] = initClient(url);
        }
    }
    return clients;
}
複製程式碼

這裡根據 connections 數量決定是獲取共享客戶端還是建立新的客戶端例項,預設情況下,使用共享客戶端例項。getSharedClient 方法中也會呼叫 initClient 方法,因此下面我們一起看一下這兩個方法。

private ExchangeClient getSharedClient(URL url) {
    String key = url.getAddress();
    // 獲取帶有“引用計數”功能的 ExchangeClient
    ReferenceCountExchangeClient client = referenceClientMap.get(key);
    if (client != null) {
        if (!client.isClosed()) {
            // 增加引用計數
            client.incrementAndGetCount();
            return client;
        } else {
            referenceClientMap.remove(key);
        }
    }

    locks.putIfAbsent(key, new Object());
    synchronized (locks.get(key)) {
        if (referenceClientMap.containsKey(key)) {
            return referenceClientMap.get(key);
        }

        // 建立 ExchangeClient 客戶端
        ExchangeClient exchangeClient = initClient(url);
        // 將 ExchangeClient 例項傳給 ReferenceCountExchangeClient,這裡使用了裝飾模式
        client = new ReferenceCountExchangeClient(exchangeClient, ghostClientMap);
        referenceClientMap.put(key, client);
        ghostClientMap.remove(key);
        locks.remove(key);
        return client;
    }
}
複製程式碼

上面方法先訪問快取,若快取未命中,則通過 initClient 方法建立新的 ExchangeClient 例項,並將該例項傳給 ReferenceCountExchangeClient 構造方法建立一個帶有引用計數功能的 ExchangeClient 例項。ReferenceCountExchangeClient 內部實現比較簡單,就不分析了。下面我們再來看一下 initClient 方法的程式碼。

private ExchangeClient initClient(URL url) {

    // 獲取客戶端型別,預設為 netty
    String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));

    // 新增編解碼和心跳包引數到 url 中
    url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
    url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));

    // 檢測客戶端型別是否存在,不存在則丟擲異常
    if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
        throw new RpcException("Unsupported client type: ...");
    }

    ExchangeClient client;
    try {
        // 獲取 lazy 配置,並根據配置值決定建立的客戶端型別
        if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
            // 建立懶載入 ExchangeClient 例項
            client = new LazyConnectExchangeClient(url, requestHandler);
        } else {
            // 建立普通 ExchangeClient 例項
            client = Exchangers.connect(url, requestHandler);
        }
    } catch (RemotingException e) {
        throw new RpcException("Fail to create remoting client for service...");
    }
    return client;
}
複製程式碼

initClient 方法首先獲取使用者配置的客戶端型別,預設為 netty。然後檢測使用者配置的客戶端型別是否存在,不存在則丟擲異常。最後根據 lazy 配置決定建立什麼型別的客戶端。這裡的 LazyConnectExchangeClient 程式碼並不是很複雜,該類會在 request 方法被呼叫時通過 Exchangers 的 connect 方法建立 ExchangeClient 客戶端,該類的程式碼本節就不分析了。下面我們分析一下 Exchangers 的 connect 方法。

public static ExchangeClient connect(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 例項,預設為 HeaderExchangeClient
    return getExchanger(url).connect(url, handler);
}
複製程式碼

如上,getExchanger 會通過 SPI 載入 HeaderExchangeClient 例項,這個方法比較簡單,大家自己看一下吧。接下來分析 HeaderExchangeClient 的實現。

public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
    // 這裡包含了多個呼叫,分別如下:
    // 1. 建立 HeaderExchangeHandler 物件
    // 2. 建立 DecodeHandler 物件
    // 3. 通過 Transporters 構建 Client 例項
    // 4. 建立 HeaderExchangeClient 物件
    return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
}
複製程式碼

這裡的呼叫比較多,我們這裡重點看一下 Transporters 的 connect 方法。如下:

public static Client connect(URL url, ChannelHandler... handlers) throws RemotingException {
    if (url == null) {
        throw new IllegalArgumentException("url == null");
    }
    ChannelHandler handler;
    if (handlers == null || handlers.length == 0) {
        handler = new ChannelHandlerAdapter();
    } else if (handlers.length == 1) {
        handler = handlers[0];
    } else {
        // 如果 handler 數量大於1,則建立一個 ChannelHandler 分發器
        handler = new ChannelHandlerDispatcher(handlers);
    }
    
    // 獲取 Transporter 自適應擴充類,並呼叫 connect 方法生成 Client 例項
    return getTransporter().connect(url, handler);
}
複製程式碼

如上,getTransporter 方法返回的是自適應擴充類,該類會在執行時根據客戶端型別載入指定的 Transporter 實現類。若使用者未配置客戶端型別,則預設載入 NettyTransporter,並呼叫該類的 connect 方法。如下:

public Client connect(URL url, ChannelHandler listener) throws RemotingException {
    // 建立 NettyClient 物件
    return new NettyClient(url, listener);
}
複製程式碼

到這裡就不繼續跟下去了,在往下就是通過 Netty 提供的 API 構建 Netty 客戶端了,大家有興趣自己看看。到這裡,關於 DubboProtocol 的 refer 方法就分析完了。接下來,繼續分析 RegistryProtocol 的 refer 方法邏輯。

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 取 registry 引數值,並將其設定為協議頭
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    // 獲取註冊中心例項
    Registry registry = registryFactory.getRegistry(url);
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }

    // 將 url 查詢字串轉為 Map
    Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
    // 獲取 group 配置
    String group = qs.get(Constants.GROUP_KEY);
    if (group != null && group.length() > 0) {
        if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
                || "*".equals(group)) {
            // 通過 SPI 載入 MergeableCluster 例項,並呼叫 doRefer 繼續執行服務引用邏輯
            return doRefer(getMergeableCluster(), registry, type, url);
        }
    }
    
    // 呼叫 doRefer 繼續執行服務引用邏輯
    return doRefer(cluster, registry, type, url);
}
複製程式碼

上面程式碼首先為 url 設定協議頭,然後根據 url 引數載入註冊中心例項。然後獲取 group 配置,根據 group 配置決定 doRefer 第一個引數的型別。這裡的重點是 doRefer 方法,如下:

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    // 建立 RegistryDirectory 例項
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    // 設定註冊中心和協議
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
    // 生成服務消費者連結
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);

    // 註冊服務消費者,在 consumers 目錄下新節點
    if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
            && url.getParameter(Constants.REGISTER_KEY, true)) {
        registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                Constants.CHECK_KEY, String.valueOf(false)));
    }

    // 訂閱 providers、configurators、routers 等節點資料
    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
            Constants.PROVIDERS_CATEGORY
                    + "," + Constants.CONFIGURATORS_CATEGORY
                    + "," + Constants.ROUTERS_CATEGORY));

    // 一個註冊中心可能有多個服務提供者,因此這裡需要將多個服務提供者合併為一個
    Invoker invoker = cluster.join(directory);
    ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
    return invoker;
}
複製程式碼

如上,doRefer 方法建立一個 RegistryDirectory 例項,然後生成服務者消費者連結,並向註冊中心進行註冊。註冊完畢後,緊接著訂閱 providers、configurators、routers 等節點下的資料。完成訂閱後,RegistryDirectory 會收到這幾個節點下的子節點資訊。由於一個服務可能部署在多臺伺服器上,這樣就會在 providers 產生多個節點,這個時候就需要 Cluster 將多個服務節點合併為一個,並生成一個 Invoker。關於 RegistryDirectory 和 Cluster,本文不打算進行分析,相關分析將會在隨後的文章中展開。

3.2.2 建立代理

Invoker 建立完畢後,接下來要做的事情是為服務介面生成代理物件。有了代理物件,即可進行遠端呼叫。代理物件生成的入口方法為 ProxyFactory 的 getProxy,接下來進行分析。

public <T> T getProxy(Invoker<T> invoker) throws RpcException {
    // 呼叫過載方法
    return getProxy(invoker, false);
}

public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
    Class<?>[] interfaces = null;
    // 獲取介面列表
    String config = invoker.getUrl().getParameter("interfaces");
    if (config != null && config.length() > 0) {
        // 切分介面列表
        String[] types = Constants.COMMA_SPLIT_PATTERN.split(config);
        if (types != null && types.length > 0) {
            interfaces = new Class<?>[types.length + 2];
            // 設定服務介面類和 EchoService.class 到 interfaces 中
            interfaces[0] = invoker.getInterface();
            interfaces[1] = EchoService.class;
            for (int i = 0; i < types.length; i++) {
                // 載入介面類
                interfaces[i + 1] = ReflectUtils.forName(types[i]);
            }
        }
    }
    if (interfaces == null) {
        interfaces = new Class<?>[]{invoker.getInterface(), EchoService.class};
    }

    // 為 http 和 hessian 協議提供泛化呼叫支援,參考 pull request #1827
    if (!invoker.getInterface().equals(GenericService.class) && generic) {
        int len = interfaces.length;
        Class<?>[] temp = interfaces;
        // 建立新的 interfaces 陣列
        interfaces = new Class<?>[len + 1];
        System.arraycopy(temp, 0, interfaces, 0, len);
        // 設定 GenericService.class 到陣列中
        interfaces[len] = GenericService.class;
    }

    // 呼叫過載方法
    return getProxy(invoker, interfaces);
}

public abstract <T> T getProxy(Invoker<T> invoker, Class<?>[] types);
複製程式碼

如上,上面大段程式碼都是用來獲取 interfaces 陣列的,我們繼續往下看。getProxy(Invoker, Class<?>[]) 這個方法是一個抽象方法,下面我們到 JavassistProxyFactory 類中看一下該方法的實現程式碼。

public T getProxy(Invoker invoker, Class<?>[] interfaces) { // 生成 Proxy 子類(Proxy 是抽象類)。並呼叫 Proxy 子類的 newInstance 方法建立 Proxy 例項 return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker)); } 上面程式碼並不多,首先是通過 Proxy 的 getProxy 方法獲取 Proxy 子類,然後建立 InvokerInvocationHandler 物件,並將該物件傳給 newInstance 生成 Proxy 例項。InvokerInvocationHandler 實現自 JDK 的 InvocationHandler 介面,具體的用途是攔截介面類呼叫。該類邏輯比較簡單,這裡就不分析了。下面我們重點關注一下 Proxy 的 getProxy 方法,如下。

public static Proxy getProxy(Class<?>... ics) {
    // 呼叫過載方法
    return getProxy(ClassHelper.getClassLoader(Proxy.class), ics);
}

public static Proxy getProxy(ClassLoader cl, Class<?>... ics) {
    if (ics.length > 65535)
        throw new IllegalArgumentException("interface limit exceeded");

    StringBuilder sb = new StringBuilder();
    // 遍歷介面列表
    for (int i = 0; i < ics.length; i++) {
        String itf = ics[i].getName();
        // 檢測型別是否為介面
        if (!ics[i].isInterface())
            throw new RuntimeException(itf + " is not a interface.");

        Class<?> tmp = null;
        try {
            // 重新載入介面類
            tmp = Class.forName(itf, false, cl);
        } catch (ClassNotFoundException e) {
        }

        // 檢測介面是否相同,這裡 tmp 有可能為空
        if (tmp != ics[i])
            throw new IllegalArgumentException(ics[i] + " is not visible from class loader");

        // 拼接介面全限定名,分隔符為 ;
        sb.append(itf).append(';');
    }

    // 使用拼接後的介面名作為 key
    String key = sb.toString();

    Map<String, Object> cache;
    synchronized (ProxyCacheMap) {
        cache = ProxyCacheMap.get(cl);
        if (cache == null) {
            cache = new HashMap<String, Object>();
            ProxyCacheMap.put(cl, cache);
        }
    }

    Proxy proxy = null;
    synchronized (cache) {
        do {
            // 從快取中獲取 Reference<Proxy> 例項
            Object value = cache.get(key);
            if (value instanceof Reference<?>) {
                proxy = (Proxy) ((Reference<?>) value).get();
                if (proxy != null) {
                    return proxy;
                }
            }

            // 併發控制,保證只有一個執行緒可以進行後續操作
            if (value == PendingGenerationMarker) {
                try {
                    // 其他執行緒在此處進行等待
                    cache.wait();
                } catch (InterruptedException e) {
                }
            } else {
                // 放置標誌位到快取中,並跳出 while 迴圈進行後續操作
                cache.put(key, PendingGenerationMarker);
                break;
            }
        }
        while (true);
    }

    long id = PROXY_CLASS_COUNTER.getAndIncrement();
    String pkg = null;
    ClassGenerator ccp = null, ccm = null;
    try {
        // 建立 ClassGenerator 物件
        ccp = ClassGenerator.newInstance(cl);

        Set<String> worked = new HashSet<String>();
        List<Method> methods = new ArrayList<Method>();

        for (int i = 0; i < ics.length; i++) {
            // 檢測介面訪問級別是否為 protected 或 privete
            if (!Modifier.isPublic(ics[i].getModifiers())) {
                // 獲取介面包名
                String npkg = ics[i].getPackage().getName();
                if (pkg == null) {
                    pkg = npkg;
                } else {
                    if (!pkg.equals(npkg))
                        // 非 public 級別的介面必須在同一個包下,否者丟擲異常
                        throw new IllegalArgumentException("non-public interfaces from different packages");
                }
            }
            
            // 新增介面到 ClassGenerator 中
            ccp.addInterface(ics[i]);

            // 遍歷介面方法
            for (Method method : ics[i].getMethods()) {
                // 獲取方法描述,可理解為方法簽名
                String desc = ReflectUtils.getDesc(method);
                // 如果方法描述字串已在 worked 中,則忽略。考慮這種情況,
                // A 介面和 B 介面中包含一個完全相同的方法
                if (worked.contains(desc))
                    continue;
                worked.add(desc);

                int ix = methods.size();
                // 獲取方法返回值型別
                Class<?> rt = method.getReturnType();
                // 獲取引數列表
                Class<?>[] pts = method.getParameterTypes();

                // 生成 Object[] args = new Object[1...N]
                StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];");
                for (int j = 0; j < pts.length; j++)
                    // 生成 args[1...N] = ($w)$1...N;
                    code.append(" args[").append(j).append("] = ($w)$").append(j + 1).append(";");
                // 生成 InvokerHandler 介面的 invoker 方法呼叫語句,如下:
                // Object ret = handler.invoke(this, methods[1...N], args);
                code.append(" Object ret = handler.invoke(this, methods[" + ix + "], args);");

                // 返回值不為 void
                if (!Void.TYPE.equals(rt))
                    // 生成返回語句,形如 return (java.lang.String) ret;
                    code.append(" return ").append(asArgument(rt, "ret")).append(";");

                methods.add(method);
                // 新增方法名、訪問控制符、引數列表、方法程式碼等資訊到 ClassGenerator 中 
                ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(), code.toString());
            }
        }

        if (pkg == null)
            pkg = PACKAGE_NAME;

        // 構建介面代理類名稱:pkg + ".proxy" + id,比如 org.apache.dubbo.proxy0
        String pcn = pkg + ".proxy" + id;
        ccp.setClassName(pcn);
        ccp.addField("public static java.lang.reflect.Method[] methods;");
        // 生成 private java.lang.reflect.InvocationHandler handler;
        ccp.addField("private " + InvocationHandler.class.getName() + " handler;");

        // 為介面代理類新增帶有 InvocationHandler 引數的構造方法,比如:
        // porxy0(java.lang.reflect.InvocationHandler arg0) {
        //     handler=$1;
    	// }
        ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{InvocationHandler.class}, new Class<?>[0], "handler=$1;");
        // 為介面代理類新增預設構造方法
        ccp.addDefaultConstructor();
        
        // 生成介面代理類
        Class<?> clazz = ccp.toClass();
        clazz.getField("methods").set(null, methods.toArray(new Method[0]));

        // 構建 Proxy 子類名稱,比如 Proxy1,Proxy2 等
        String fcn = Proxy.class.getName() + id;
        ccm = ClassGenerator.newInstance(cl);
        ccm.setClassName(fcn);
        ccm.addDefaultConstructor();
        ccm.setSuperClass(Proxy.class);
        // 為 Proxy 的抽象方法 newInstance 生成實現程式碼,形如:
        // public Object newInstance(java.lang.reflect.InvocationHandler h) { 
        //     return new org.apache.dubbo.proxy0($1);
        // }
        ccm.addMethod("public Object newInstance(" + InvocationHandler.class.getName() + " h){ return new " + pcn + "($1); }");
        // 生成 Proxy 實現類
        Class<?> pc = ccm.toClass();
        // 通過反射建立 Proxy 例項
        proxy = (Proxy) pc.newInstance();
    } catch (RuntimeException e) {
        throw e;
    } catch (Exception e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        if (ccp != null)
            // 釋放資源
            ccp.release();
        if (ccm != null)
            ccm.release();
        synchronized (cache) {
            if (proxy == null)
                cache.remove(key);
            else
                // 寫快取
                cache.put(key, new WeakReference<Proxy>(proxy));
            // 喚醒其他等待執行緒
            cache.notifyAll();
        }
    }
    return proxy;
}
複製程式碼

上面程式碼比較複雜,我們寫了大量的註釋。大家在閱讀這段程式碼時,要搞清楚 ccp 和 ccm 的用途,不然會被搞暈。ccp 用於為服務介面生成代理類,比如我們有一個 DemoService 介面,這個介面代理類就是由 ccp 生成的。ccm 則是用於為 org.apache.dubbo.common.bytecode.Proxy 抽象類生成子類,主要是實現 Proxy 類的抽象方法。下面以 org.apache.dubbo.demo.DemoService 這個介面為例,來看一下該介面代理類程式碼大致是怎樣的(忽略 EchoService 介面)。

package org.apache.dubbo.common.bytecode;

public class proxy0 implements org.apache.dubbo.demo.DemoService {

    public static java.lang.reflect.Method[] methods;

    private java.lang.reflect.InvocationHandler handler;

    public proxy0() {
    }

    public proxy0(java.lang.reflect.InvocationHandler arg0) {
        handler = $1;
    }

    public java.lang.String sayHello(java.lang.String arg0) {
        Object[] args = new Object[1];
        args[0] = ($w) $1;
        Object ret = handler.invoke(this, methods[0], args);
        return (java.lang.String) ret;
    }
}
複製程式碼

好了,到這裡代理類生成邏輯就分析完了。整個過程比較複雜,大家需要耐心看一下。

4.總結

本篇文章對服務引用的過程進行了較為詳盡的分析,還有一些邏輯暫時沒有分析到,比如 Directory、Cluster。這些介面及實現類功能比較獨立,後續會單獨成文進行分析。暫時我們可以先把這些類看成黑盒,只要知道這些類的用途即可。關於服務引用過程就分析到這裡。

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

相關文章