【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 配置文件的相关流程,其中 <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() 流程比较长,先放张时序图让大家有个基本的印象:
这里简单概括下上图中的重点内容:
- refProtocolProtocol.refer()
上面我们已经提了这里的 refer() 方法最终调用的是 RegistryProtocol.refer() 方法。
** 在 refer() 方法中首先会调用 registryFactory.getRegistry(url) 获取 Registry 对象(Dubbo SPI 机制);
** 接着调用 doRefer() 方法。 - doRefer()
- registry.register() 在 step3 介绍
- directory.subscribe() 在 step4 介绍
- registry.register()
笔者在调试时,用的注册中心为 zookeeper(实际官方也推荐),因此这里会在 zookeeper 上创建一个节点。节点类似:/dubbo/org.apache.dubbo.service.DemoService/consumers/url,如果没有设置 dynamic 参数,默认为临时节点; - RegistryDirectory.subscribe(url)
- 当前 url 中的 category 参数值被设置成了:providers,consumers,routers。
接着调用 registry.subscribe(url, this) 方法,不难分析最后调用的是 FailbackRegistry 类的 subscribe() 方法。 - 另外需要注意 RegistryDirectory 类实现了 NotifyListener 接口中的 notify() 方法:
- 当前 url 中的 category 参数值被设置成了:providers,consumers,routers。
- FailbackRegistry.subscribe()
- 调用 doSubscribe() 方法,在 Step6 介绍
- 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 方法
- url 会被 toCategoriesPath() 方法转换类似如下形式的 path 集合
- notify(url, listener, urls);
执行 doNotify() 方法,见 step8 - doNotify(url, listener, urls)
调用父类的 notify() 方法 - AbstractRegistry.notify(URL url, NotifyListener listener, List urls)
- 这里拿到的 urls 是 step6 中生成的。我们根据 url 中的 category 值对其分类,最后放到一个 map 集合中(key 为 category,value 为 url 集合)
- 遍历上面生成的 map 集合,执行 listener.notify(categoryList) 方法。这里的 listener 为 RegistryDirecotry 对象,在 step4 中作为参数开始传递;
- RegistryDirectory.notify(categoryList)
- 遍历 categoryList,将 category 值为 providers 的 url 添加到 invokerUrls 集合中
- 执行 refreshInvoker(invokerUrls) 方法
- refreshInvoker(invokerUrls)
refreshInvoker 非常重要,用于将 invokerUrls 转换为 invoker 对象。- 如果 invokerUrls 只有一条记录,且该条记录的 protocol 参数值为 empty,禁止访问
- 调用 toInvokers() 方法
- toInvokers(List urls)
这里会维护一个本地缓存 urlInvokerMap,key 值为 url 字符串;- 遍历 urls ,如果 urlInvokerMap 集合中 url 对应的 value 不为空,执行如下代码:
invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
这里会新建一个 DubboInvoker 对象并返回,我们会在后文详细分析;
- 直接取缓存中的值,不会重新构造 invoker 对象;
- 遍历 urls ,如果 urlInvokerMap 集合中 url 对应的 value 不为空,执行如下代码:
- toMethodInvokers
step12 中最终会生成一个 key 为 url,value 为 invoker 集合。在这里进行处理后最后返回的集合 key 值为 method,value 为 invoker 集合 - 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);
- 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 客户端的。
- getClients(url)
如果 url 中没有设置 connections 参数,默认共享链接,调用 getSharedClient() 获取 ExchangeClient 对象。 - getSharedClient(URL url)
getSharedClient() 顾名思义是用于获取共享客户端的。referenceClientMap 集合用于缓存 client,key 值为 url 的 address 参数。如果取缓存时对应值为 null ,会调用 initClient(url) 方法新建 ExchangeClient - initClient(url)
调用 Exchangers.connect() 方法构建 client ,最后返回的 client 会通过构造方法被赋值到到 DubboInvoker 类的 clients 成员变量中; - Exchangers.connect()
return getExchanger(url).connect(url, handler);
- 调用 getExchanger(url) 获取 HeaderExchanger 类(Dubbo SPI 机制)
- 调用 HeaderExchanger.connet() 方法建立连接
- 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()
- Transporters.connect()
return getTransporter().connect(url, handler);
- 调用 getTransporter() 方法获取 NettyTransporter 类(Dubbo SPI 机制)
- 调用 NettyTransporter.connet() 方法建立连接
- 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源码阅读系列】之远程服务调用(上)的更多相关文章
- 【Dubbo源码阅读系列】服务暴露之远程暴露
引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...
- 【Dubbo源码阅读系列】服务暴露之本地暴露
在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...
- 【Dubbo源码阅读系列】之 Dubbo SPI 机制
最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...
- 【Dubbo源码阅读系列】之 Dubbo XML 配置加载
今天我们来谈谈 Dubbo XML 配置相关内容.关于这部分内容我打算分为以下几个部分进行介绍: Dubbo XML Spring 自定义 XML 标签解析 Dubbo 自定义 XML 标签解析 Du ...
- 源码阅读系列:EventBus
title: 源码阅读系列:EventBus date: 2016-12-22 16:16:47 tags: 源码阅读 --- EventBus 是人们在日常开发中经常会用到的开源库,即使是不直接用的 ...
- JDK1.8源码阅读系列之三:Vector
本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...
- DM 源码阅读系列文章(六)relay log 的实现
2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第六篇,在 上篇文章 中我们介绍了 binlog replication 处理单 ...
- Spring源码阅读系列总结
最近一段时间,粗略的查看了一下Spring源码,对Spring的两大核心和Spring的组件有了更深入的了解.同时在学习Spring源码时,得了解一些设计模式,不然阅读源码还是有一定难度的,所以一些重 ...
- SpringMVC源码阅读系列汇总
1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...
随机推荐
- html-路径的介绍
一.绝对路径 绝对路径是指文件在硬盘上真正存在的路径 - D:\Java\JavaWeb\day01\a.jpg - http://www.baidu.com/b.jpg 二.相对路径 一个文件相对于 ...
- CSS使用小技巧
对于元素position 属性中的 relative absolute 页面居中问题 /* relative */div{ position:relative; margin: 0 auto}/* ...
- webstorm 打开后 一直停留在scanning files to index....,或跳出内存不够的提示框
用着有时会这样,超级卡, 网上搜了下,原来如此,记录下,免得忘了. ------------------------- 说明: 在npm install 后,会出现Scanning files to ...
- Oracle客户端安装以及PL/SQL Developer安装方法
1,安装Oracle客户端 2,配置数据库,如下: 安装路径:D:\app\ThinkPad\product\11.2.0\client_1\network\admin 建立文件:tnsnames.o ...
- zTree 学习笔记之(一)
zTree 学习笔记之(一) 简介 zTree 是一个依靠 jQuery 实现的多功能 “树插件”.优异的性能.灵活的配置.多种功能的组合是 zTree 最大优点. 到底有哪些具体的优点,可以参见官网 ...
- sql查询学习和实践点滴积累
https://blog.rjmetrics.com/2008/10/28/correlated-subqueries-in-mysql/ http://www.mysqltutorial.org/m ...
- 自动化测试基础篇--Selenium unittest生成测试报告(HTMLTestRunner)
如何生成HTMLTestRunner测试报告.接上篇文章,对于unittest框架,运行后,测试结果不便于查看,同时多个case存在的时候,可能会导致case result记录不正确的情况. 为此,引 ...
- golang 时间戳 时间格式化 获取当前时间 timestamp 计算时间差
获取当前时间 func Now func Now() Time 1 Now returns the current local time. func (Time) UTC func (t Time) ...
- Effective C++(1-2) 编译器替换预处理器
1 C++最主要的四部分: C Object-Oriented C++: 面向对象 Template C++:泛型编程 STL C++高效编程守则视状况而变化,取决于你使用C++的哪一部分. 2 尽量 ...
- MySQL 数据库--SQL语句优化
explain查询和分析sql 开发中,为满足一业务功能,使用mysql书写sql时,一条sql往往有多种写法,那么我们就需要选择执行效率比较高的sql. 因此要比较分析sql的执行过程,且同一条sq ...