深入理解Apache Dubbo与实战读书笔记

第3章 Dubbo注册中心
3.1注册中心概述
主要作用如下:
动态加入。一个服务提供者通过注册中心可以动态地把自己暴露给其他消费者
动态发现。一个消费者可以动态地感知新的配置、路由规则和新的服务提供者
动态调整。注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点。
统一配置。避免了本地配置导致每个服务的配置不一致问题。

Dubbo的注册中心源码在模块dubbo-registry中,里面包含了五个子模块
模块介绍
dubbo-registry-api 包含了注册中心的所有API和抽象实现类
dubbo-registry-zookeeper 使用ZooKeeper作为注册中心的实现
dubbo-registry-redis 使用Redis作为注册中心的实现
dubbo-registry-default Dubbo基于内存的默认实现
dubbo-registry-multicast multicast模式的服务注册与发现

ZooKeeper是官方推荐的注册中心
Dubbo拥有良好的扩展性,如果以上注册中心都不能满足需求,那么用户可以基于
RegistryFactory和Registry自行扩展。
3.1.1工作流程
服务提供者启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息。

消费者启动时,也会向注册中心写入自己的元数据信息,并订阅服务提供者、路由和
配置元数据信息。

服务治理中心(dubbo-admin)启动时,会同时订阅所有消费者、服务提供者、路由和
配置元数据信息。

当有服务提供者离开或有新的服务提供者加入时,注册中心服务提供者目录会发生变
化,变化信息会动态通知给消费者、服务治理中心。

当消费方发起服务调用时,会异步将调用、统计信息等上报给监控中心 dubbo-monitor simple 。

3.1.3 ZooKeeper 原理概述
Dubbo使用ZooKeeper作为注册中心时,只会创建持久节点和临时节点两种,

/dubbo/com.foo.BarService/providers是服务提供者在ZooKeeper注册中心的路径示例,

是一种树形结构,该结构分为四层:
root (根节点,对应示例中的dubbo)
service (接口名称,对应示例中的com.foo.BarService)
四种服务目录(对应示例中的providers,其他目录还有consumers、routers、configurators)。

在服务分类节点下是具体的Dubbo服务URL。树形结构示例如下:

  • /dubbo
    ±- service
    ±- providers
    ±- consumers
    ±- routers
    ±- configurators

树形结构的关系:
(1) 树的根节点是注册中心分组,下面有多个服务接口,分组值来自用户配置
dubbo:registry中的 group 属性,默认是/dubbo。

(2) 服务接口下包含4类子目录,分别是providers> consumers> routers> configurators,
这个路径是持久节点。

(3) 服务提供者目录(/dubbo/service/providers)
下面包含的接口有多个服务者 URL 元数据信息。

(4) 服务消费者目录(/dubbo/service/consumers)

(5) 路由配置目录(/dubbo/service/routers)下面包含多个用于消费者路由策略
url元数据信息。

(6) 动态配置目录(/dubbo/service/configurators)下面包含多个用于服务者动态配置
URL元数据信息。

在Dubbo框架启动时,会根据用户配置的服务,在注册中心中创建4个目录,在providers
和consumers目录中分别存储服务提供方、消费方元数据信息,主要包括IP、端口、权重和应用名等数据。

在Dubbo框架进行服务调用时,用户可以通过服务治理平台(dubbo-admin)下发路由配
置。如果要在运行时改变服务参数,则用户可以通过服务治理平台(dubbo-admin)下发动态配置。服务器端会通过订阅机制收到属性变更,并重新更新已经暴露的服务。

四个子目录存储值样例
/dubbo/service/providers
dubbo://192.168.0.1.20880/com.alibaba.demo.Service?key=value&…

/dubbo/service/consumers
consumer://192.168.0.1.5002/com.alibaba.demo.Service?key=value&…

/dubbo/service/routers
condition://0.0.0.0/com.alibaba.demo.Service?category=routers&key=value&…

/dubbo/service/configurators override://0.0.0.0/com.alibaba.demo.Service?category=configurators&key=value&…

在Dubbo中启用注册中心可以参考如下方式:

<dubbo:registry protocol=“zookeeper” address=“ip:port>ip:port” />

<dubbo:registry protocol=“zookeeper” address=“ip:port|ip:port” />

3.2订阅/发布
订阅/发布是整个注册中心的核心功能之一。
针对服务的上下线可以自动检测发现
3.2.1 ZooKeeper 的实现
1.发布的实现
服务提供者和消费者都需要把自己注册到注册中心。服务提供者的注册是为了让消费者
感知服务的存在,从而发起远程调用;也让服务治理中心感知有新的服务提供者上线。消费
者的发布是为了让服务治理中心可以发现自己。

ZooKeeper发布代码非常简单,只是调用了
ZooKeeper的客户端库在注册中心上创建一个目录
zkClient.create(toUrlPath(url)
url.getParameter(Constants.DYNAMIJKEY, true));

取消发布也很简单,只是把ZooKeeper注册中心上对应的路径删除
zkClient.delete(toUrlPath(url));
2.订阅的实现
订阅通常有pull和push两种方式,一种是客户端定时轮询注册中心拉取配置,另一种是注
册中心主动推送数据给客户端。

目前Dubbo采用的是第一次启动拉取方式,后续接收事件重新拉取数据。

在服务暴露时,服务端会订阅configurators用于监听动态配置,在消费端启动时,消费
端会订阅providers、routers和configuratops这三个目录,分别对应服务提供者、路由和动
态配置变更通知

Dubbo中有哪些ZooKeeper客户端实现?
Dubbo在dubbo-remoting-zookeeper模块中实现了 ZooKeeper客户端 的统一封装,定义了统一的Client API,并用两种不同的ZooKeeper开源客户端库实现了这个接口:
Apache Curator;
zkCliento
用户可以在<dubbo: registry>的client属性中设置curator、zkclient来使用不同的客户端实现库,如果不设置则默认使用Curator作为实现。

注意,此处会根据URL中的category属性值获取具体的类别:
providers、routers、consumers> configurators,

然后拉取直接子节点的数据进行通知(notify)。
如果是providers 类别的数据,则订阅方会更新本地Directory管理的Invoker服务列表;
如果是routers分类,则订阅方会更新本地路由规则列表;
如果是configuators类别,则订阅方会更新或覆盖本地动态参数列表
3.3缓存机制
缓存的存在就是用空间换取时间,如果每次远程调用都要先从注册中心获取一次可调用的
服务列表,则会让注册中心承受巨大的流量压力。另外,每次额外的网络请求也会让整个系统的性能下降。因此Dubbo的注册中心实现了通用的缓存机制

消费者或服务治理中心获取注册信息后会做本地缓存。内存中会有一份,保存在Properties
对象里,磁盘上也会持久化一份文件,通过file对象引用。

内存中的缓存notified是ConcurrentHashMap里面又嵌套了一个Map,外层Map的key
是消费者的 URL,内层 Map 的 key 是分类,包含 providers> consumers> routes> configurators 四种。value则是对应的服务列表,对于没有服务提供者提供服务的URL,它会以特殊的empty:// 前缀开头
3.3.1缓存的加载
在服务初始化的时候,AbstractRegistry构造函数里会从本地磁盘文件中把持久化的注册
数据读到Properties对象里,并加载到内存缓存中

Properties保存了所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表、
路由规则列表、配置规则列表等作为value。由于value是列表,当存在多个的时候使用空格隔开。还有一个特殊的key.registies,保存所有的注册中心的地址。如果应用在启动过程中,
注册中心无法连接或宕机,则Dubbo框架会自动通过本地缓存加载Invokerso
3.3.2缓存的保存与更新
缓存的保存有同步和异步两种方式。异步会使用线程池异步保存,如果线程在执行过程中
出现异常,则会再次调用线程池不断重试
AbstractRegistry#notify方法中封装了更新内存缓存和更新文件缓存的逻辑。当客户端第一
次订阅获取全量数据,或者后续由于订阅得到新数据时,都会调用该方法进行保存。
3.4重试机制
由图 3-5 我们可以得知 com.alibaba.dubbo.registry.support.FailbackRegistry 继承了
AbstractRegistry,并在此基础上增加了失败重试机制作为抽象能力。ZookeeperRegistry和
RedisRegistry继承该抽象方法后,直接使用即可。

FailbackRegistry抽象类中定义了一个ScheduledExecutorService,每经过固定间隔(默认
为5秒)调用FailbackRegistry#retry()方法。另外,该抽象类中还有五个比较重要的集合,
如表3-3所示

表3-3集合名称与介绍
Set failedRegistered 发起注册失败的URL集合
Set failedUnregistered 取消注册失败的URL集合
ConcurrentMap> failedSubscribed 发起订阅失败的监听器集合
ConcurrentMap> failedUnsubscribed 取消订阅失败的监听器集合
ConcurrentMap» failedNotified 通知失败的URL集合

在定时器中调用retry方法的时候,会把这五个集合分别遍历和重试,重试成功则从集合中
移除。FailbackRegistry实现了 subscribe> unsubscribe等通用方法,里面调用了未实现的模板
方法,会由子类实现。通用方法会调用这些模板方法,如果捕获到异常,则会把URL添加到对应的重试集合中,以供定时器去重试
第4章 Dubbo扩展点加载机制
@SPI、©Adaptive> ©Activateo然后介绍整个加载机制中最核心的ExtensionLoader的工作流程及实现原理。最后介绍扩展中使用的类动态编译的实现原理。
4.1加载机制概述
4.1.2扩展点加载机制的改进
与Java SPI相比,Dubbo SPI做了一定的改进和优化
1.JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现则初始化很耗时,如果没用上也加载,则浪费资源。
2.如果扩展加载失败,则连扩展的名称都获取取不到了, 就是有一些错误信息返回的不正常
3…增加了对扩展IoC和AOP的支持,一个扩展可以直接setter注入其他扩展。

代码清单4-2 PrintService接口的Dubbo SPI改造
在目录META-INF/dubbo/internal下建立配置文件com.test.spi.Printservice
内容: impl=com.test.spi.PrintServiceImpl

② 为接口类添加SPI注解,设置默认实现为impl
@SPI(“impl”)
public interface Printservice

PrintService printservice = ExtensionLoader .getExtensionLoader(PrintService.class)
.getDefaultExtension();
4.1.3扩展点的配置规范
需要在META-INF/dubbo/下放置对应的SPI配置文件,文件名 称需要命名为接口的全路径名。
配置文件的内容为key=扩展点实现类全路径名,如果有多个实现类则使用换行符分隔。
其中,key会作为DubboSPI注解中的传入参数。
另外,Dubbo SPI还兼容了 Java SPI的配置路径和内容配置方式。
在Dubbo启动的时候,会默认扫这三个目录下的配置文件:
META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/

4-1 所示。
表4-1 Dubbo SPI配置规范

SPI配置文件路径
META-INF/services/> META-INF/dubbo/> META-INF/dubbo/intemal/

SPI配置文件名称
全路径类名

文件内容格式
key=value方式,多个用换行符分隔
4.1.4扩展点的分类与缓存
Dubbo SPI可以分为Class缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩
展类、包装扩展类(Wrapper类)、自适应扩展类(Adaptive类)等。

Class缓存:Dubbo SPI获取扩展类时,会先从缓存中读取。如果缓存中不存在,则加
载配置文件,根据配置把Class缓存到内存中,并不会直接全部初始化。

实例缓存:基于性能考虑,Dubbo框架中不仅缓存Class,也会缓存Class实例化后的
对象。这也是为什么Dubbo SPI相对Java SPI性能上有优势的原因,因为Dubbo SPI
缓存的Class并不会全部实例化,而是按需实例化并缓存,因此性能更好。

被缓存的Class和对象实例可以根据不同的特性分为不同的类别:
(1) 普通扩展类。最基础的,配置在SPI配置文件中的扩展类实现。
(2) 包装扩展类。这种Wrapper类没有具体的实现,只是做了通用逻辑的抽象,并且需要
在构造方法中传入一个具体的扩展接口的实现。
(3) 自适应扩展类。一个扩展接口会有多种实现类,具体使用哪个实现类可以不写死在配
置或代码中,在运行时,通过传入URL中的某些参数动态来确定。这属于扩展点的自适应特性,使用的@Adaptive注解
(4) 其他缓存,如扩展类加载器缓存、扩展名缓存等。
4.1.5扩展点的特性
扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。

1.自动包装
ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,则这个扩展类就会被认为是Wrapper类这是一种装饰器模式,把通用的抽象逻辑进行封装或对子类进行增强,让子类可以更加专注具体的实现,也可以是代理模式

2,自动加载
我们还经常使用setter方法设置属性值。如果某个扩展类是另外一个扩展点类的成员属性,并且拥有setter方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader在执行扩展点初始化的时候,会自动通过setter方法注入对应的实现类。这里存在一个问题,如果扩展类属性是一个接口,它有多种实现,那么具体注入哪一个呢?这就涉及第三个特性一一自适应。

  1. 自适应
    在Dubbo SPI中,我们使用@Adaptive注解,可以动态地通过URL中的参数来确定要使用
    哪个具体的实现类。从而解决自动加载中的实例注入问题。

@SPI(nnetty")
public interface Transporter (
@Adaptive({Constants SERVER_KEY, Constants.TRANSPORTER_KEY})
Server bind(URL url, ChannelHandler handler) throws RemotingException;

@Adaptive传入了两个Constants中的参数,它们的值分别是"server”和“transporter”。当
外部调用Transporter#bind方法时,会动态从传入的参数“URL”中提取key参数“server”
的value值,如果能匹配上某个扩展实现类则直接使用对应的实现类;
如果未匹配上,则继续通过第二个key参数“transporter”提取value值。如果都没匹配上,则抛出异常。也就是说,如果©Adaptive中传入了多个参数,则依次进行实现类的匹配,直到最后抛出异常。

这种动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现
类同时被激活,如Filter可以同时有多个过滤器;或者根据不同的条件,同时激活多个实现类,
如何实现?这就涉及最后一个特性一一自动激活。

  1. 自动激活
    使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同
    的参数,设置扩展点在不同的条件下被自动激活。主要的使用场景是某个扩展点的多个实现类需要同时启用(比如Filter扩展点)。
    4.2扩展点注解
    4.2.1扩展点注解:@SPI
    @SPI注解可以使用在类、接口和枚举类上
    是标记这个接口是一个Dubbo SPI接口,即是一个扩展点
    可以有多个不同的内置或用户定义的实现。运行时需要通过配置找到具体的实现类。

到SPI注解有一个value属性,通过这个属性,我们可以传入不同的参数来设
置这个接口的默认实现类

Dubbo中很多地方通过getExtension (Class type. String name)来获取扩展点接口的
具体实现,此时会对传入的Class做校验,判断是否是接口,以及是否有@SPI注解,两者缺一不可。
4.2.2扩展点自适应注解:©Adaptive
@Adaptive注解可以标记在类、接口、枚举类和方法上
但是在整个Dubbo框架中,只有几个地方使用在类级别上

如果标注在接口的方法上,即方法级别注解,则可以通过参数动态获得实现类
方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。

当该注解放在实现类上,则整个实现类会直接作为默认实现,不再自动生成代码清单4-8
中的代码。在扩展点接口的多个实现里,只能有一个实现上可以加@Adaptive注解。如果多个实现类都有该注解,则会抛出异常:More than 1 adaptive class found0

public @interface Adaptive (
String[] value() default {}; 数组,可以设置多个key,会按顺序依次匹配
}

该注解也可以传入value参数,是一个数组。Adaptive可以传入多个key值,在初始化Adaptive注解的接口时,会先对传入的URL进行key值匹配,第一个key没匹配上则匹配第二个,以此类推。直到所有的key匹配完毕,如果还没有匹配到,则会使用“驼峰规则”匹配,如果也没匹配到,则会抛出IllegalStateException异常。

最后,为什么有些实现类上会标注©Adaptive呢?放在实现类上,主要是为了直接固定对
应的实现而不需要动态生成代码实现,就像策略模式直接确定实现类。。如果注解在接口方法上, 则会根据参数,动态获得扩展点的实现,会生成Adaptive类
4.2.3扩展点自动激活注解:©Activate
@Activate可以标记在类、接口、枚举类和方法上。主要使用在有多个扩展点实现、需要根
据不同条件被激活的场景中,如Filter需要多个同时激活,因为每个Filter实现的是不同的功能。

©Activate可传入的参数很多
表 4-3 Activate 参数
参数名
效 果
String[] group()
URL中的分组如果匹配则激活,则可以设置多个
String[] value()
查找URL中如果含有该key值,则会激活
String[] before()
填写扩展点列表,表示哪些扩展点要在本扩展点之前
String[] after()
同上,表示哪些需要在本扩展点之后
int order()
整型,直接的排序信息
4.3 ExtensionLoader 的工作原理
4.3.1工作流程
ExtensionLoader 的逻辑入口可以分为 getExtension、getAdaptiveExtension、
getActivateExtension三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活的扩
展类。每个方法可能会有不同的重载的方法,根据不同的传入参数进行调整三个入口中,getActivateExtension对getExtension 的依赖比较getAdaptiveExtension则相对独立。

getActivateExtension方法只是根据不同的条件同时激活多个普通扩展类。因此,该方法中只会做一些通用的判断逻辑,如接口是否包含©Activate注解、匹配条件是否符合等。最终还是通过调用getExtension方法获得具体扩展点实现类

getExtension (String name)是整个扩展加载器中最核心的方法。加载过程中的每一步,都会先检查缓存中是否己经存在所需的数据。这个方法每次只会根据名称返回一个扩展点实现类。
初始化的过程可以分为4步:
(1)框架读取SPI对应路径下的配置文件,并根据配置加载所有扩展类并缓存(不初始化)。
(2) 根据传入的名称初始化对应的扩展类。
(3) 尝试查找符合条件的包装类:包含扩展点的setter方法,例如setProtocol(Protocol
protocol)方法会自动注入protocol扩展点实现;包含与扩展点类型相同的构造函数,为其注入扩展类实例,例如本次初始化了一个Class A,初始化完成后,会寻找构造参数中需要Class A的包装类(Wrapper),然后注入Class A实例,并初始化这个包装类。
(4) 返回对应的扩展类实例。

getAdaptiveExtension也相对独立,只有加载配置信息部分与getExtension共用了同一个
方法。和获取普通扩展类一样,框架会先检查缓存中是否有已经初始化化好的Adaptive实例,没有则调用createAdaptiveExtension重新初始化。初始化过程分为4步:
(1) 和getExtension 一样先加载配置文件。
(2) 生成自适应类的代码字符串。
(3) 获取类加载器和编译器,并用编译器编译刚才生成的代码字符串。Dubbo 一共有三种
类型的编译器实现,
(4) 返回对应的自适应类实例。
4.3.2 getExtension 的实现原理
当调用getExtension(String name)方法时,会先检查缓存中是否有现成的数据,没有则
调用createExtension开始创建。这里有个特殊点,如果getExtension传入的name是true,
则加载并返回默认扩展类。
在调用createExtension开始创建的过程中,也会先检查缓存中是否有配置信息,如果不
存在扩展类,则会从 META-INF/services/> META-INF/dubbo/、META-INF/dubbo/internal/这几个路径中读取所有的配置文件,通过I/O读取字符流,然后通过解析字符串,得到配置文件中对应的扩展点实现类的全称(如 com.alibaba.dubbo.common.extensionloader.activate.impl.
GroupActivateExtImpl)
4.3.3 getAdaptiveExtension 的实现原理
会为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:为接口中每个有@Adaptive注解的方法生成默认实现(没有注解的方法则生成空实现),每个默认实现都会从URL中提取Adaptive参数值,并以此为依据动态加载扩展点。然后,框架会使用不同的编译器,把实现类字符串编译为自适应类并返回。本节主要讲解字符串代码生成的实现原理。

接口上既有@SPI(”impl”)注解,方法上又有@Adaptive(”impl2”)注解最终动态生成的实现方法会 是url.getParameter(nimpl2n, "impl”),即优先通过©Adaptive注解传入的key去查找扩展实现类; 如果没找到,则通过@SPI注解中的key去查找;如果@SPI注解中没有默认值,则把类名转化为key,再去查找。
4.3.4 getActivateExtension 的实现原理
4.3.5 ExtensionFactory 的实现原理
4.4扩展点动态编译的实现
4.4.1总体结构
Dubbo中有三种代码编译器,分别是JDK编译器、Javassist编译器和AdaptiveCompiler编
译器。这几种编译器都实现了 Compiler接口,Compiler接口上含有一个SPI注解,注解的默认值是@SPI(”javassist”), 很明显,Javassist编译器将作为默认编译器。如果用户想改变默认编译器,则可以通过 <dubbo:application compiler=“jdk” />标签进行配置。
4.4.2 Javassist动态代码编译
4.4.3 JDK动态代码编译
DdkCompiler是Dubbo编译器的另一种实现,使用了 JDK自带的编译器,原生JDK编译器
包位于 javax. tools下
4.5小结
本章的内容比较多,首先介绍了 Dubbo SPI的一些概要信息,包括与JavaSPI的区别>Dubbo
SPI的新特性、配置规范和内部缓存等。其次介绍了 Dubbo SPI中最重要的三个注解:@SPI、
@Adaptive、@Activate,讲解了这几个注解的作用及实现原理。然后结合ExtensionLoader类
的源码介绍了整个Dubbo SPI中最关键的三个入口: getExtension、getAdaptiveExtension>
getActivateExtension,并讲解了创建 ExtensionLoader 的工厂(ExtensionFactory)的工作原
理。最后还讲解了自适应机制中动态编译的实现原理。
第5章 Dubbo启停原理解析
5.1配置解析
目前Dubbo框架同时提供了 3种配置方式: XML配置、注解、属性文件(properties和ymal)
配置,最常用的还是XML和注解两种方式

本节主要详细探讨schema设计、XML解析和注解配置实现原理

schema设计就是对于配置文件的约束和配置的内容对应使用时候的属性
和spring的配置文件是一个, 只不过schema是xsd的约束

5.1.1 基于schema设计解析
oDubbo配置约束文件在dubbo-config/dubbo-config
spring/src/main/resources/dubbo.xsd

dubbo.xsd文件用来约束使用XML配置时的标签和对应的属性

Dubbo 设计之初也考虑到属性最大限度的复用,因此对schema进行了精心的设计,Dubbo schema层级的详细设计如图5.1所示。

在图 5-1 中,左边代表 schema 有继承关系的类型,右边是独立的类型

schema模块说明如表5-1所示

图5-1中没有体现出来的是annotationType模块,这个模块主要配置项目要扫描的注解包。
因为篇幅和完整性,这里特意说明一下,接下来我们详细探讨Dubbo框架是如何对dubbo.xsd
做扩展的
5.1.2 基于XML配置原理解析
接下来我们探讨框架是如何解析配置的。
主要解析逻辑入口是在DubboNamespaceHandler类中完成的
代码清单5-3 Dubbo注册属性解析处理器
public class DubboNamespaceHandler extends NamespaceHandlerSupport (
Override
public void init() (
registerBeanDefinitionParser("application"new
DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module"new
DubboBeanDefinitionParser(ModuleConfig.classJ true));

DubboNamespaceHandler主要把不同的标签关联到解析实现类中

registerBeanDefinitionParser方法约定了在Dubbo框架中遇到标签application> module和registry等都会委托给DubboBeanDefinitionParser处理

代码…
前面的逻辑主要负责把标签解析成对应的Bean定义并注册到Spring 下文中,同时保证
了 Spring容器中相同id的Bean不会被覆盖。

接下来分析具体的标签是如何解析的,我们依次分析dubbo:service> dubbo:provider
和dubbo:consumer标签

代码清单5-6标签属性值解析

这里给出了核心属性值解析的注释代码,省略了特殊属性解析。本质上都是把属性注入Spring框架的BeanDefinition。如果属性是引用对象,则Dubbo默认会创建RuntimeBeanReference 类型注入,运行时由Spring注入引用对象。通过对属性解析的理解,其实Dubbo只做了属性提取的事情,运行时属性注入和转换都是Spring处理的

Dubbo框架生成的BeanDefinition最终还是会委托Spring创建对应的Java对象,dubbo.xsd
中定义的类型都会有与之对应的POJO, Dubbo承载对象和继承关系如图5-2所示

5.1.3基于注解配置原理解析
主要就是简介注解在怎么被扫描然后注入到容器中,并且给dubbo远程调用注解赋值

注解处理逻辑主要包含3部分内容,第一部分是如果用户使用了配置文件,则框架按需生
成对应Bean,第二部分是要将所有使用Dubbo的注解@Service的class提升为Bean,第三部
分要为使用@Reference注解的字段或方法注入代理对象。我们先看一下EnableDubbo注解,如
代码清单5-7所示。
@EnableDubboConfig
@DubboComponentScan
public ^interface EnableDubbo {

代码清单5・8 Dubbo解析注解属性
可以发现处理用户指定配置代码的逻辑比较简单,在DubboConfigBindingRegistrar实现中做
了下面几件事情:
(1)如果用户配置了属性,比如dubbo.application.name,则会自动创建对应Spring Bean
(2)注册和配置对象Bean属性绑定处理器DubboConfigBindingBeanPostProcessor,委托
Spring做属性值绑定。

5.2服务暴露的实现原理
5.2.1配置承载初始化
不管在服务暴露还是服务消费场景下,Dubbo框架都会根据优先级对配置信息做聚合处理,
目前默认覆盖策略主要遵循以下几点规则:
(1) -D 传递给 JVM 参数优先级最高,比如-Ddubbo. protocol.port=20880o
(2) 代码或XML配置优先级次高,比如Spring中XML文件指定<dubbo:protocol
port=H20880’7>
(3)配置文件优先级最低,比如 dubbo.properties 文件指定 dubbo.protocol.port=20880

一般推荐使用dubbo.properties作为默认值,只有XML没有配置时,dubbo.properties
配置项才会生效,通常用于共享公共配置,比如应用名等。

Dubbo的配置也会受到provider的影响,这个属于运行期属性值影响,同样遵循以下几点
规则:
(1) 如果只有provider端指定配置,则会自动透传到客户端(比如timeout)o
(2) 如果客户端也配置了相应属性,则服务端配置会被覆盖(比如timeout)o
5.2.2远程服务的暴露机制
在整体上看,Dubbo框架做服务暴露分为两大部分,第一步将持有的服务实例通过代理转
换成Invoker,第二步会把Invoker通过具体的协议(比如Dubbo 转换成Exporter,

框架真正进行服务暴露的入口点在
ServiceConfig#doExport中,无论XML还是注解,都会转换成ServiceBean,它继承自
ServiceConfig

Dubbo支持多注册中心同时写,如果配置了服务同时注册多个注册中心,则会在
ServiceConfig#doExportUrls中依次暴露

代码清单5-11多协议多注册中心暴露

Dubbo也支持相同服务暴露多个协议,比如同时暴露Dubbo和REST协议

代码清单5-12服务暴露

protocol实例会自动根据服务暴露URL自动做适配,有注册中心场景会取出具体协议,比
如ZooKeeper,首先会创建注册中心实例,然后取出export对应的具体服务URL,最后用服务
URL对应的协议(默认为Dubbo)进行服务暴露,当服务暴露成功后把服务数据注册到ZooKeeper

如果没有注册中心,则在⑦中会自动判断URL对应的协议(Dubbo)并直接暴露服务,从而没有经过注册中心在将服务实例ref转换成Invoker之后,如果有注册中心时,则会通过RegistryProtocol#export 进行更细粒度的控制,比如先进行服务暴露再注册服务元数据。注册中心在做服务暴露时依次做了以下几件事情(逻辑如代码清单5.13所示)。
(1) 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。
(2) 创建注册中心对象,与注册中心创建TCP连接。
(3) 注册服务元数据到注册中心。
(4) 订阅configurators节点,监听服务动态属性变更事件。
(5) 服务销毁收尾工作,比如关闭端口、反注册服务信息等。

代码清单5-13注册中心控制服务暴露

当服务真实调用时会触发各种拦截器Filter,这个是在哪里初始化的呢?在①中进行服务暴露前,框架会做拦截器初始化

代码清单5-14拦截器构造
①:在触发Dubbo协议暴露前先对服务Invoker做了一层拦截器构建,在加载所有拦截器时
会过滤只对provider生效的数据。
②:首先获取真实服务ref对应的Invoker并挂载到整个拦截器链尾部,然后逐层包裹其他拦截器,这样保证了真实服务调用是最后触发的。
③:逐层转发拦截器服务调用,是否调用下一个拦截器由具体拦截器实现

代码清单5-15 Dubbo协议暴露
①和②:中主要根据服务分组、版本、服务接口和暴露端口作为key用于关联具体服务Invoker。
③:对服务暴露做校验判断,因为同一个协议暴露有很多接口,只有初次暴露的接口才需要打开端口监听,然后在④中触发HeaderExchanger中的绑定方法,最后会调用底层NettyServer
进行处理。在初始化Server过程中会初始化很多Handler用于支持一些特性,比如心跳、业务线程池处理编解码的Handler和口向应方法调用的Handler
5.2.3本地服务的暴露机制
很多使用Dubbo框架的应用可能存在同一个JVM暴露了远程服务,同时同一个JVM内部又引用了自身服务的情况,Dubbo默认会把远程服务用injvm协议再暴露一份,这样消费方直接消费同一个JVM内部的服务,避免了跨网络进行远程通信。

代码清单5・16本地服务暴露

通过exportLocal实现可以发现,在①中显示Dubbo指定用injvm协议暴露服务,这个协
议比较特殊,不会做端口打开操作,仅仅把服务保存在内存中而已。在②中会提取URL中的协议,在InjvmProtocol类中存储服务实例信息,它的实现也是非常直截了当的,直接返回InjvmExporter实例对象,构造函数内部会把当前Invoker加入exporterMap

代码清单5-17本地服务暴露
5.3服务消费的实现原理
5.3.1单注册中心消费原理
在整体上看,Dubbo框架做服务消费也分为两大部分,第一步通过持有远程服务实例生成
Invoker,这个Invoker在客户端是核心的远程代理对象。第二步会把Invoker通过动态代理转换成实现用户接口的动态代理引用。这里的Invoker承载了网络连接、服务调用和重试等功能,在客户端,它可能是一个远程的实现,也可能是一个集群实现

Dubbo支持多注册中心同时消费,如果配置了服务同时注册多个注册中心,则会在
ReferenceConfig#createProxy 中合并成一个 Invoke

代码清单5-18多协议多注册中心暴露

在createProxy实现中完成了远程代理对象的创建及代理对象的转换等工作。在①中会优先判断是否在同 一个JVM中包含要消费的服务,默认场景下,Dubbo会通过②找出内存中injvm协议的服务,其实injvm协议是比较好理解的,前面提到服务实例都放到内存map中,消费也是直接获取实 例调用而已。③:主要在注册中心中追加消费者元数据信息,应用启动时订阅注册中心、服务 提供者参数等合并时会用到这部分信息。④:处理只有一个注册中心的场景,这种场景在客户 端中是最常见的,客户端启动拉取服务元数据,订阅provider、路由和配置变更。⑤和⑥:分别处理多注册中心的场景,详细内容会在后面讲解

代码清单5・19 Dubbo通过注册中心消费
这段逻辑主要完成了注册中心实例的创建,元数据注册到注册中心及订阅的功能

代码清单5-20 Dubbo服务消费服务通知
Dubbo框架允许在消费方配置只消费指定协议的服务,具体协议过滤在①中进行处理,支
持消费多个协议,允许消费多个协议时,在配置Protocol值时用逗号分隔即可。在②中消费信息是客户端处理的,需要合并服务端相关信息,比如远程IP和端口等信息,通过注册中心获取这些信息,解耦了消费方强绑定配置。在③中消除重复推送的服务列表,防止重复引用。在④中使用具体的协议发起远程连接等操作。

代码清单5・21初始化客户端连接

5.3.2多注册中心消费原理
在实际使用过程中,我们更多遇到的是单注册中心场景,但是当跨机房消费时,Dubbo框
架允许同时消费多个机房服务。默认Dubbo消费机房的服务顺序是按照配置注册中心的顺序决定的,配置靠前优先消费。

多注册中心消费原理比较简单,每个单独注册中心抽象成一个单独的Invoker,多个注册中
心实例最终通过StaticDirectory保存所有的Invoker,最终通过Cluster合并成一个Invoker0
5.3.3直连服务消费原理
Dubbo可以绕过注册中心直接向指定服务(直接指定目标IP和端口)发起RPC调用,使
用直连模式可以方便在某些场景下使用,比如压测指定机器等。Dubbo框架也支持同时指定直连多台机器进行服务调用

代码清单5-23直连服务消费

在①中允许用分号指定多个直连机器地址,多个直连机器调用会使用负载均衡,更多场景
是单个直连,但是不建议在生产环境中使用直连模式,因为上游服务发布会影响服务调用方。
在②中允许配置注册中心地址,这样可以通过注册中心发现服务消费。在③中指定服务调用协议、IP和端口,注意这里的URL没有添加refer和注册中心协议,默认是Dubbo会直接触发DubboProtocol进行远程消费,不会经过Registryprotocol去做服务发现。
5.4优雅停机原理解析
Dubbo中实现的优雅停机机制主要包含6个步骤:
(1)收到kill 9进程退出信号,Spring容器会触发容器销毁事件。
2 provider端会取消注册服务元数据信息。
3) consumer端会收到最新地址列表(不包含准备停机的地址)。
4 Dubbo协议会发送readonly事件报文通知consumer服务不可用。
(5 服务端等待已经执行的任务结束并拒绝新任务执行。

可能读者会有疑问,既然注册中心已经通知了最新服务列表,为什么还要再发送readonly
报文呢?这里主要考虑到注册中心推送服务有网络延迟,以及客户端计算服务列表可能占用一 些时间。Dubbo协议发送readonly时间报文时,consumer端会设置响应的provider为不可用状态,下次负载均衡就不会调用下线的机器。
5.5小结
本章我们首先对Dubbo中XML schema约束文件进行了讲解,也包括如何映射到对应Java
对象中。现在越来越多地使用注解的方式,我们也对注解的解析核心流程进行了探讨。然后对Dubbo框架的几种服务暴露原理进行了详解,紧接着对服务消费进行了讲解,这些服务暴露和消费对所有的协议都具有参考价值。最后我们对优雅停机的原理进行了探讨,也对以前的实现缺陷的原因进行了概述
第6章 Dubbo远程调用
6.1 Dubbo调用介绍
Dubbo在一次完整的RPC调用流程中经过的步骤,如图6.1所示。

首先在客户端启动时会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列
表聚合成一个Invoker,每次RPC调用前会通过Directory#list获取providers地址(已经生成
好的Invoker列表),获取这些服务列表给后续路由和负载均衡使用。

对应图6.1,在①中主要是将多个服务提供者做聚合。在框架内部另外一个实现Directory接口是RegistryDirectory类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取和订阅服务提供者、动态配置和路由项。

在Dubbo发起服务调用时,所有路由和负载均衡都是在客户端实现的。客户端服务调用首
先会触发路由操作,然后将路由结果得到的服务列表作为负载均衡参数,经过负载均衡后会选出一台机器进行RPC调用,这3个步骤依次对应于②、③和④。

客户端经过路由和负载均衡后,会将请求交给底层I/O线程池(比如Netty 处理,I/O线程池主要处理读写、序列化和反序列化等逻辑,因此这里一定不能阻塞操作,Dubbo也提供参数控制 decode.in.io 参数,在处理反序列化对象时会在业务线程池中处理。在⑤中包含两种类似的线程池,一种是I/O线程池Netty ,另一种是Dubbo业务线程池(承载业务方法调用)。
目前Dubbo将服务调用和Telnet调用做了端口复用,在编解码层面也做了适配。在Telnet
调用时,会新建立一个TCP连接,传递接口、方法和JSON格式的参数进行服务调用,在编解码层面简单读取流中的字符串(因为不是Dubbo标准头报文),最终交给Telnet对应的Handler 去解析方法调用。如果是非Telnet调用,则服务提供方会根据传递过来的接口、分组和版本信 息查找Invoker对应的实例进行反射调用。在⑦中进行了端口复用,如果是Telnet调用,则先找 到对应的Invoker进行方法调用。Telnet和正常RPC调用不一样的地方是序列化和反序列化使 用的不是Hessian方式,而是直接使用fastjson进行处理。如果读者对目前的流程没有完全理解也没有关系,后面会逐渐深入讲解。

6.2 Dubbo协议详解
本节我们讲解Dubbo协议设计,其协议设计参考了现有TCP/IP协议,在阅读图6-2时,我
们发现一次RPC调用包括协议头和协议体两部分。16字节长的报文头部主要携带了魔法数
(Oxdabb),以及当前请求报文是否是Request、Response 心跳和事件的信息,请求时也会携
带当前报文体内序列化协议编号。除此之外,报文头部还携带了请求状态,以及请求唯一标识和报文体长度

理解协议本身的内容对后面的编码器和解码器的实现非常重要,我们先逐字节、逐比特位
讲解协议内容(具体内容参考表6-l)
表6-1 Dubbo协议字段解析
偏移比特位 字段描述 作 用
0〜7 魔数高位 存储的是魔法数高位(OxdaOO)

8〜15 魔数低位 存储的是魔法数高位(Oxbb)

16 数据包类型 是否为双向的RPC调用(比如方法调用有返回值),
0为Response, 1 为 Request

17 调用方式 仅在第16位被设为1的情况下有效 0为单向调用,1为双向调用比如在优雅停机时服务端发送readonly不需要双向调用,这里标志位就不会设定

18 事件标识
0为当前数据包是请求或响应包
1为当前数据包是心跳包,比如框架为了保活TCP连接,每次客户端和
服务端互相发送心跳包时这个标志位被设定
设置了心跳报文不会透传到业务方法调用,仅用于框架内部保活机制

19〜23 序列化器编号
2 为 Hessian2Serialization
3 为 JavaSerialization
4 为 CompactedJavaSerialization
6 为 FastJsonSerialization
7 为 NativeJavaSerialization
8 为 KryoSerialization
9 为 FstSerialization

24〜31 状态
20 为 OK
30 为 CLIENT TIMEOUT
31 为 SERVER_TIMEOUT
40 为 BAD_REQUEST
50 为 BAD RESPONSE

32〜95 请求编号
这8个字节存储RPC请求的唯一 id,用来将请求和响应做关联

96〜127 消息体长度
占用的4个字节存储消息体长度。在一次RPC请求过程中,消息体中
依次会存储7部分内容

在消息体中,客户端严格按照序列化顺序写入消息,服务端也会遵循相同的顺序读取消息,
客户端发起请求的消息体依次保存下列内容:Dubb。版本号、服务接口名、服务接口版本、方 法名、参数类型、方法参数值和请求额外参数(attachment)。
在协议报文头部的status中,完整状态响应码和作用如表6-2所示。

表6・2完整状态响应码和作用
状态值 状态符号 作 用
20 OK 正确返回
30 CLIENT TIMEOUT 客户端超时
31 SERVERTIMEOUT 服务端超时
40 BADREQUEST 请求报文格式错误
50 BADRESPONSE 响应报文格式错误
60 SERVICE NOT FOUND 未找到匹配的服务
70 SERVICEERROR 服务调用错误
80 SERVER ERROR 服务端内部错误
90 CLIENTERROR 客户端错误
100 SERVER THREADPOOL EXHAUSTED ERROR 服务端线程池满拒绝执行

主要根据以下标记判断返回值,如表6-3所示。
表6-3 Dubbo响应标记
状态值 状态符号 作 用
5 RESPONSE NULL VALUE WITH ATTACHMENTS 响应空值包含隐藏参数
4 RESPONSE VALUE WITH ATTACHMENTS 响应结果包含隐藏参数
3 RESPONSE WITH EXCEPTION WITH ATTACHMENTS 异常返回包含隐藏参数
2 RESPONSENULLVALUE 响应空值
1 RESPONSE VALUE 响应结果
0 RESPONSE WITH EXCEPTION 异常返回

在返回消息体中,会先把返回值状态标记写入输出流,根据标记状态判断RPC是否正常,
比如一次正常RPC调用成功,则先往消息体中写一个标记1,紧接着再写方法返回值。

我们知道在网络通信中(基于TCP 需要解决网络粘包/解包的问题,一些常用解决办法比
如用回车、换行、固定长度和特殊分隔符等进行处理,通过对前面协议的理解,我们很容易发现Dubbo其实就是用特殊符号exdabb魔法数来分割处理粘包问题的。

在实际使用场景中,客户端会使用多线程并发调用服务,Dubbo是如何做到正确响应调用
线程的呢?关键点在于协议头全局请求id标识,我们先来看一下原理图

当客户端多个线程并发请求时,框架内部会调用DefaultFuture对象的get方法进行等待。
在请求发起时,框架内部会创建Request对象,这个时候会被分配一个唯一 id, DefaultFuture
可以从Request对象中获取id,并将关联关系存储到静态HashMap中,就是图6-3中的Futures
集合。当客户端收到响应时,会根据Response对象中的id,从Futures集合中查找对应
DefaultFuture对象,最终会唤醒对应的线程并通知结果。客户端也会启动一个定时扫描线程去探测超时没有返回的请求。
6.3编解码器原理
先熟悉一下编解码设计关系

在图6-4中,AbstractCodec主要提供基础能力,比如校验报文长度和查找具体编解码器等。
TransportCodec主要抽象编解码实现,自动帮我们去调用序列化、反序列实现和自动cleanup流。 我们通过Dubbo编解码继承结构可以清晰看到,DubboCodec继承自ExchangeCodec,它又再次继承了 TelnetCodec实现。我们前面说过Telnet实现复用了 Dubbo协议端口,其实就是在这层编解码做了通用处理。因为流中可能包含多个RPC请求,Dubbo框架尝试一次性读取更多完整报文编解码生成对象,也就是图中的DubboCountCodec,它的实现思想比较简单,依次调用 DubboCodec去解码,如果能解码成完整报文,则加入消息列表,然后触发下一个Handler方法 调用。

6.3.1 Dubbo协议编码器
Dubbo中的编码器主要将Java对象编码成字节流返回给客户端,主要做两部分事情,构造
报文头部,然后对消息体进行序列化处理。所有编解码层实现都应该继承自Exchangecodec,
Dubbo协议编码器也不例外。当Dubbo协议编码请求对象时,会调用ExchangeCodec#encode
方法。我们首先分析编码请求对象,如代码清单6.1所示。

代码清单6-1请求对象编码

代码清单6-1的主要职责是将Dubbo请求对象编码成字节流(包括协议报文头部)。
在①中主要提取URL中配置的序列化协议或默认协议。
在②中会创建16字节的报文头部。
在③中首 先会将魔法数写入头部并占用2个字节。
在④中主要设置请求标识和消息体中使用的序列化协 议。
在⑤中会复用同一个字节,标记这个请求需要服务端返回。
在⑥中主要承载请求的唯一标 识,这个标识用于匹配响应的数据。
在⑦中会在buffer中预留16字节存储头部,
在⑧中会序列 化请求部分,比如方法名等信息,后面会讲解。
在⑨中会检查编码后的报文是否超过大小限制 (默认是8MB)。
在⑩中将消息体长度写入头部偏移量(第12个字节),长度占用4个字节。
在⑪中将buffer定位到报文头部开始,
在⑫中将构造好的头部写入buffer。
在⑬中再将buffer 写入索引执行消息体结尾的下一个位置。
通过上面的请求编码器实现,在理解6.2节协议的基础上很容易理解这里的代码,
在⑧中 会调用encodeRequestData方法对Rpclnvocation调用进行编码,这部分主要就是对接口、方法、 方法参数类型、方法参数等进行编码,在DubboCodec#encodeRequestData中重写了这个方法实 现,
如代码清单6-2所示。

代码清单6-2编码请求对象体

代码清单6-2的主要职责是将Dubbo方法调用参数和值编码成字节流。在编码消息体的时
候,
在①中主要先写入框架的版本,这里主要用于支持服务端版本隔离和服务端隐式参数透传
给客户端的特性。
在②中向服务端写入调用的接口。
在③中指定接口的版本,默认版本为0.0.0,
Dubbo允许同一个接口有多个实现,可以指定版本或分组来区分。
在④中指定远程调用的接口方法。
在⑤中将方法参数类型以Java类型方式传递给服务端。
在⑥中循环对参数值进行序列化。
在⑦中写入隐式参数HashMap,这里可能包含timeout和group等动态参数。

代码清单6-3编码响应对象

代码清单6-3的主要职责是将Dubbo响应对象编码成字节流(包括协议报文头部)。在编码
响应中,
在①中获取用户指定或默认的序列化协议,
在②中构造报文头部(16字节)。
在③中同样将魔法数填入报文头部前2个字节。
在④中会将服务端配置的序列化协议写入头部。
在⑤中报文头部中status会保存服务端调用状态码。
在⑥中会将请求唯一 id设置回响应头中。
在⑦中空出16字节头部用于存储响应体报文。
在⑧中会对服务端调用结果进行编码,后面会进行详 细解释。
在⑨中主要对响应报文大小做检查,默认校验是否超过8MB大小。
在⑩中将消息体长度写入头部偏移量(第12个字节),长度占用4个字节。
在⑪中将buffer定位到报文头部开始,
在⑫中将构造好的头部写入buffer。
在⑬中再将buffer写入索引执行消息体结尾的下一个位置。
在⑭中主要处理编码报错复位buffer,否则导致缓冲区中数据错乱。
在⑥中会将异常响应返回到客户端,防止客户端只有等到超时才能感知服务调用返回。
在⑯和⑰中主要对报错进行了细分,处理服务端报文超过限制和具体报错原因。为了防止报错对象无法在客户端反序列化,在服务端会将异常信息转成字符串处理。 我们再回到编码响应消息提的部分,
在⑧中处理响应,具体实现在DubboCodec#encode
ResponseData中,如代码清单6-4所示。

代码清单6.4编码响应对象

代码清单6-4的主要职责是将Dubbo方法调用状态和返回值编码成字节流。编码响应体也
是比较简单的,
在①中判断客户端的版本是否支持隐式参数从服务端传递到客户端。
在②和③中处理正常服务调用,并且返回值为null的场景,用一个字节标记。
在④中处理方法正常调用并且有返回值,先写一个字节标记并序列化结果。
在⑤中处理方法调用发生异常,写一个字节标记并序列化异常对象。
在⑥中处理客户端支持隐式参数回传,记录服务端Dubbo版本,并返回服务端隐式参数。

除了编码请求和响应对象,还有一种处理普通字符串的场景,这种场景正是为了支持Telnet
协议调用实现的,这里主要是简单读取字符串值处理,后面会继续分析。

6.3.2 Dubbo协议解码器
代码清单6-5解码报文头

整体实现解码过程中要解决粘包和半包问题。
在①中最多读取Dubbo报文头部(16字节), 如果流中不足16字节,则会把流中数据读取完毕。在decode方法中会先判断流当前位置是不是Dubbo报文开始处,在流中判断报文分割点是通过
②判断的(Oxdabb魔法数)。如果当前流 中没有遇到完整Dubbo报文(
在③中会判断流可读字节数),
在④中会为剩余可读流分配存储空间,
在⑤中会将流中数据全部读取并追加在header数组中。当流被读取完后,会查找流中第
一个Dubbo报文开始处的索引,
在⑥中会将buffer索引指向流中第一个Dubbo报文开始处
(Oxdabb)0
在⑦中主要将流中从起始位置(初始buffer的readerindex)到第一个Dubbo报文
开始处的数据保存在header中,
用于⑧解码header数据,目前常用的场景有Telnet调用等。
在正常场景中解析时,
在⑨中首先判断当次读取的字节是否多于16字节,否则等待更多网
络数据到来。
在⑩中会判断Dubbo报文头部包含的消息体长度,然后校验消息体长度是否超过
限制(默认为8MB 。
在⑪中会校验这次解码能否处理整个报文。
在⑫中处理消息体解码,这个是强协议相关的,因此Dubbo协议重写了这部分实现,我们先看一下在DubboCodec中是如 何处理的,如代码清单6-6所示。

代码清单6-6解码请求报文

站在解码器的角度,解码请求一定是通过标志判断类别的,否则不知道是请求还是响应,
Dubbo报文16字节头部长度包含了 FLAG_REQUEST标志位。
①:根据这个标志位创建请求对象,
②:在I/O线程中直接解码(比如在Netty的I/O线程中),然后简单调用decode解码,解码逻辑在后面会详细探讨。
③:实际上不做解码,延迟到业务线程池中解码。
④:将解码消息体作为Rpclnvocation放到请求数据域中。如果解码失败了,则会通过
⑤标记,并把异常原因记录下 来。

这里没有提到的是心跳和事件的解码,这两种解码非常简单,心跳报文是没有消息体的,
事件有消息体,在使用Hessian2协议的情况下默认会传递字符R,当优雅停机时会通过发送
readonly事件来通知客户端服务端不可用

代码清单6・7解码请求消息体

在解码请求时,是严格按照客户端写数据顺序来处理的。
在①中会读取远端传递的框架版本,
在②中会读取调用接口全称,
在③中会读取调用的服务版本,用来实现分组和版本隔离。
在④中会读取调用方法的名称,
在⑤中读取方法参数类型,通过类型能够解析出实际参数个数。
在⑥中会对方法参数值依次读取,这里具体解析参数值是和序列化协议相关的。
在⑦中读取隐式参数,比如同机房优先调用会读取其中的tag值。
⑧是为了支持异步参数回调,因为参数是
回调客户端方法,所以需要在服务端创建客户端连接代理。
解码响应和解码请求类似,解码响应会调用DubboCodec#decodeBody方法,为了节省篇幅,
我们重点讲解解码响应的结果值。当方法调用返回时,会触发DecodeableRpcResult#decode方法调用,解析响应报文

代码清单6・8解析响应报文

在读取服务端响应报文时,先读取状态标志,然后根据状态标志判断后续的数据内容。在
代码清单6 4编码响应对象中,响应结果首先会写一个字节标记位。
在①中处理标记位代表返回值为Null的场景。
②代表正常返回,首先判断请求方法的返回值类型,返回值类型方便底层
反序列化正确读取,将读取的值存在result字段中。
在④中处理服务端返回异常对象的场景,同时会将结果保存在exception字段中。
在⑤中处理返回值为Null,并且支持服务端隐式参数透
传给客户端,在客户端会继续读取保存在HashMap中的隐式参数值。当然,还有其他场景,比 如RPC调用有返回值,RPC调用抛出异常时需要隐式参数给客户端的场景,可以举一反三,不再重复说明。
6.4 Telnet调用原理
6.4.1 Telnet指令解析原理
为了支持未来更多的Telnet命令和扩展性,Telnet指令解析被设置成了扩展点TelnetHandler,
每个Telnet指令都会实现这个扩展点

代码清单6-9 TelnetHandler定义

代码清单6-10 Telnet转发解析

理解编解码后,可以更好地理解上层的实现和原理,
在①中提取Telnet一行消息的首个字符串作为命令,如果命令行有空格,则将后面的内容作为字符串,再通过
②提取并存储到message中。
在③中判断并加载是否有对应的扩展点,如果存在对应的Telnet扩展点,
则会通过④加载具体的扩展点并调用其telnet方法,最后连同返回结果并追加消息结束符(在⑤中处理)返回给调用方。

代码清单6・11 Telnet本地方法调用

当本地没有客户端,想测试服务端提供的方法时,可以使用Telnet登录到远程服务器
(Telnet IP port),根据invoke指令执行方法调用来获得结果。当用户输入invoke指令时,
会被转发到代码清单6-11对应的Handler
在①中提取方法调用信息(去除参数信息),
在②中会提取调用括号内的信息作为参数值。
在③中提取方法调用的接口信息,
在④中提取接口调用的方法名称。
在⑤中会将传递的JSON参数值转换成fastjson对象,然后
在⑥中根据接口名称、方法和参数值查找对应的方法和Invoker对象。
在真正方法调用前,需要通过
⑦把fastjson对象转换成Java对象,在⑧中触发方法调用并返回结果值。
6.4.2 Telnet实现健康监测
Telnet提供了健康检查的命令,可以在Telnet连接成功后执行status -1查看线程池、内
存和注册中心等状态信息。为了完成线程池监控、内存和注册中心监控等诉求,Telnet提供了新的扩展点Statuschecke

代码清单6・12健康检查扩展点

当执行status命令时会触发StatusTelnetHandler#telnet调用,这个方法的实现也比较简
单,它会加载所有实现Statuschecker扩展点的类,然后调用所有扩展点的check方法

6.5 ChannelHandler
Dubbo框架内部使用大量Handler组成类似链表,依次处理具体逻辑,比如编解码、心跳时间戳和方法调用Handler等。因为Netty每次创建Handler都会经过ChannelPipeline,大量的事件经过很多Pipeline会有较多的开销,因此Dubbo会将多个Handler聚合为一个Handler
6.5.1核心Handler和线程模型
Dubbo中Handler的5种状态
connected Channel已经被创建
disconnected Channel已经被断开
sent 消息被发送
received 消息被接收
caught 捕获到异常

Dubbo针对每个特性都会实现对应的ChannelHandler:

Dubbo中提供了大量的Handler去承载特性和扩展,这些Handler最终会和底层通信框架做
关联,比如Netty等。一次完整的RPC调用贯穿了一系列的Handler,如果直接挂载到底层通信框架(Netty),因为整个链路比较长,则需要触发大量链式查找和事件,不仅低效,而且浪费资源。

图6-5展示了同时具有入站和出站ChannelHandler的布局,如果有一个入站事件被触发,
比如连接或数据读取,那么它会从ChannelPipeline头部开始一直传播到ChannelPipeline的尾端。出站的I/O事件将从ChannelPipeline最右边开始,然后向左传播。当然,在ChannelPipeline传播事件时,它会测试入站是否实现了ChannelInboundHandler接口,如果没有实现则会自动跳过,出站时会监测是否实现channel0utboundHandler,如果没有实现,那么也会自动跳过。在Dubbo框架中实现的这两个接口类主要是NettyServerHandler和 NettyClientHandler。Dubbo通过装饰者模式层包装Handler,从而不需要将每个Handler 都追加到Pipeline中。在NettyServer和NettyClient中最多有3个Handler,分别是编码、解码和NettyServerHandler或NettyclientHandler。
6.5.2Dubbo请求响应Handler
在Dubbo框架内部,所有方法调用会被抽象成Request/Response,每次调用(一次会话)都会创建一个请求Request,如果是方法调用则会返回一个Response对象。HeaderExchangeHandler
用来处理这种场景,它主要负责以下4种事情。
(1)更新发送和读取请求时间戳。
(2)判断请求格式或编解码是否有错,并响应客户端失则的具体原因。
(3)处理Request请求和Response正常响应。
(4)支持Telnet 调用。

6.5.3 Dubbo心跳Handler
Dubbo默认客户端和服务端都会发送心跳报文,用来保持TCP长连接状态。在客户端和服
务端,Dubbo内部开启一个线程循环扫描并检测连接是否超时,在服务端如果发现超时则会主动关闭客户端连接,在客户端发现超时则会主动重新创建连接。默认心跳检测时间是60秒,具体应用可以通过heartbeat进行配置。

6.6小结
本章首先讲解了Dubbo调用原理和流程,同时对Dubbo 的协议做了详细的讲解,这里的基
础知识对RPC调用来说至关重要。在讲解完协议的基础上,我们又对Dubbo实现编解码、解决粘包和解包做了深入探讨。
本章重点在RPC 调用,以及处理常规方法调用,我们也对本地Telnet调用的设计和实现原
理做了说明。在实际开发过程中,不熟悉Dubbo开发的人员也能快速通过fastjson方式测试和验证服务,在 Telnet健康检查方面我们也做了进一步的说明。
最后,我们对Dubbo中比较重要的Handler,比如Request/Response模型Handler和心跳
Handler等做了详细的解析,同时对Dubbo的线程模型做了剖析。后面的关注点会聚焦于解决业务问题和服务治理上。

第7章Dubbo集群容错
7.1Cluster层概述
我们可以把Cluster看作一个集群容错层,该层中包含Cluster、Directory.Router.LoadBalance
几大核心接口。注意这里要区分Cluster层和Cluster接口,Cluster层是抽象概念,表示的是对外的整个集群容错层;Cluster是容错接口,提供Failover、Failfast等容错策略。
由于Cluster层的实现众多,因此本节介绍的流程是一个基于AbstractClusterInvoker的全
量流程,某些实现可能只使用了该流程的一小部分。Cluster的总体工作流程可以分为以下几步:
(1)生成Invoker对象。不同的Cluster实现会生成不同类型的clusterInvoker对象并返
回。然后调用clusterInvoker的 Invoker方法,正式开始调用流程。
(2)获得可调用的服务列表。首先会做前置校验,检查远程服务是否已被销毁。然后通过
Directory#list方法获取所有可用的服务列表。接着使用Router接口处理该服务列表,根据路
由规则过滤一部分服务,最终返回剩余的服务列表。
(3)做负载均衡。在第2步中得到的服务列表还需要通过不同的负载均衡策略选出一个服
务,用作最后的调用。首先框架会根据用户的配置,调用ExtensionLoader获取不同负载均衡策略的扩展点实现(具体负载均衡策略会在后面讲解)。然后做一些后置操作,如果是异步调用则设置调用编号。接着调用子类实现的doInvoke方法(父类专门留了这个抽象方法让子类实现),子类会根据具体的负载均衡策略选出一个可以调用的服务。
(4)做RPC 调用。首先保存每次调用的Invoker到RPC上下文,并做RPC调用。然后处
理调用结果,对于调用出现异常、成功、失败等情况,每种容错策略会有不同的处理方式。

图7-1是一个全量的通用流程,其中1〜3步都是在抽象方法Abstractclusterinvoker中
实现的,可以理解为通用的模板流程,主要做了校验、参数准备等工作,最终调用子类实现的dolnvoke方法。不同的Clusterinvoker子类都继承了该抽象类,子类会在上述流程中做个性化的裁剪。

7.2容错机制的实现
Cluster接口一共有9种不同的实现,每种实现分别对应不同的ClusterInvoker。本节会介
绍继承了AbstractClusterInvoker的7种ClusterInvoker实现,Merge和Mock属于特殊机制,
会在其他章节讲解。
7.2.1容错机制概述
Dubbo容错机制能增强整个应用的鲁棒性,容错过程对上层用户是完全透明的,但用户也
可以通过不同的配置项来选择不同的容错机制。每种容错机制又有自己个性化的配置项。Dubbo中现有Failover、Failfast、Failsafe、Failback、Forking、Broadcast等容错机制,容错机制的特性
如表7-1所示。

表7-1容错机制的特性
机制名
机制简介

Failover
当出现失败时,会重试其他服务器。用户可以通过retries="2"设置重试次数。这是
Dubbo的默认容错机制,会对请求做负载均衡。通常使用在读操作或幂等的写操作上,
但重试会导致接口的延迟增大,在下游机器负载已经达到极限时,重试容易加重下游
服务的负载

Failfast
快速失败,当请求失败后,快速返回异常结果,不做任何重试。该容错机制会对请求
做负载均衡,通常使用在非幂等接口的调用上。该机制受网络抖动的影响较大

Failsafe
当出现异常时,直接忽略异常。会对请求做负载均衡。通常使用在“佛系”调用场景,
即不关心调用是否成功,并且不想抛异常影响外层调用,如某些不重要的日志同步,
即使出现异常也无所谓

Failback
请求失败后,会自动记录在失败队列中,并由一个定时线程池定时重试,适用于一些
异步或最终一致性的请求。请求会做负载均衡

Forking
同时调用多个相同的服务,只要其中一个返回,则立即返回结果。用户可以配置
forks="最大并行调用数"参数来确定最大并行调用的服务数量。通常使用在对接口
实时性要求极高的调用上,但也会浪费更多的资源

Broadcast
广播调用所有可用的服务,任意一个节点报错则报错。由于是广播,因此请求不需要
做负载均衡。通常用于服务状态更新后的广播

Mock
提供调用失败时,返回伪造的响应结果。或直接强制返回伪造的结果,不会发起远程
调用
Available
最简单的方式,请求不会做负载均衡,遍历所有服务列表,找到第一个可用的节点,
直接请求并返回结果。如果没有可用的节点,则直接抛出异常

Mergeable
Mergeable可以自动把多个节点请求得到的结果进行合并

Cluseter的具体实现:用户可以在dubbo:service,dubbo:reference,dubbo:consumer.
标签上通过cluster属性设置。

对于Failover容错模式,用户可以通过retries属性来设置最大重试次数。可以设置在
dubbo:reference标签上,也可以设置在细粒度的方法标签dubbo:method上。

对于Forking容错模式,用户可通过forks="最大并行数"属性来设置最大并行数。假设设置
的forks数为n,可用的服务数为v,当n<v时,即可用的服务数大于配置的并行数,则并行请求n个服务;当n>v时,即可用的服务数小于配置的并行数,则请求所有可用的服务v。

7.2.2Cluster接口关系
在微服务环境中,可能多个节点同时都提供同一个服务。当上层调用Invoker时,无论实际
存在多少个Invoker,只需要通过Cluster层,即可完成整个调用的容错逻辑,包括获取服务列表、路由、负载均衡等,整个过程对上层都是透明的。

当然,Cluster接口只是串联起整个逻辑,其中 clusterInvoker只实现了容错策略部分,其他逻辑则是调用了Directory、Router.LoadBalance等接口实现。

容错的接口主要分为两大类,第一类是Cluster类,第二类是ClusterInvoker类。Cluster和ClusterInvoker之间的关系也非常简单:Cluster接口下面有多种不同的实现,每种实现中都需要实现接口的join方法,在方法中会“new”一个对应的ClusterInvoker实现。我们以FailoverCluster

FailoverCluster是 Cluster的其中一种实现,FailoverCluster中直接创建了一个新的
FailoverClusterInvoker并返回。FailoverClusterInvoker继承的接口是Invoker。
7.2.3Failover策略
Cluster接口上有SPI注解@SPI(Failovercluster.NAME),即默认实现是Failover.
7.2.4Failfast策略
Failfast 会在失败后直接抛出异常并返回
7.2.5Failsafe策略
Failsafe 调用时如果出现异常,则会直接忽略
7.2.6Failback策略
Failback如果调用失败,则会定期重试
7.2.7Available策略
Available是找到第一个可用的服务直接调用,并返回结果
7.2.8Broadcast 策略
Broadcast会广播给所有可用的节点,如果任何一个节点报错,则返回异常
7.2.9Forking 策略
Forking可以同时并行请求多个服务﹐有任何一个返回,则直接返回
7.3Directory的实现
整个容错过程中首先会使用Directory#list来获取所有的Invoker列表。Directory也有多
种实现子类,既可以提供静态的Invoker列表,也可以提供动态的Invoker列表。静态列表是用户自己设置的Invoker列表;动态列表根据注册中心的数据动态变化,动态更新Invoker列表的数据,整个过程对上层透明。

又是熟悉的“套路”,使用了模板模式。Directory是顶层的接口。AbstractDirectory封装了
通用的实现逻辑。抽象类包含RegistryDirectory和StaticDirectory两个子类。下面分别介绍每个类的职责和工作:
(1)AbstractDirectory。封装了通用逻辑,主要实现了四个方法:检测Invoker是否可用,
销毁所有Invoker,list方法,还留了一个抽象的doList方法给子类自行实现。
(2)RegistryDirectory。属于Directory 的动态列表实现,会自动从注册中心更新Invoker
列表、配置信息、路由列表。
(3)StaticDirectory。Directory的静态列表实现,即将传入的Invoker列表封装成静态的
Directory对象,里面的列表不会改变。
7.3.2RegistryDirectory的实现
RegistryDirectory中有两条比较重要的逻辑线,第一条,框架与注册中心的订阅,并动态更
新本地Invoker列表、路由列表、配置信息的逻辑;第二条,子类实现父类的doList方法。
7.4路由的实现
通过7.3节的Directory获取所有Invoker列表的时候,就会调用到本节的路由接口。路由
接口会根据用户配置的不同路由策略对Invoker列表进行过滤,只返回符合规则的Invoker。例如:如果用户配置了接口A的所有调用,都使用IP为192.168.1.22的节点,则路由会过滤其他的Invoker,只返回IP为192.168.1.22的 Invoker。
7.4.1路由的总体结构
路由分为条件路由、文件路由、脚本路由,对应dubbo-admin中三种不同的规则配置方式。
条件路由是用户使用Dubbo定义的语法规则去写路由规则;文件路由则需要用户提交一个文件,里面写着对应的路由规则,框架基于文件读取对应的规则;脚本路由则是使用JDK自身的脚本引擎解析路由规则脚本,所有JDK脚本引擎支持的脚本都能解析,默认是JavaScript。我们先来看一下接口之间的关系,如图7-9所示。
7.4.2条件路由的参数规则
条件路由使用的是condition://协议,URL形式是"condition:// 0.0.0.0/com.foo.BarService?
category=routers&dynamic=false&rule="+URL.encode(“host=10.20.153.10=>host=10.20.153.11”)
7.4.4文件路由的实现
文件路由是把规则写在文件中,文件中写的是自定义的脚本规则,可以是JavaScript、Groovy
7.4.5脚本路由的实现
脚本路由使用JDK自带的脚本解析器解析脚本并运行,默认使用JavaScript解析器
7.5负载均衡的实现
在整个集群容错流程中,首先经过Directory获取所有Invoker列表,然后经过Router根据
路由规则过滤Invoker,最后幸存下来的Invoker还需要经过负载均衡这一关,选出最终要调用的Invoker。

容错策略中的负载均衡都使用了抽象父类AbstractClusterInvoker中定义的Invoker select
方法,而并不是直接使用LoadBalance方法。因为抽象父类在LoadBalance的基础上又封装了一些新的特性:
7.5.2负载均衡的总体结构
Dubbo现在内置了4种负载均衡算法,用户也可以自行扩展,因为LoadBalance接口上有
@SPI注解,

从代码中我们可以知道默认的负载均衡实现就是RandomLoadBalance,即随机负载均衡。
由于select方法上有@Adaptive(“loadbalance”)注解,因此我们在URL中可以通过
loadbalance=xxx来动态指定select时的负载均衡算法。

4种负载均衡算法都继承自同一个抽象类,使用的也是模板模式,抽象父类中已经把通用
的逻辑完成,留了一个抽象的doSelect方法给子类实现。

7.5.3Random负载均衡
Random负载均衡是按照权重设置随机概率做负载均衡的
7.5.4RoundRobin负载均衡
权重轮询负载均衡会根据设置的权重来判断轮询的比例
7.5.5LeastActive负载均衡
LeastActive负载均衡称为最少活跃调用数负载均衡,即框架会记下每个Invoker的活跃数,
每次只从活跃数最少的Invoker里选一个节点。这个负载均衡算法需要配合ActiveLimitFilter
过滤器来计算每个接口方法的活跃数。最少活跃负载均衡可以看作 Random 负载均衡的“加强
7.5.6一致性Hash负载均衡
一致性Hash负载均衡可以让参数相同的请求每次都路由到相同的机器上。这种负载均衡的
方式可以让请求相对平均,相比直接使用Hash而言,当某些节点下线时,请求会平摊到其他服务提供者,不会引起剧烈变动。普通一致性 Hash的简单示例如图7-12所示。
7.6Merger的实现
当一个接口有多种实现,消费者又需要同时引用不同的实现时,可以用group来区分不同
的实现,如下所示。
<dubbo:service group=“group1” interface=“com. xxx.testService” />
<dubbo:service group=“group2” interface=“com.xxx.testService” />
7.7Mock
在Cluster中,还有最后一个MockClusterWrapper,由它实现了Dubbo的本地伪装。这个功
能的使用场景较多,通常会应用在以下场景中:服务降级;部分非关键服务全部不可用,希望
主流程继续进行;在下游某些节点调用异常时,可以以Mock的结果返回。
7.7.1Mock常见的使用方式
Mock只有在拦截到RpcException的时候会启用,属于异常容错方式的一种。业务层面其
实也可以用try-catch来实现这种功能,如果使用下沉到框架中的Mock机制,则可以让业务的实现更优雅

/配置方式1:可以在配置文件中配置
<dubbo:reference interface=“com.foo.BarService” mock=“true”/>
<dubbo:reference interface=“com.foo.BarService” mock=“com.foo.BarServiceMock” /><—
配置方式2
<dubbo:reference interface=“com.foo.BarService” mock=“return null”/><
配置方式3

提供Mock实现,如果Mock配置了true或 default,则实现的类名必须是接口名+Mock,如配置方式1 否则会直接取Mock参数值作为Mock实现类,

如配置方式2
package com.foo;<
public class BarServiceMock implements BarService {
public String sayHello(String name) i
return"容错数据";

可以伪造容错数据,此方法只在出现RpcException时被执行当接口配置了Mock,在RPC调用抛出RpcException时就会执行Mock方法。最后一种returnnull 的配置方式通常会在想直接忽略异常的时候使用。

服务的降级是在dubbo-admin中通过override协议更新Invoker的Mock参数实现的。如果
Mock参数设置为mock=force:return+null,则表明是强制Mock,强制Mock 会让消费者对该
服务的调用直接返回null,不再发起远程调用。通常使用在非重要服务已经不可用的时候,可以屏蔽下游对上游系统造成的影响。

此外,还能把参数设置为mock=fail:return+null,这样消费者还是会发起远程调用,不过失败后会返回null,但是不抛出异常。最后,如果配置的参数是以throw开头的,即mock=throw,则直接抛出RpcException,不会发起远程调用。
7.7.2Mock的总体结构
Mock涉及的接口比较多,整个流程贯穿Cluster和 Protocol层
7.7.3Mock的实现原理
1.MockClusterInvoker的实现原理
MockClusterWrapper是一个包装类,它在创建MockClusterInvoker的时候会把被包装的
Invoker传入构造方法,因此 MockClusterInvoker内部天生就含有一个Invoker的引用。
7.8小结
本章的内容较多,首先介绍了整个集群容错层的总体结构,讲解了7种普通集群容错策略
的实现原理——都使用了模板模式,继承了AbstractClusterInvoker,在 AbstractClusterInvoker
中完成了总体的抽象逻辑,并留了一个抽象方法让子类实现自己的独特功能。其次我们介绍了整个集群容错层都会使用的Directory 接口,重点讲解RegistryDirectory监听注册中心,并动态更新本地缓存的Invoker列表、路由列表、配置列表。然后我们讲解了相关的路由接口、负载均衡接口的实现原理,介绍了三种不同路由规则的实现方式和四种不同负载均衡策略的实现方式。接着讲解了特殊容错机制Merger,包含默认合并器的总体大图,以及具体Merge的实现步骤。最后讲解了Mock机制的实现,分为Cluster层的逻辑线,以及Protocol层的逻辑线。
第8章Dubbo扩展点
本章主要内容:
核心扩展点概述;
RPC层扩展点;
Remote层扩展点;
其他扩展点。

我们在第4章已经了解了Dubbo的SPI扩展机制,本章主要介绍在整个框架中有哪些已有
的接口是可以扩展的,主要涉及扩展接口的作用,原理性的内容相对较少。首先介绍整个框架中核心扩展点的总体大图,让读者对这些扩展点有一个总体的了解。其次从上到下介绍整个RPC层的扩展点。然后介绍Remote层的扩展点。最后会把其他一些零散的扩展点也简单介绍一下。
8.1 Dubbo核心扩展点概述
8.1.1扩展点的背景
实现Dubbo的“微内核+富生态”的技术发展策略。
8.1.2扩展点整体架构
如果按照使用者和开发者两种类型来区分,Dubbo可以分为API层和SPI层。API层让用
户只关注业务的配置,直接使用框架的API即可;SPI层则可以让用户自定义不同的实现类来
扩展整个框架的功能。

如果按照逻辑来区分,那么又可以把 Dubbo从上到下分为业务、RPC、Remote三个领域。
由于业务层不属于SPI的扩展,因此不是本章关注的内容。可扩展的RPC和Remote层继续细分,又能分出7层,如图8-1所示。

图8-1中已经把监控层(Monitor层)移除,因为监控层的实现过于简单,此外即使没有监
控层也不会影响整个主流程的进行,因此不再单独讲解。另外,细分出来的每一层(Proxy、
Registry…)的作用,已经在第1章中介绍,因此本章不再重复赘述。
图8-1中已经把每一层的扩展点接口列了出来。在下面的章节中,我们将逐一讲解每个扩
展点的作用和约束。

8.2RPC层扩展点
按照完整的Dubbo结构分层,RPC层可以分为四层:Config、Proxy、Registry、Cluster。
由于Config属于API的范畴,因此我们只基于Proxy、Registry、Cluster三层来介绍对应的扩
展点。
8.2.1 Proxy层扩展点
Proxy层主要的扩展接口是ProxyFactory。由于ProxyFactory帮我们生成了代理类,当我们调用某个远程接口时,实际上使用的是代理类。
Dubbo中的ProxyFactory有两种默认实现:Javassist和JDK,用户可以自行扩展自己的实现,如CGLIB。
Dubbo选用Javassist作为默认字节码生成工具,主要是基于性能和使用的
简易性考虑,Javassist 的字节码生成效率相对于其他库更快,使用也更简单。

已有的扩展点实现
扩展key 名 扩展类名
Stub org.apache.dubbo.rpc.proxy.wrapper.StubProxyFactory Wrapper
Jdk org.apache.dubbo.rpc.proxy.jdk.JdkProxyFactory
Javassist org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory

8.2.2Registry层扩展点
Registry层可以理解为注册层,这一层中最重要的扩展点就是org.apache.dubbo.registry.
RegistryFactory。整个框架的注册与服务发现客户端都是由这个扩展点负责创建的。该扩展点有@Adaptive({ “protocol”})注解,可以根据URL中的protocol参数创建不同的注册中心客户

在 Dubbo,有 AbstractRegistryFactory已经抽象了一些通用的逻辑,用户可以直接继承
该抽象类实现自定义的注册中心工厂。已有的RegistryFactory实现如表8-2所示。

表8-2已有的RegistryFactory实现
扩展key 名 扩展类名
Zookeeper org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory
Redis org.apache.dubbo.registry.redis.RedisRegistryFactory
Multicast org.apache.dubbo.registry.multicast.MulticastRegistryFactory
Dubbo org.apache.dubbo.registry.multicast.MulticastRegistryFactory
8.2.3 Cluster层扩展点
Cluster层负责了整个Dubbo框架的集群容错,涉及的扩展点较多,包括容错(Cluster)、
路由(Router)、负载均衡(LoadBalance)、配置管理工厂(ConfiguratorFactory)和合并器(Merger)。

1.Cluster扩展点
Cluster需要与Cluster层区分开,Cluster主要负责一些容错的策略,也是整个集群容错的
入口。当远程调用失败后,由Cluster负责重试、快速失败等,整个过程对上层透明。整个集群容错层之间的关系,已经在第7章有比较详细的讲解了,因此不再赘述。

Cluster扩展点主要负责Dubbo框架中的容错机制,如Failover.Failfast等,默认使用Failover
机制。Cluster扩展点接口如代码清单8-3所示。
代码清单8-3Cluster扩展点接口
@SPI(FailoverCluster.NAME)
public interface cluster {
@Adaptive
Invoker join(Directory directory) throws RpcException;

Cluster接口只有一个join方法,并且有@Adaptive注解,说明会根据配置动态调用不同的
容错机制。已有的Cluster实现如表8-3所示。

2.RouterFactory扩展点
RouterFactory是一个工厂类,顾名思义,就是用于创建不同的Router。假设接口A有多个
服务提供者提供服务,如果配置了路由规则(某个消费者只能调用某个几个服务提供者),则Router会过滤其他服务提供者,只留下符合路由规则的服务提供者列表。

现有的路由规则支持文件、脚本和自定义表达式等方式。接口上有@Adaptive(“protocol”)
注解,会根据不同的protocol自动匹配路由规则,如代码清单8-4所示。
代码清单8-4RouterFactory扩展点实现
@SPI
public interface RouterFactory {
@Adaptive( “protocol”)
Router getRouter(URL url);

3.LoadBalance扩展点
LoadBalance是 Dubbo框架中的负载均衡策略扩展点,框架中已经内置随机(Random)、
轮询(RoundRobin)、最小连接数(LeastActive)、一致性Hash(ConsistentHash)这几种负载均
衡的方式,默认使用随机负载均衡策略。LoadBalance主要负责在多个节点中,根据不同的负载均衡策略选择一个合适的节点来调用。由于在集群容错章节对负载均衡也有比较深入的讲解,因此也不再赘述。LoadBalance扩展点接口源码如代码清单8-5所示。

4.ConfiguratorFactory 扩展点
ConfiguratorFactory是创建配置实例的工厂类,现有override和absent两种工厂实现,分
别会创建overrideConfigurator和 AbsentConfigurator两种配置对象。默认的两种实现,
OverrideConfigurator会直接把配置中心中的参数覆盖本地的参数;AbsentConfigurator会先
看本地是否存在该配置,没有则新增本地配置,如果已经存在则不会覆盖。ConfiguratorFactory

扩展点源码如代码清单8-6所示。
代码清单8-6ConfiguratorFactory 扩展点源码
@SPI
public interface ConfiguratorFactory i
@Adaptive(“protocol”)
Configurator getConfigurator(URL url);

该扩展点的方法上也有@Adaptive( “protocol”)注解,会根据URL中的protocol配置值使
用不同的扩展点实现。框架中内置的扩展点实现如表8-6所示。

5.Merger扩展点
Merger是合并器,可以对并行调用的结果集进行合并,例如:并行调用A、B两个服务都
会返回一个List结果集,Merger可以把两个List合并为一个并返回给应用。默认已经支持map、set、list、 byte等11种类型的返回值。用户可以基于该扩展点,添加自定义类型的合并器。Merger

扩展点源码如代码清单8-7所示。
代码清单8-7Merger 扩展点源码
@SPI
public interface Merger {
merge(T… items);
}

8.3Remote层扩展点
Remote处于整个Dubbo框架的底层,涉及协议、数据的交换、网络的传输、序列化、线程
池等,涵盖了一个远程调用的所有要素。

Remote层是对Dubbo传输协议的封装,内部再划为Transport传输层和Exchange信息交换
层。
其中Transport层只负责单向消息传输,是对Mina、Netty等传输工具库的抽象。
而Exchange层在传输层之上实现了Request-Response 语义,这样我们可以在不同传输方式之上都能做到统一的请求/响应处理。
Serialize层是RPC的一部分,决定了在消费者和服务提供者之间的二进制数据传输格式。不同的序列化库的选择会对RPC 调用的性能产生重要影响,目前默认选择是Hessian2序列化。
8.3.1Protocol层扩展点
Protocol层主要包含四大扩展点,分别是Protocol,Filter、ExporterListener和InvokerListener.
其中Protocol、Filter这两个扩展点使用得最多。下面分别介绍每个扩展点。
1.Protocol扩展点
Protocol是Dubbo RPC的核心调用层,具体的RPC协议都可以由Protocol点扩展。如果想
增加一种新的RPC协议,则只需要扩展一个新的Protocol扩展点实现即可。

2.Filter扩展点
Filter是Dubbo的过滤器扩展点,可以自定义过滤器,在Invoker调用前后执行自定义的逻
辑。在Filter的实现中,必须要调用传入的Invoker的invoke方法,否则整个链路就断了。Filter接口定义及实现的示例如代码清单8-9所示。

3.ExporterListener/InvokerListener 扩展点
ExporterListener和InvokerListener这两个扩展点非常相似,ExporterListener是在暴露和取消
暴露服务时提供回调;InvokerListener则是在服务的引用与销毁引用时提供回调。
8.3.2Exchange层扩展点
Exchange层只有一个扩展点接口Exchanger,这个接口主要是为了封装请求/响应模式,例
如:把同步请求转化为异步请求。默认的扩展点实现是org.apache.dubbo.remoting.exchange.
support.header.HeaderExchanger。每个方法上都有@Adaptive注解,会根据URL中的
Exchanger参数决定实现类。

既然已经有了Transport层来传输数据了,为什么还要有Exchange层呢?因为上层业务关
注的并不是诸如Netty这样的底层Channel。上层一个Request只关注对应的Response,对于是同步还是异步请求,或者使用什么传输根本不关心。Transport层是无法满足这项需求的,Exchange层因此实现了Request-Response模型,我们可以理解为基于Transport层做了更高层次的封装
8.3.3Transport层扩展点
Transport层为了屏蔽不同通信框架的异同,封装了统一的对外接口。主要的扩展点接口有
Transporter、Dispatcher、Codec2和ChannelHandler。
其中,ChannelHandler主要处理连接相关的事件,例如:连接上、断开、发送消息、收到
消息、出现异常等。虽然接口上有SPI注解,但是在框架中实现类的使用却是直接“new”的
方式。因此不在本章做过多介绍。

1.Transporter 扩展接口
Transporter屏蔽了通信框架接口、实现的不同,使用统一的通信接口。Transporter扩展接
口如代码清单8-12所示。
代码清单8-12Transporter扩展接口
@SPI(“netty”)
public interface Transporter {

2.Dispatcher 扩展接口
如果有些逻辑的处理比较慢,例如:发起IO请求查询数据库、请求远程数据等,则需要
使用线程池。因为I/O速度相对CPU是很慢的,如果不使用线程池,则线程会因为IO导致同步阻塞等待。Dispatcher扩展接口通过不同的派发策略,把工作派发到不同的线程池,以此来应对不同的业务场景。Dispatcher扩展接口如代码清单8-13所示。

3.Codec2扩展接口
Codec2主要实现对数据的编码和解码,但这个接口只是需要实现编码/解码过程中的通用逻
辑流程,如解决半包、粘包等问题。该接口属于在序列化上封装的一层。

4.ThreadPool扩展接口
我们在Transport层由Dispatcher实现不同的派发策略,最终会派发到不同的ThreadPool中
执行。ThreadPool扩展接口就是线程池的扩展。

8.3.4Serialize层扩展点
Serialize层主要实现具体的对象序列化,只有Serialization一个扩展接口。Serialization是具
体的对象序列化扩展接口,即把对象序列化成可以通过网络进行传输的二进制流。

1.Serialization扩展接口
Serialization就是具体的对象序列化,Serialization扩展接口如代码清单8-16所示。
代码清单8-16Serialization扩展接口
@SPI(“hessian2”)
public interface Serialization {

8.4其他扩展点
还有其他的一些扩展点接口:TelnetHandler、StatusChecker、Container、CacheFactory、
Validation、LoggerAdapter和 Compiler。由于平时使用得比较少,因此归类到其他扩展点中,下面简单介绍每个扩展点的用途。
1.TelnetHandler扩展点
我们知道,Dubbo框架支持Telnet命令连接,TelnetHandler接口就是用于扩展新的Telnet
命令的接口。

2.StatusChecker扩展点
通过这个扩展点,可以让 Dubbo框架支持各种状态的检查,默认已经实现了内存和 load的检查。用户可以自定义扩展,如硬盘、CPU等的状态检查。已有的实现如下所示。

3.Container扩展点
服务容器就是为了不需要使用外部的Tomcat、JBoss等Web容器来运行服务,因为有可能
服务根本用不到它们的功能,只是需要简单地在Main方法中暴露一个服务即可。此时就可以使用服务容器。Dubbo中默认使用Spring作为服务容器。

4.CacheFactory 扩展点
我们可以通过dubbo:method配置每个方法的调用返回值是否进行缓存,用于加速数据访问
速度。

5.Validation扩展点
该扩展点主要实现参数的校验,我们可以在配置中使用实现参数的校验。

6.LoggerAdapter扩展点
日志适配器主要用于适配各种不同的日志框架,使其有统一的使用接口。已知的扩展点实
现如下:

7.Compiler 扩展点
我们在第4章讲Dubbo扩展点加载机制的时候就提到:@Adaptive注解会生成Java代码,
然后使用编译器动态编译出新的Class。Compiler接口就是可扩展的编译器,现有两个具体的实现( adaptive不算在内):
第9章Dubbo高级特性
本章主要内容:
Dubbo高级特性概述;
Dubbo高级特性原理。

9.1Dubbo高级特性概述
目前Dubbo框架在支持RPC通信的基础上,提供了大量的高级特性,比如服务端Telnet
调用、Telnet 调用统计、服务版本和分组隔离、隐式参数、异步调用、泛化调用、上下文信息和结果缓存等特性。本章会对常用的高级特性原理做进一步分析,在表9-1中展示了目前Dubbo支持的高级特性。

9.2服务分组和版本
Dubbo中提供的服务分组和版本是强隔离的,如果服务指定了服务分组和版本,则消费方
调用也必须传递相同的分组名称和版本名称。

服务暴露直接配置version属性即可,如果要为服务指定分组,则继续添加group属性即可。因为这个特性是强隔离的,消费方必须在配置文件中指定消费的版本。如果消费方式为泛化调用或注解引用,那么也需要指定对应的相同名称的版本号,

当Dubbo客户端启动时,实际上会把调用接口所有的协议节点都拉取下来,然后根据本地URL配置的接口、category、分组和版本做过滤,具体过滤是在注册中心层面实现的。会把所有服务列表进行一次过滤,
9.3参数回调
Dubbo支持异步参数回调,当消费方调用服务端方法时,允许服务端在某个时间点回调回
客户端的方法。在服务端回调到客户端时,服务端不会重新开启TCP连接,会复用已经建立的从客户端到服务端的TCP连接。

再服务端和客户端写好对应的代码之后配置一下

可以发现服务提供方要想实现回调,就需要指定回调方法参数是否为回调,对于客户端消
费方来说没有任何区别。
实现异步回调的原理比较容易理解,客户端在启动时,会拉取服务CallbackService元数据,
因为服务端配置了异步回调信息,这些信息会透传给客户端。客户端在编码请求时,会发现第2个方法参数为回调对象。此时,客户端会暴露一个Dubbo协议的服务,服务暴露的端口号是本地TCP连接自动生成的端口。
9.4隐式参数
Dubbo服务提供者或消费者启动时,配置元数据会生成URL,一般是不可变的。在很多实
际的使用场景中,在服务运行期需要动态改变属性值,在做动态路由和灰度发布场景中需要这个特性。Dubbo框架支持消费方在RpcContext#setAttachment方法中设置隐式参数,在服务端RpcContextgetAttachment方法中获取隐式传递。

当客户端发起调用前,设置隐藏参数,框架会在拦截器中把当前线程隐藏参数传递到
RpcInvocation的attachment 中,服务端在拦截器中提取隐藏参数并设置到当前线程RpcContext中。

RpcContext.getContext( ).setAttachment( “index”,“1”);
string index = RpcContext.getContext().getAttachment(“index”);

9.5异步调用
2.7.0+版本才在服务端支持异步调用。在客户端实现异步调用非常简单,在消费接口时配置异步标识,在调用时从上下文中获取Future对象,在期望结果返回时再调用阻塞方法Future.get()即可。

Future fooFuture = RpcContext.getContext( ).getFuture();
在发起其他RPC调用时,先获取Future引用,当结果返回后,会被通知和设置到此Future
Foo foo = fooFuture.get();
如果foo已返回,则直接获取返回值,否则当前线程会被阻塞并等待

我们知道在客户端发起异步调用时,应该在保存当前调用的Future后,再发起其他远程调用,否则前一次异步调用的结果可能丢失(异步Future对象会被上下文覆盖)。
因为框架要明确知道用户意图,所以需要再明确开启使用异步特性,在<dubbo:reference . …>
标签中指定async标记,如代码清单9-9所示。
<dubbo:reference id=“fooService” interface=“com.alibaba.foo.FooService” async=“true”/>

9.6泛化调用
Dubbo泛化调用特性可以在不依赖服务接口API包的场景中发起远程调用。这种特性特别
适合框架集成和网关类应用开发。Dubbo在客户端发起泛化调用并不要求服务端是泛化暴露。

服务端在处理服务调用时,在GenericFilter拦截器中先把RpcInvocation中传递过来的参数类型和参数值提取出来,然后根据传递过来的接口名、方法名和参数类型查找服务端被调用的方法。获取真实方法后,主要提取真实方法参数类型(可能包含泛化类型),然后将参数值做Java类型转换。最后用解析后的参数值构造新的RpcInvocation对象发起调用。

9.7上下文信息
Dubbo上下文信息的获取和存储同样是基于JDK的ThreadLocal实现的。上下文中存放的
是当前调用过程中所需的环境信息。RpcContext是一个ThreadLocal的临时状态记录器,当收到或发送RPC时,当前线程关联的RpcContext状态都会变化。

比如:A调用B,B再调用C,则在B机器上,在B调用C之前,RpcContext记录的是A调用B的信息,在B调用C之后,RpcContext记录的是B调用C的信息。

boolean isProviderSide = RpcContext.getContext( ).isProviderSide();
本端是否为提供端,这里会返回true

string clientIP = RpcContext.getContext().getRemoteHost();
获取远程客户端IP地址

9.8Telnet 操作
目前Dubbo支持通过Telnet登录进行简单的运维,比如查看特定机器暴露了哪些服务、显
示服务端口连接列表、跟踪服务调用情况、调用本地服务和服务健康状况等。
本节我们主要讲解ls、ps、trace和count命令的实现和原理。
9.9Mock 调用
Dubbo 提供服务容错的能力,通常用于服务降级,比如验权服务,当服务提供方“挂掉”
后,客户端不抛出异常,而是通过Mock数据返回授权失败。
目前Dubbo提供以下几种方式来使用Mock能力:
(1) <dubbo:reference mock=“true” …/>。
(2) <dubbo:reference mock=“com.foo.BarServiceMock” …/>。
(3)<dubbo:reference mock=“return null” …/>。
(4)<dubbo:reference mock=“throw com.alibaba.XXXException” …/>。
(5)<dubbo:reference mock=“force:return fake” …/>。
(6)<dubbo:reference mock=“force:throw com.foo.MocKException” …/>。

当Dubbo服务提供者调用抛出RpcException时,框架会降级到本地Mock伪装
9.10结果缓存
Dubbo框架提供了对服务调用结果进行缓存的特性,用于加速热门数据的访问速度,Dubbo
提供声明式缓存,以减少用户加缓存的工作量。
如果要使用缓存,则可以在消费方添加如下配置:
<dubbo:reference cache=“lru” …/>

lru缓存策略是框架默认使用的,因此我们会对它进行简单的说明。它的原理比较简单,缓
存对应实现类是LRUCache。缓存实现类LRUCache继承了JDK的 LinkedHashMap类,
LinkedHashMap是基于链表的实现,它提供了钩子方法removeEldestEntry,它的返回值用于判断每次向集合中添加元素时是否应该删除最少访问的元素。LRUCache重写了这个方法,当缓存值达到1000时,这个方法会返回true,链表会把头部节点移除。链表每次添加数据时都会在队列尾部添加,因此队列头部就是最少访问的数据(LinkedHashMap在更新数据时,会把更新数据更新到列表尾部)。
9.11小结
本章主要对Dubbo 中的高级特性进行讲解,比如服务分组和版本、参数回调、隐式参数、
异步调用、泛化调用、上下文信息、Telnet操作、Mock调用和结果缓存原理。虽然本章的知识点比较独立,但这些特性点能够解决实际业务场景中的很多问题。比如版本和分组能够解决业务资源隔离,防止整体资源被个别调用方拖垮,可以将某些调用分配一个隔离的资源池中,单独为它们提供服务。
第10章Dubbo过滤器
本章主要内容:
Dubbo 过滤器概述;
过滤器链初始化的实现原理;
服务提供者过滤器的实现;
消费者过滤器的实现。
10.1Dubbo过滤器概述
10.1.1过滤器的使用
我们知道Dubbo中已经有很多内置的过滤器,并且大多数都是默认启用的,如ContextFilter。
对于自行扩展的过滤器,要如何启用呢?一种方式是使用@Activate注解默认启用;另一种方
式是在配置文件中配置,下面是官方文档中的配置,如代码清单10-1所示。

<dubbo: consumer filter=“xxx, yyy” />

<dubbo:service filter=“xXx,yyy”/>
!–服务提供方调用过程默认拦截器,将拦截所有service -->

以上就是常见的配置方式,下面我们来了解一下配置上的一些“潜规则”:
1)过滤器顺序。
用户自定义的过滤器的顺序默认会在框架内置过滤器之后,我们可以使用filter=“xxx,default"这种配置方式让自定义的过滤器顺序靠前。写在前面的xxx会比yyy的顺序要靠前。
(2)剔除过滤器。对于一些默认的过滤器或自动激活的过滤器,有些方法不想使用这些过
滤器,则可以使用“-”加过滤器名称来过滤,如filter=”-xFilter"会让 xxFilter不生效。如
果不想使用所有默认启用的过滤器,则可以配置filter="-default"来进行剔除。
(3)过滤器的叠加。如果服务提供者、消费者端都配置了过滤器,则两边的过滤器不会互
相覆盖,而是互相叠加,都会生效。如果需要覆盖,则可以在消费方使用“-”的方式剔除对应的过滤器。
10.1.2过滤器的总体结构

其他的内置过滤器都使用了@Activate注解,即默认被激活。Filter接口上有@SPI注解,说明过滤器是一个扩展点,用户可以基于这个扩展点接口实现自己的过滤器。
所有的过滤器会被分为消费者和服务提供者两种类型,

每个过滤器的使用方不一样,有的是服务提供者使用,有的是消费者使用。Dubbo是如何
保证服务提供者不会使用消费者的过滤器的呢?答案就在@Activate注解上,该注解可以设置
过滤器激活的条件和顺序,如@Activate(group = Constants.PROVIDER,order = -110000)表
示在服务提供端扩展点实现才有效,并且过滤器的顺序是-110000。
10.2过滤器链初始化的实现原理
使用过Filter 的读者都知道,所有的Filter会连接成一个过滤器链,每个请求都会经过整个
链路中的每一个Filter。那么这个过滤器链在Dubbo框架中是如何组装起来的呢?

我们在前面的章节已经了解过,服务的暴露与引用会使用Protocol层,而ProtocolFilterwrapper包装类则实现了过滤器链的组装。在服务的暴露与引用过程中,会使用ProtocolFilterWrapper#buildInvokerChain方法组装整个过滤器链,

然后我们来关注一下buildInvokerChain方法是如何构造调用链的,总的来说可以分为两步:
(1)获取并遍历所有过滤器。通过ExtensionLoader#getActivateExtension方法获取所
有的过滤器并遍历。
(2)使用装饰器模式,增强原有Invoker,组装过滤器链。使用装饰器模式,像俄罗斯套
娃一样,把过滤器一个又一个地“套”到Invoker 上。

源码中为什么要倒排遍历呢?因为是通过从里到外构造匿名类的方式构造Invoker的,所以
只有倒排,最外层的Invoker才能是第一个过滤器。
10.3服务提供者过滤器的实现原理
在表10-1中,@Activate注解上可以设置group属性,从而设定某些过滤器只有在服务提
供者端才生效。本章将详细介绍每一个在服务提供者端生效的过滤器,共8个。服务提供者端的过滤器数量明显比消费者端多。
10.4消费者过滤器的实现原理
10.5小结
之前的章节已经涉及很多过滤器的讲解,因此本章只介绍了一些前面章节都没有涉及的过
滤器。首先介绍了Dubbo框架中所有过滤器的总体大图,讲解了每个过滤器的作用及归属,过滤器分为服务提供者端生效和消费者端生效两种,其中服务提供者端有11种过滤器,消费者端有5种过滤器。有一个特殊的Monitor过滤器在两端都会生效,还有一个CompatibleFilter过滤器并没有默认启用。然后,我们介绍了整个过滤器链串联起来的原理,框架在ProtocolFilterWrapper中为每个Invoker包上了一层又一层的过滤器,最终形成一个过滤器链。我们分别详细介绍了服务提供者、消费者端的每个过滤器的实现原理。
第11章Dubbo注册中心扩展实践
第12章Dubbo服务治理平台
本章主要内容:
.服务搜索;
.路由规则;
动态配置;
访问控制;
权重管理;
.负载均衡。
本章主要介绍Dubbo最新的服务治理平台的实现原理。通过学习本章的内容,读者可以自
行对服务治理平台进行扩展,以满足自身不同的业务场景。首先介绍整个服务治理平台的大体框架。然后讲解最基础的服务搜索是如何实现的。最后详细介绍服务治理中的路由规则、动态配置、访问控制、权重管理、负载均衡的实现原理。
12.1服务治理平台总体结构
12.2服务治理平台的实现原理

  1. 服务搜索的实现

2.override特性的实现
override特性主要使用在动态参数的更新上,各个节点监听到注册中心的参数发生变化,从
而更新本地的参数信息。override类型的URL是以override://开头的,允许整个URL中只有
部分属性变化,监听者监听到变化后会做部分更新。

3.route 的实现
route规则可以为不同的服务指定特定的路由规则,route协议在注册中心的 URL以
route://开头。

4.LoadBalance的实现
如果用户不做任何配置,则默认使用RandomLoadBalance,即加权随机负载算法。用户可
以在服务治理平台里修改某个服务的负载均衡策略,其配置参数较少,

5.Weight的实现
当用户对服务设置了权重后,对权重高的节点会提高调用频率,对权重低的节点会
降低调用频率。
12.3小结
本章内容较少,由于Dubbo的服务治理平台一直处于半成品状态,实现的方式也比较简单,
因此可以讲解的原理不多。总的来说,Dubbo服务治理平台各种功能的实现,都是通过
RegistryServerSync工具类把注册中心的数据缓存到本地,然后通过override协议更新到注册中心,订阅者得知URL变更后,自动更新本地的配置缓存,从而实现配置的下发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值