【Dubbo源码阅读系列】之远程服务调用(上)

今天打算来讲一讲 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 配置文件的相关流程,其中 标签会被解析为 ServiceBean 对象。类似的, 标签会被解析为 ReferenceBean 对象。ReferenceBean 类继承自 ReferenceConfig 类,仔细观察 ReferenceConfig 类不难发现 ReferenceConfig 中存在这样一条调用链。
get() ==> init() ==> createProxy()
不要怀疑...crateProxy() 方法就是我们今天的主角...更令人激动的是,我们可以在该方法中找到与前文归纳的动态代理实现核心步骤相对应的代码实现:

  • 创建 invoker 对象
    java invoker = refprotocol.refer(interfaceClass, urls.get(0));
    这里返回的对象为 Invoker 对象。Invoke 类是一个接口类,里面定义了一个 invoke() 方法。
  • 调用工厂类创建代理对象
    java 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 不为空,执行如下代码:
      java 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()
    java return getExchanger(url).connect(url, handler);

    • 调用 getExchanger(url) 获取 HeaderExchanger 类(Dubbo SPI 机制)
    • 调用 HeaderExchanger.connet() 方法建立连接
  6. HeaderExchanger.connect()
    java 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()
    java return getTransporter().connect(url, handler);

    • 调用 getTransporter() 方法获取 NettyTransporter 类(Dubbo SPI 机制)
    • 调用 NettyTransporter.connet() 方法建立连接
  8. NettyTransporter.connet()
    java 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 字节码技术生成动态代理,类似的文章网上比较多,这里就不赘述了。生成的代理类如下所示:
    ```java
    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 文件了

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

原文地址:https://www.cnblogs.com/cfyrwang/p/10344925.html

时间: 2024-10-08 20:27:09

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

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

引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A 提供了 Message 功能?那么我们是不是可以把目前已提供的服务暴露在一个地方,让调用方知道某台机器提供了某个特定功能?带着这样的假设,我们今天就来聊聊 Dubbo 服务暴露之远程暴露!! 服务远程暴露 先回顾一下上篇文章,上篇文章我们聊到了 ServiceConfig 的 export() 方

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line:

Dubbo源码分析系列-服务的发布

RPC简化类图 RPC模块核心接口和抽象实现 默认实现Dubbo协议的接口和抽象实现 服务发布过程 调用过程 上图是服务提供者暴露服务的主过程: 首先ServiceConfig类拿到对外提供服务的实际类ref(如:HelloWorldImpl),然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转化.接下来就是Invoker转换到Exporter的过程. Dubbo处理服务暴露的关键

Spring源码阅读系列总结

最近一段时间,粗略的查看了一下Spring源码,对Spring的两大核心和Spring的组件有了更深入的了解.同时在学习Spring源码时,得了解一些设计模式,不然阅读源码还是有一定难度的,所以一些重要的设计模式简单的做了阐述.同时还会简单的加入一些GOF中提到的设计原则.Spring的源码阅读系列,也暂告一段落.下面是就带你走进Spring世界: Spring系列的引子 1)Spring WebApplicationContext初始化与消亡 这一节帮我们了解Spring是如何初始化WebAp

源码阅读系列:源码阅读方法

一.前提条件 1.纯熟扎实的语言基础 ??如果你学java,却对反射.泛型.注解一直半解,还是不要去读什么框架了,回去把java基础打扎实反而对你自身更有益. 2.UML能力 ??在软件工程中,UML在软件的不同生命周期阶段扮演着非常重要的角色,没有好的UML水平,面对大型的项目源码会束手无策. 3.对业务的理解 ??如果你要阅读的项目业务性比较强,事先对业务有一定的了解是必须的. 4.设计模式.重构的掌握 ??编程语言什么的没什么好说.着重提一个:设计模式由于Android源代码用到各种各样的

dubbo源码阅读-阅读前的准备(一)

说明 之前自己看了一篇dubbo源码,但是对整体还是没有清晰的了解所以重新跟着别人的博文整理一遍 获取源码 github代码fork 1.打开https://github.com/alibaba/dubbo fork到自己仓库,为了后续自己看的过程中会写一些自己的注释 从自己仓库将项目拉取到本地 模块划分 项目结构一览 官方dubbo框架设计文档地址 dubbo-common 提供 Util 类和通用模型 dubbo-remoting 远程通信模块:提供通用的客户端和服务端的通讯功能. 原文地址

dubbo源码分析系列——项目工程结构介绍

摘要 dubbo是目前国内最流行的分布式服务框架,已经俨然成为行业的标准了,多数无自研能力的公司都在使用这个框架,而且这个框架本身非常具有代表性,即使很多公司自研的分布式服务框架也都是对dubbo的扩展或者借鉴了该框架,因此研究它的源码意义还是非常大的,对于掌握分布式服务框架的原理和实现细节有着非常好的帮助. 项目源码地址 本系列文章是基于当当网维护的dubbox版本进行分析的,源码地址参考:https://github.com/dangdangdotcom/dubbox 项目源码结构 我们下载

【Dubbo 源码解析】06_Dubbo 服务调用

Dubbo 服务调用 根据上图,可以看出,服务调用过程为: Consumer 端的 Proxy 调用 Cluster 层选择集群中的某一个 Invoker(负载均衡) Invoker 最终会调用 Protocol 层进行 RPC 通讯(netty,tcp 长连接),将服务调用信息和配置信息进行传递 Provider 端 Protocol 层接收到服务调用信息后,最终会调用真实的服务实现 Consumer 端调用过程 通过前面 Dubbo 服务发现&引用 的学习,我们知道,Consumer 端的调

【源码阅读系列】JDK 8 ConcurrentHashMap 源码分析之 由transfer引发的bug

不阅读源码就不会发现这个事儿 前段时间在阅读ConcurrentHashMap源码,版本JDK 8,目前源码研究已经告一段落.感谢鲁道的ConcurrentHashMap源码分析文章,读到文章,感觉和作者发生了一些交流,解答了很多疑惑,也验证了一些想法.鲁道在简书的addCount分析文章点这里 (文章底部的评论中就有这篇文章发酵的原由).鲁道还有其他ConcurrentHashMap源码分析的系列文章,在简书.掘金都有分布,感兴趣的同学可以进一步追踪. 推完文章,回到本篇的主题"阅读源码&qu