【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 文件了

小结:这一小节写的比较水~不过不要紧,意思已经到了!!