今天打算来讲一讲 Dubbo 服务远程调用。笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊。后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现。本地消费者无须知道远程服务具体的实现,消费者和提供者通过代理类来进行交互!!

一、JAVA 动态代理

简单看一段代码回顾一下动态代理:

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

实现动态代理的核心步骤有两步:

1、自定义实现了 InvocationHandler 接口的 handler 类,并重写 invoke() 方法

2、调用 Proxy.newProxyInstance 方法创建代理类

我们最后在调用 proxy.sayHello() 的时候,代理类会调用 MyInvocationHandler 类的 invoke() 方法,invoke() 方法通过反射机制最终调用 sayHello() 方法。既然对动态代理的核心组成已经了然,接下来我们就结合这两点分析下 Dubbo 远程服务调用的实现。

二、远程服务代理类的创建

创建时机

【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);

在这一小节,我们只需要对代理类创建流程有个大致的印象即可,我们在后文深入分析具体流程。

创建 invoker

    invoker = refprotocol.refer(interfaceClass, urls.get(0));

熟悉的配方熟悉的料,通过 Dubbo SPI 机制我们发现这里调用的实际为 RegistryProtocol.refer(),问我为啥?详见:【Dubbo源码阅读系列】之 Dubbo SPI 机制

refprotocol.refer() 流程比较长,先放张时序图让大家有个基本的印象:

这里简单概括下上图中的重点内容:

  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() ==》MockClusterInvoker.join() ==> FailoverCluster().join() ==> FailoverClusterInvoker() ==> AbstractClusterInvoker()

    最后返回的为一个 MockClusterInvoker 对象

DubboInvoker 对象创建流程

在上节中关于 invoker 的创建我们留了个小尾巴没有讲完。代码如下:

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() 方法最后一句:

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

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

【Dubbo源码阅读系列】之远程服务调用(上)的更多相关文章

  1. 【Dubbo源码阅读系列】服务暴露之远程暴露

    引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...

  2. 【Dubbo源码阅读系列】服务暴露之本地暴露

    在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...

  3. 【Dubbo源码阅读系列】之 Dubbo SPI 机制

    最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...

  4. 【Dubbo源码阅读系列】之 Dubbo XML 配置加载

    今天我们来谈谈 Dubbo XML 配置相关内容.关于这部分内容我打算分为以下几个部分进行介绍: Dubbo XML Spring 自定义 XML 标签解析 Dubbo 自定义 XML 标签解析 Du ...

  5. 源码阅读系列:EventBus

    title: 源码阅读系列:EventBus date: 2016-12-22 16:16:47 tags: 源码阅读 --- EventBus 是人们在日常开发中经常会用到的开源库,即使是不直接用的 ...

  6. JDK1.8源码阅读系列之三:Vector

    本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...

  7. DM 源码阅读系列文章(六)relay log 的实现

    2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第六篇,在 上篇文章 中我们介绍了 binlog replication 处理单 ...

  8. Spring源码阅读系列总结

    最近一段时间,粗略的查看了一下Spring源码,对Spring的两大核心和Spring的组件有了更深入的了解.同时在学习Spring源码时,得了解一些设计模式,不然阅读源码还是有一定难度的,所以一些重 ...

  9. SpringMVC源码阅读系列汇总

    1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...

随机推荐

  1. PHP 如何向关联数组指定的 Key 之前插入元素

    PHP 关联数组可以通过三种方式插入新元素: $array[$insert_key] = $insert_value; $array = array_merge($array, $insert_arr ...

  2. 结合java的反射和泛型性质简化JDBC和相应的同步等服务器数据库操作代码

    github地址:https://github.com/hzphzp/HeartTrace_Server 我们的服务器端数据库并没有用sqllite, 而是直接用mysql,并且用JDBC直接进行操作 ...

  3. 使用ajax请求后返回数据显示undefinded解决办法

    今天在使用七牛的sdk做断点续传时候,当文件过大,本地没有存储时候,想要通过ajax将本地信息存到服务器缓存,之后通过读取缓存的方式来完成文件过大断点续传无法使用问题. 但是在使用ajax请求的时候, ...

  4. C#字节流通信格式

    类似通信格式的实现关键点: byte数组转float的实现,BitConvetor.toSingle() float类型转by,BitConverter.GetBytes #客户端发送public b ...

  5. 为什么懂云的IT高手能过得比你好

    盼望着,盼望着,一年一度的国庆7天长假还有不到24小时就到来了.各个部门的同事都已准备好满世界旅行去了. IT 部门各位同事的心还是悬着,信息系统还要持续的运转,对外的网站不能停,假期的线上促销也不能 ...

  6. Tcpdump一些常用指令

    1.tcpdump安装:yum install tcpdump 2.关键字介绍 类型关键字: 指定主机 host 192.168.1.111 指定网络地址 net 202.0.0.0 指定端口 por ...

  7. 企业级Apache详解

    安装Apache #Apache安装 rpm -qa|grep httpd yum install httpd #2编译安装: -->推荐安装 cd /root/software yum -y ...

  8. 为什么TCP协议终止链接要四次?

    为什么TCP协议终止链接要四次? 1.当主机A确认发送完数据且知道B已经接受完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给主机B. 2.主机B收到A发送的FIN,表示收到了,就会发 ...

  9. 栋哥你好,让我们回顾最初认识C++的时候(课堂作业)

    计算器的第一步,至今还记记忆犹新,本次的课堂作业,便是那个框架.闲话少叙,代码如下传送门: Main.cpp #include "stdafx.h" #include<ios ...

  10. Programming Assignment 3: Baseball Elimination

    编程作业三 作业链接:Baseball Elimination & Checklist 我的代码:BaseballElimination.java 问题简介 这是一个最大流模型的实际应用问题: ...