【Dubbo源碼閱讀系列】之遠程服務調用(上)

今天打算來說一講 Dubbo 服務遠程調用。筆者在開始看 Dubbo 遠程服務相關源碼的時候,看的有點迷糊。後來慢慢明白 Dubbo 遠程服務的調用的本質就是動態代理模式的一種實現。本地消費者無須知道遠程服務具體的實現,消費者和提供者經過代理類來進行交互!!java

1、JAVA 動態代理

簡單看一段代碼回顧一下動態代理:apache

public class MyInvocationHandler implements InvocationHandler{
	private Object object;
	
	public MyInvocationHandler(Object object){
		this.object = object;
	}
 
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object result = method.invoke(object, args);
		return result;
	}
}

public static void main(String[] args) {
	MyInvocationHandler handler = new MyInvocationHandler(stu);
	// 生成代理類
	Student proxy = (Student)Proxy.newProxyInstance(loader, interfaces, handler);
	// 經過代理類調用 sayHello 方法
	proxy.sayHello(message);
}
複製代碼

實現動態代理的核心步驟有兩步:
一、自定義實現了 InvocationHandler 接口的 handler 類,並重寫 invoke() 方法
二、調用 Proxy.newProxyInstance 方法建立代理類bootstrap

咱們最後在調用 proxy.sayHello() 的時候,代理類會調用 MyInvocationHandler 類的 invoke() 方法,invoke() 方法經過反射機制最終調用 sayHello() 方法。既然對動態代理的核心組成已經瞭然,接下來咱們就結合這兩點分析下 Dubbo 遠程服務調用的實現。windows

2、遠程服務代理類的建立

建立時機

【Dubbo源碼閱讀系列】之 Dubbo XML 配置加載 中咱們分析了 Dubbo 解析 XML 配置文件的相關流程,其中 <dubbo:service /> 標籤會被解析爲 ServiceBean 對象。相似的,<dubbo:reference /> 標籤會被解析爲 ReferenceBean 對象。ReferenceBean 類繼承自 ReferenceConfig 類,仔細觀察 ReferenceConfig 類不難發現 ReferenceConfig 中存在這樣一條調用鏈。
get() ==> init() ==> createProxy()
不要懷疑...crateProxy() 方法就是咱們今天的主角...更使人激動的是,咱們能夠在該方法中找到與前文概括的動態代理實現核心步驟相對應的代碼實現:緩存

  • 建立 invoker 對象
    invoker = refprotocol.refer(interfaceClass, urls.get(0));  
    複製代碼
    這裏返回的對象爲 Invoker 對象。Invoke 類是一個接口類,裏面定義了一個 invoke() 方法。
  • 調用工廠類建立代理對象
    return (T) proxyFactory.getProxy(invoker);  
    複製代碼

在這一小節,咱們只須要對代理類建立流程有個大體的印象便可,咱們在後文深刻分析具體流程。bash

建立 invoker

invoker = refprotocol.refer(interfaceClass, urls.get(0)); 
複製代碼

熟悉的配方熟悉的料,經過 Dubbo SPI 機制咱們發現這裏調用的實際爲 RegistryProtocol.refer(),問我爲啥?詳見:【Dubbo源碼閱讀系列】之 Dubbo SPI 機制
refprotocol.refer() 流程比較長,先放張時序圖讓你們有個基本的印象:app

這裏簡單歸納下上圖中的重點內容:jvm

  1. refProtocolProtocol.refer()
    上面咱們已經提了這裏的 refer() 方法最終調用的是 RegistryProtocol.refer() 方法。
    ** 在 refer() 方法中首先會調用 registryFactory.getRegistry(url) 獲取 Registry 對象(Dubbo SPI 機制);
    ** 接着調用 doRefer() 方法。
  2. doRefer()
    • registry.register() 在 step3 介紹
    • directory.subscribe() 在 step4 介紹
  3. registry.register()
    筆者在調試時,用的註冊中心爲 zookeeper(實際官方也推薦),所以這裏會在 zookeeper 上建立一個節點。節點相似:/dubbo/org.apache.dubbo.service.DemoService/consumers/url,若是沒有設置 dynamic 參數,默認爲臨時節點;
  4. RegistryDirectory.subscribe(url)
    • 當前 url 中的 category 參數值被設置成了:providers,consumers,routers。 接着調用 registry.subscribe(url, this) 方法,不難分析最後調用的是 FailbackRegistry 類的 subscribe() 方法。
    • 另外須要注意 RegistryDirectory 類實現了 NotifyListener 接口中的 notify() 方法:
  5. FailbackRegistry.subscribe()
    • 調用 doSubscribe() 方法,在 Step6 介紹
  6. ZookeeperRegistry.doSubscribe()
    • url 會被 toCategoriesPath() 方法轉換相似以下形式的 path 集合
      /dubbo/org.apache.dubbo.service.DemoService/providers
      /dubbo/org.apache.dubbo.service.DemoService/consumers
      /dubbo/org.apache.dubbo.service.DemoService/routers
    • 在 zookeeper 上建立對應的路徑節點,同時添加監聽器,這裏監聽器檢測到變化會執行 ZookeeperRegistry 類的 notify() 方法
    • 若是對應 path 節點子節點爲空,設置 url 的 protocol 值爲 empty;子節點不爲空,符合條件的 url 會被添加到 urls 集合中 。PS:若是服務提供方服務已經成功啓動,/dubbo/org.apache.dubbo.service.DemoService/providers 路徑下應該會子節點。
    • 執行 notify 方法
  7. notify(url, listener, urls);
    執行 doNotify() 方法,見 step8
  8. doNotify(url, listener, urls) 調用父類的 notify() 方法
  9. AbstractRegistry.notify(URL url, NotifyListener listener, List urls)
    • 這裏拿到的 urls 是 step6 中生成的。咱們根據 url 中的 category 值對其分類,最後放到一個 map 集合中(key 爲 category,value 爲 url 集合)
    • 遍歷上面生成的 map 集合,執行 listener.notify(categoryList) 方法。這裏的 listener 爲 RegistryDirecotry 對象,在 step4 中做爲參數開始傳遞;
  10. RegistryDirectory.notify(categoryList)
    • 遍歷 categoryList,將 category 值爲 providers 的 url 添加到 invokerUrls 集合中
    • 執行 refreshInvoker(invokerUrls) 方法
  11. refreshInvoker(invokerUrls) refreshInvoker 很是重要,用於將 invokerUrls 轉換爲 invoker 對象。
    • 若是 invokerUrls 只有一條記錄,且該條記錄的 protocol 參數值爲 empty,禁止訪問
    • 調用 toInvokers() 方法
  12. toInvokers(List urls)
    這裏會維護一個本地緩存 urlInvokerMap,key 值爲 url 字符串;
    • 遍歷 urls ,若是 urlInvokerMap 集合中 url 對應的 value 不爲空,執行以下代碼:
      invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
      複製代碼
      這裏會新建一個 DubboInvoker 對象並返回,咱們會在後文詳細分析;
    • 直接取緩存中的值,不會從新構造 invoker 對象;
  13. toMethodInvokers step12 中最終會生成一個 key 爲 url,value 爲 invoker 集合。在這裏進行處理後最後返回的集合 key 值爲 method,value 爲 invoker 集合
  14. cluster.join(directory); 這裏最後又用到 Dubbo SPI 機制,實際調用流程爲: Cluster$Adaptive.join() ==》MockClusterWrapper.join() ==> FailoverCluster().join() ==> FailoverClusterInvoker() ==> AbstractClusterInvoker()
    最後返回的爲一個 MockClusterInvoker 對象

DubboInvoker 對象建立流程

在上節中關於 invoker 的建立咱們留了個小尾巴沒有講完。代碼以下:ide

invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
複製代碼
  1. protocol.refer() 這裏又用到了 Dubbo SPI 機制,照例給出簡單的調用流程: Protocol$Adaptive.refer() ==》 ProtocolListenerWrapper.refer() ==》 ProtocolFilterWrapper.refer() ==》 DubboProtocol.refer()
    其中在 DubboProtocol.refer() 方法中會構建 DubboInvoker 對象。
    DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
    複製代碼
    總體流程比較簡單,可是注意看,這裏有個很重要的方法:getClient(url)。它是用來幹啥的?還記得咱們再服務暴露之遠程暴露那一節啓動了 Netty 服務端嗎?當時留了個關於 Netty 客戶端在哪裏啓動的坑。這裏的 getClients() 就是用來開啓 Netty 客戶端的。
  2. getClients(url)
    若是 url 中沒有設置 connections 參數,默認共享連接,調用 getSharedClient() 獲取 ExchangeClient 對象。
  3. getSharedClient(URL url) getSharedClient() 顧名思義是用於獲取共享客戶端的。referenceClientMap 集合用於緩存 client,key 值爲 url 的 address 參數。若是取緩存時對應值爲 null ,會調用 initClient(url) 方法新建 ExchangeClient
  4. initClient(url)
    調用 Exchangers.connect() 方法構建 client ,最後返回的 client 會經過構造方法被賦值到到 DubboInvoker 類的 clients 成員變量中;
  5. Exchangers.connect()
    return getExchanger(url).connect(url, handler);
    複製代碼
    • 調用 getExchanger(url) 獲取 HeaderExchanger 類(Dubbo SPI 機制)
    • 調用 HeaderExchanger.connet() 方法創建鏈接
  6. HeaderExchanger.connect()
    public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
        return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
    }
    複製代碼
    核心方法爲 Transporters.connect()
  7. Transporters.connect()
    return getTransporter().connect(url, handler);
    複製代碼
    • 調用 getTransporter() 方法獲取 NettyTransporter 類(Dubbo SPI 機制)
    • 調用 NettyTransporter.connet() 方法創建鏈接
  8. NettyTransporter.connet()
    public Client connect(URL url, ChannelHandler listener) throws RemotingException {
        return new NettyClient(url, listener);
    }
    複製代碼
    NettyClient 類構造方法會調用父類 AbstractClient 構造方法。核心方法有兩個:
    • doOpen() 初始化 bootstrap
    • connect() 創建鏈接 小結:本節咱們介紹 DubboInvoker 對象的建立流程,而且介紹 Netty 客戶端鏈接建立時機。至此爲止 Invoker 的建立流程算是大體的過了一遍!

4.建立代理類

終於要開始建立代理類了,回顧下 ReferenceConfig 中 createProxy() 方法最後一句:post

return (T) proxyFactory.getProxy(invoker);  
複製代碼

Invoker 對象的建立已經在第二小節詳細分析過了。那麼 proxyFactory 的 getProxy() 到底幹了什麼呢?實際上這裏又藉助了 Dubbo SPI 機制的實現。執行流程大體爲:
proxyFactory.getProxy(invoker) ==》 StubProxyFactoryWrapper.getProxy(invoker) ==》AbstractProxyFactory.getProxy(invoker) ==》AbstractProxyFactory.getProxy(invoker, generic) ==> JavassistProxyFactory.getProxy(invoker, interfaces)
重點看 JavassistProxyFactory.getProxy() 方法:

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
    return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
複製代碼

這裏有兩點值得提一下:

  • InvokerInvocationHandler 類實現了 InvocationHandler 接口,是否是有種很熟悉的感受;
  • Proxy.getProxy(interfaces) 使用了 javassist 字節碼技術生成動態代理,相似的文章網上比較多,這裏就不贅述了。生成的代理類以下所示:
    package org.apache.dubbo.common.bytecode;
    
    import com.alibaba.dubbo.rpc.service.EchoService;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import org.apache.dubbo.common.bytecode.ClassGenerator.DC;
    import org.apache.dubbo.demo.DemoService;
    
    public class proxy0 implements DC, EchoService, DemoService {
        public static Method[] methods;
        private InvocationHandler handler;
    
        public proxy0(InvocationHandler var1) {
            this.handler = var1;
        }
    
        public proxy0() {
        }
    
        public String sayHello(String var1) {
            Object[] var2 = new Object[]{var1};
            Object var3 = this.handler.invoke(this, methods[0], var2);
            return (String)var3;
        }
    
        public Object $echo(Object var1) {
            Object[] var2 = new Object[]{var1};
            Object var3 = this.handler.invoke(this, methods[1], var2);
            return (Object)var3;
        }
    }
    複製代碼

最後囉嗦一下如何在 windows 系統下查看使用 javassist 字節碼技術生成的代理類!!

  • 進入當前使用 jdk 目錄,例如:C:\Program Files\Java\jdk1.8.0\
  • 執行指令 java -cp lib/sa-jdi.jar sun.jvm.hotspot.HSDB 後會彈出一個對話框
  • 點擊 File ==》Attach to HotSpot process,輸入當前進程 PID(最傻瓜的辦法...任務管理器...)
  • 若是提示 sawindbg.dll 找不到,不要慌...到 C:\Program Files\Java\jdk1.8.0\jre\bin 下找找?
  • 最後點擊選擇 Tools ==> Class Browser 就能夠看到不少 class 了...
  • 選中某個 calss ,點擊 Create .class File 就會在當前目錄下建立一個 .class 文件了

小結:這一小節寫的比較水~不過不要緊,意思已經到了!!

相關文章
相關標籤/搜索