分布式 RPC 框架
Dubbo(2.7)

课程讲义


分布式 RPC 框架 Dubbo(2.7) - 图1

主讲:Reythor





2020


分布式 RPC 框架 Dubbo

第4章 Dubbo 的系统架构解析

4.1 阿里系 RPC 框架

l Dubbo:阿里巴巴 B2B
l HSF:淘宝
l SOFA:蚂蚁金服

4.2 Dubbo 的两大设计原则

Dubbo 框架在设计时遵循了两大设计原则:
l Dubbo 使用“微内核+插件”的设计模式。内核只负责组装插件(扩展点),Dubbo 的功能都是由插件实现的。Dubbo 作为一个优秀的 RPC 框架,一个 Apache 的顶级项目,其最大的亮点之一就是其优秀的无限开放性设计架构—“微内核+插件”的架构设计思想,使得其几乎所有组件均可方便的进行扩展、增强、替换。
l 采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。
分布式 RPC 框架 Dubbo(2.7) - 图2JSON protocol:dubbo, host:ip,port:8888,interface:com.abc.service.SomeService
URL:dubbo://ip:8888/com.abc.service.SomeService?

4.3 Dubbo 的三大领域模型

为了对 Dubbo 整体架构叙述的方便,Dubbo 抽象出了三大领域模型。
l Protocol 服务域:是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
l Invoker 实体域:是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。其就是提供者代理对象。
l Invocation 会话域:它持有调用过程中的变量,比如方法名,参数等。远程调用信息。

4.4 Dubbo 的四大组件

Dubbo 中存在四大组件:
l Provider:暴露服务方,亦称为服务提供者。
l Consumer:调用远程服务方,亦称为服务消费者。
l Registry:服务注册与发现的中心,提供目录服务,亦称为服务注册中心
l Monitor:统计服务的调用次数、调用时间等信息的日志服务,亦称为服务监控中心

4.5 Dubbo 的十层架构


Dubbo 的架构设计划分为了 10 层。图中左边淡蓝色背景为服务 Consumer 使用的接口,右边淡绿色背景为服务 Provider 使用的接口,位于中轴线的为双方都要用到的接口。对于这
10 层,根据其总体功能划分,可以划分为三大层:

4.5.1 Business 层

该层仅包含一个 service 服务层,该层与实际业务逻辑有关,根据服务消费方和服务提供方的业务设计,实现对应的接口。
4.5.2 RPC
该层主要负责整个分布式系统中各个主机间的通讯。该层包含了以下 6 层。

(1) config 配置层

以 ServiceConfig 和 ReferenceConfig 为中心,用于加载并解析 Spring 配置文件中的 Dubbo 标签。

(2) proxy 服务代理层

服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory。
Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以运行的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。

(3) registry 注册中心层

封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、
RegistryService,可能没有服务注册中心,此时服务提供方直接暴露服务。

(4) cluster 路由层

封装多个提供者的路由和负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBalance,将多个服务提供方组合为一个服务提供方,实现对服务消费通明。只需要与一个服务提供方进行交互。
Dubbo 官方指出,在 Dubbo 的整体架构中,Cluster 只是一个外围概念。Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样用户只需关注 Protocol 层 Invoker 即可,加上
Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要
Cluster 的。

(5) monitor 监控层

RPC 调用时间和次数监控,以 Statistics 为中心,扩展接口 MonitorFactory、Monitor 和
MonitorService。

(6) protocol 远程调用层

封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocol、Invoker 和 Exporter。 Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。 Invoker 是实体域,它是 Dubbo 的核心模型,其他模型都是向它靠拢,或转换成它,它代表一个可执行体,可向它发起 Invoker 调用,它有可能是一个本地实现,也有可能是一个远程实现,也有可能是一个集群实现。
在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。

4.5.3 Remotting 层

Remoting 实现是 Dubbo 协议的实现,如果我们选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。具体包含以下三层:

(1) exchange 信息交换层

封装请求响应模式,同步转异步,以 Request 和 Response 为中心,扩展接口为 Exchanger 和 ExchangeChannel,ExchangeClient 和 ExchangeServer。

(2) transport 网络传输层

抽象和 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、
Client、Server 和 Codec。

(3) serialize 数据序列化层

分布式 RPC 框架 Dubbo(2.7) - 图3
可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObejctOutput 和 ThreadPool。注:这里解析的源码为 dubbo-2.7.3 版本。

4.6 Dubbo 框架架构

4.6.1 将 Dubbo 源码工程导入 Idea

本例以 Dubbo2.7.3 为例进行源码解析。从官网下载到源码的 zip 包后解压后就可以直接
导入到 Idea 中。

4.6.2 Dubbo2.7 版本与 2.6 版本

Dubbo2.7 版本需要 Java 8 及以上版本。
2.7.0 版本在改造的过程中遵循了一个原则,即保持与低版本的兼容性,因此从功能层面来说它是与 2.6.x 及更低版本完全兼容的。2.7 与 2.6 版本相比,改动最大的就是包名,由原来的 com.alibaba.dubbo 改为了 org.apache.dubbo。






分布式 RPC 框架 Dubbo(2.7) - 图4



















第5章 Dubbo 的内核解析
所谓 Dubbo 的内核是指,Dubbo 中所有功能都是基于它之上完成的,都是由它作为基础的。Dubbo 内核的工作原理由四部分构成:服务发现机制 SPI、自适应机制 Adaptive、包装机制 Wrapper 与激活机制 Activate。Dubbo 通过这四种机制实现了对插件的 IoC、AOP,实现了对自动生成类的动态编译 Compile。

5.1 JDK 的服务发现机制 SPI

5.1.1 简介


SPI,Service Provider Interface,服务提供者接口,是一种服务发现机制。

5.1.2 JDK 的 SPI 规范

JDK 的 SPI 规范规定:
l 接口名:可随意定义
l 实现类名:可随意定义
l 提供者配置文件路径:其查找的目录为 META-INF/services
l 提供者配置文件名称:接口的全限定性类名,没有扩展名
l 提供者配置文件内容:该接口的所有实现类的全限类性类名写入到该文件中,一个类名占一行

5.1.3 代码举例 12-jdkspi

(1) 定义工程
创建一个 Maven 的 Java 工程。
(2) 修改 pom 文件
指定编译器的源与目标版本。

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
分布式 RPC 框架 Dubbo(2.7) - 图5 <artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>


(3) 定义接口
分布式 RPC 框架 Dubbo(2.7) - 图6
(4) 定义两个实现类

(5) 创建目录与配置文件

(6) 定义配置文件内容
分布式 RPC 框架 Dubbo(2.7) - 图7
(7) 定义服务消费者类

5.2 Dubbo 的服务发现机制 SPI

Dubbo 并没有直接使用 JDK 的 SPI,而是在其基础之上对其进行了改进。

5.2.1 规范说明

Dubbo 的 SPI 规范是:
l 接口名:可以随意定义
l 实现类名:在接口名前添加一个用于表示自身功能的“标识前辍”字符串
l 提供者配置文件路径:在依次查找的目录为
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
l 提供者配置文件名称:接口的全限定性类名,无需扩展名
l 提供者配置文件内容:文件的内容为 key=value 形式,value 为该接口的实现类的全限类性类名,key 可以随意,但一般为该实现类的“标识前辍”(首字母小写)。一个类名占一行。
l 提供者加载:ExtensionLoader 类相当于 JDK SPI 中的 ServiceLoader 类,用于加载提供者配置文件中所有的实现类,并创建相应的实例。

5.2.2 Dubbo 的 SPI 举例 13-dubbospi

下面将实现一个下单功能。其支付方式仅支持支付宝或微信两种方式。即这里要定义一个 SPI 接口,其存在两个扩展类。
(1) 创建工程
创建一个 Maven 的 Java 工程。
(2) 导入依赖
导入 Dubbo 的依赖。

| |
| <properties> | | —- |


分布式 RPC 框架 Dubbo(2.7) - 图8project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> maven.compiler.source>1.8</maven.compiler.source> maven.compiler.target>1.8</maven.compiler.target>
>
>
依赖—>
>
>org.apache.dubbo</groupId> >dubbo</artifactId> >2.7.3</version>
>
>
>junit</groupId>
>junit</artifactId>
>4.11</version> test</scope>
>
>
<
<
<
</properties

<dependencies
<!—Dubbo
<dependency
<groupId
<artifactId
<version
</dependency

<dependency
<groupId
<artifactId
<version
<scope>
</dependency
</dependencies

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>

(3) 定义 SPI 接口

(4) 定义两个扩展类
分布式 RPC 框架 Dubbo(2.7) - 图9

(5) 定义扩展类配置文件

(6) 定义测试类
分布式 RPC 框架 Dubbo(2.7) - 图10

5.3 自适应机制 Adaptive

分布式 RPC 框架 Dubbo(2.7) - 图11
Adaptive 机制,即扩展类的自适应机制。即其可以指定想要加载的扩展名,也可以不指定。若不指定,则直接加载默认的扩展类。即其会自动匹配,做到自适应。其是通过@Adaptive 注解实现的。
5.3.1 @Adaptive 注解
@Adaptive 注解可以修饰类与方法,其作用相差很大。

(1) @Adaptive 修饰类

被@Adapative 修饰的 SPI 接口扩展类称为 Adaptive 类,表示该 SPI 扩展类会按照该类中指定的方式获取,即用于固定实现方式。其是装饰者设计模式的应用。

(2) @Adaptive 修饰方法

被@Adapative 修饰 SPI 接口中的方法称为 Adaptive 方法。在 SPI 扩展类中若没有找到
Adaptive 类,但系统却发现了 Adapative 方法,就会根据 Adaptive 方法自动为该 SPI 接口动态生成一个 Adaptive 扩展类,并自动将其编译。例如 Protocol 接口中就包含两个 Adaptive 方法。

5.3.2 Adaptive 类代码举例 14-adaptiveclass

1) 创建工程
复制 13-dubbospi 工程,在其基础之上修改。

(2) 定义 adaptive 扩展类

分布式 RPC 框架 Dubbo(2.7) - 图12
4) 测试 1

分布式 RPC 框架 Dubbo(2.7) - 图13

5.3.3 Adaptive 方法规范

下面我们准备要定义 Adaptive 方法。那么 Adaptive 方法的定义有什么要求呢?我们通过查看动态生成的 Adaptive 类来总结 Adaptive 方法的要求。

(1) 动态生成 Adaptive 类格式

package ;

public class SPI 接口名$Adpative implements SPI 接口 { public adaptiveMethod (arg0, arg1, …) {
// 注意,下面的判断仅对 URL 类型,或可以获取到 URL 类型值的参数进行判断
// 例如,dubbo 的 Invoker 类型中就包含有 URL 属性
if(arg1==null) throw new IllegalArgumentException(异常信息); if(arg1.getUrl()==null) throw new IllegalArgumentException(异常信息);

URL url = arg1.getUrl();
// 其会根据@Adaptive 注解上声明的 Key 的顺序,从 URL 获取 Value, // 作为实际扩展类。若有默认扩展类,则获取默认扩展类名;否则获取 // 指定扩展名名。
String extName = url.get 接口名() == null?默认扩展前辍名:url.get 接口名();

if(extName==null) throw new IllegalStateException(异常信息);
SPI 接口 extension = ExtensionLoader.getExtensionLoader(SPI 接口.class)
.getExtension(extName);
return extension. adaptiveMethod(arg0, arg1, …); }
public unAdaptiveMethod( arg0, arg1, …) {
throw new UnsupportedOperationException(异常信息); }
}


2) 方法规范
从前面的动态生成的 Adaptive 类中的 adaptiveMethod()方法体可知,其对于要加载的扩展名的指定方式是通过 URL 类型的方法参数指定的。所以对于 Adaptive 方法的定义规范仅一条:其参数包含 URL 类型的参数,或参数可以获取到 URL 类型的值。方法调用者是通过
URL 传递要加载的扩展名的。

5.3.4 Adaptive 方法代码举例 14-adaptivemethod

1) 创建工程
复制 14-adaptiveclass 工程,在其基础之上修改。

3) 修改两个扩展类

若存在 Adaptive 类,即使定义了 Adaptive 方法,其执行的也是 Adaptive 类,所以这里要首先将 Adaptive 类删除。
5) 定义扩展类配置文件
由于 Adaptive 类已经删除,所以在配置文件中也需要将 Adaptive 类的注册也删除。

7) 测试 2
分布式 RPC 框架 Dubbo(2.7) - 图14

5.4 包装机制 Wrapper

Wrapper 机制,即扩展类的包装机制。就是对扩展类中的 SPI 接口方法进行增强,进行包装,是 AOP 思想的体现,是 Wrapper 设计模式(装饰者设计模式)的应用。一个 SPI 可以包含多个 Wrapper。

5.4.1 Wrapper 类规范

Wrapper 机制不是通过注解实现的,而是通过一套 Wrapper 规范实现的。
Wrapper 类在定义时需要遵循如下规范。
l 该类要实现 SPI 接口
l 该类中要有 SPI 接口的引用
l 该类中 SPI 接口实例是通过仅包含一个 SPI 接口参数的带参构造器传的
l 在接口实现方法中要调用 SPI 接口引用对象的相应方法
l 该类名称以 Wrapper 结尾

5.4.2 代码举例 15-wrapper

复制 14-adaptivemethod 工程,在其基础之上修改。

2) 修改扩展类配置文件
将这两个 wrapper 注册到扩展类配置文件中。

5.5 激活机制 Activate

用于激活扩展类的。
Activate 机制,即扩展类的激活机制。通过指定的条件来激活当前的扩展类。其是通过
@Active 注解实现的。

5.5.1 @Activate 注解

在@Activate 注解中共有五个属性,其中 before、after 两个属性已经过时,剩余有效属性还有三个。它们的意义为:

l group:为扩展类指定所属的组别,是当前扩展类的一个标识。String[]类型,表示一个扩展类可以属于多个组。
l value:为当前扩展类指定的 key,是当前扩展类的一个标识。String[]类型,表示一个扩展类可以有多个指定的 key。
l order:指定筛选条件相同的扩展类的加载顺序。序号越小,优先级越高。默认值为 0。

5.5.2 代码举例

1) 创建工程
复制 13-dubbospi 工程,在其基础之上修改。

(2) 修改扩展类 AlipayOrder

(3)修改扩展类 WeChatOrder

(4) 定义扩展类 CardOrder

(5)定义扩展类 CashOrder

(6) 定义扩展类 CouponOrder


(7) 注册扩展类
分布式 RPC 框架 Dubbo(2.7) - 图15
(8) 测试

5.6 总结

l 在配置文件中可能会存在四种扩展类:直接扩展类、Adaptive 类、Wrapper 类,及 Activate 类。它们的共同特点是,都实现了 SPI 接口。
l Adaptive 类与 Activate 类是通过注解定义的,而 Wrapper 则是通过一套规范定义的。
l 一个 SPI 接口中只会有一个 Adaptive 类(无论是否是自动生成的),而 Wrapper 类与
Activate 类则可以有多个。
l 只有 Activate 类属于直接扩展类,Adaptive 类与 Wrapper 类均不属于直接扩展类范畴,因为它们都无法独立使用,均依赖于直接扩展类。

5.7 Dubbo 的 SPI 源码解析

下面以 Protocol 扩展实例的获取过程为例来解析 SPI 的执行过程。

分布式 RPC 框架 Dubbo(2.7) - 图16

(1) factory 实例的 loader 的 extensionFactory 实例是 null

这个 objectFactory 实例也是通过 SPI 获取到的。

(2) 获取 factory 的 extensionLoader

构造器执行完毕,则 ExtensionFactory 的 extensionLoader 实例就创建完毕。重新返回其调用语句。

(3) 创建 Container 的 loader 的 extensionFactory

跟踪执行 getAdaptiveExtension()。

分布式 RPC 框架 Dubbo(2.7) - 图17

分布式 RPC 框架 Dubbo(2.7) - 图18

跟踪 cacheWrapperClass()方法。
分布式 RPC 框架 Dubbo(2.7) - 图19
重新返回 loadClass()方法。

跟踪 cacheName()方法。
分布式 RPC 框架 Dubbo(2.7) - 图20

可以看出,其是将当前 name 与扩展类配对后放到了 extensionClasses 中了。

(4) 获取 Protocol 的 extensionLoader

前面的代码执行完毕,然后一层层的返回,最终就返回到了这里。
分布式 RPC 框架 Dubbo(2.7) - 图21
此时 Protocol 的 ExtensionLoader 的 objectFactory 实例就创建完毕。返回其调用语句。
分布式 RPC 框架 Dubbo(2.7) - 图22 最后第 131 行返回这个新创建的 loader,返回给该 getExtensionLoader()方法的调用语句。

(5) 获取 Protocol 的默认扩展类实例
此时再跟踪第 121 行的 getAdaptiveExtension()方法,是通过刚才获取到的 Protocol 的 laoder 对象获取 Protocol 的自适合实例。其执行过程与前面的相同。
5.8 插件的 IoC 注入源码解析
5.8.1 第一次注入场景分析
在应用启动时系统会尝试着从配置中心获取动态配置,为应用启动准备运行环境。若没有指定配置中心,其默认使用注册中心作为配置中心。
若要获取动态配置,就需要获取到 ZookeeperDynamicConfigurationFactory 动态配置工厂实例。而这个实例是从 zk 配置中心中获取的动态配置。若其要读取 zk 中的数据,则该实例必须具有 zk 的客户端引用。而这个 zk 的客户端就是 ZookeeperTransporter 实例。其为一个 SPI 接口,默认扩展名为 curator。

5.8.3 injectExtension()


跟踪第 566 行的 getExtension()方法。

分布式 RPC 框架 Dubbo(2.7) - 图23

分布式 RPC 框架 Dubbo(2.7) - 图24
5.9 插件的 AOP 包装源码解析
Dubbo 的 AOP 是对 SPI 扩展类进行增强的方式,而 Wrapper 机制就是对 SPI 扩展类的增强。不同 SPI 的不同 Wrapper,其增强的功能不同。为了方便大家的理解,我们就以前面在学习“Wrapper 机制”时写的代码 15-wrapper 为例进行源码解析,来查看扩展类是如何被包装起来的。

5.9.1 找到 AOP 源码

分布式 RPC 框架 Dubbo(2.7) - 图25

逐级返回,直至 Order@Adaptive 类。
分布式 RPC 框架 Dubbo(2.7) - 图26

5.10 自动生成类的动态编译源码解析
当一个SPI接口没有Adaptive类时,系统会根据Adaptive方法为其自动生成一个Adaptive 类,这个自动生成的类是一个 java 代码类,这个类是需要编译的。而该编译是由系统动态完成的。

5.10.1 Javassist 简介

Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。一般情况下,对字节码文件进行修改是需要使用虚拟机指令的。而使用 Javassist,可以直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
也就是说,使用 javassist 可以对现有的.class 文件进行直接的编辑,修改,也可以越过.java 文件,直接创建出.class 文件。
除了实现动态编译外,javassist 通常还会用于动态代理的生成。

5.10.2 手工实现 Javassist 动态编译 17-javassist

(1) 创建工程
创建一个 Maven 的 Java 工程。
分布式 RPC 框架 Dubbo(2.7) - 图27
(2) 导入依赖
仅需要一个 Javassist 依赖。

<properties>
project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> maven.compiler.source>1.8</maven.compiler.source> maven.compiler.target>1.8</maven.compiler.target>
>
> 依赖 —>
>
>org.javassist</groupId>
>javassist</artifactId> >3.26.0-GA</version>
>
>
<
<
<
</properties

<dependencies
<!— javassist
<dependency
<groupId
<artifactId
<version
</dependency
</dependencies

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>

(3) 定义 JavassistCompiler


5.10.3 解析 Dubbo 的动态编译

下面以获取 Protocol 接口的 adaptive 扩展类实例为例进行解析。

分布式 RPC 框架 Dubbo(2.7) - 图28 此时查看 code 的值,其为一个字符串,其内容就是动态生成的 Adaptive 扩展类。
在其上右击,选择 copy value 即可复制该字符串的内容。
分布式 RPC 框架 Dubbo(2.7) - 图29 动态生成 Adaptive 代码后,就可以对其进行编译了。
分布式 RPC 框架 Dubbo(2.7) - 图30

分布式 RPC 框架 Dubbo(2.7) - 图31








分布式 RPC 框架 Dubbo(2.7) - 图32