前言
上次我们说了,Dubbo 主要用于构建分布式服务架构。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源Java RPC框架,这篇文章就细说如何使用 Dubbo 进行 RPC 远程调用
别忘了,Dubbo 是提供了地址发现、负载均衡、流量管控等治理能力,上次我们说了Dubbo的架构如下
- 服务提供者:实现业务逻辑的模块,将业务接口暴露为RPC服务,供其他服务调用
- 服务消费者:调用远程服务的模块
- 注册中心
- 负载均衡器
- 监控中心
架构上我们抽象成了两层,代表了一个微服务框架必须具有的基本功能:
- 服务治理层:服务治理控制面不是特指如注册中心类的单个具体组件,而是对 Dubbo 治理体系的抽象表达。控制面包含协调服务发现的注册中心、流量管控策略、Dubbo Admin 控制台等。
- 服务调用层:实际上,这是一整个数据面,数据面代表集群部署的所有 Dubbo 进程,进程之间通过 RPC 协议实现数据交换,Dubbo 定义了微服务应用开发与调用规范并负责完成数据传输的编解码工作。
所以说今天回到 Dubbo 最根本的功能,通过定义接口的方式实现远程方法调用,使得开发者无需关心底层网络通信的细节。
Dubbo的RPC通信协议
通信协议就是对通信传输的数据进行定义,表示每一位的含义是什么
Dubbo 框架是不和任何通信协议绑定的,对通信协议的支持非常灵活,支持任意的第三方协议,因为使用的是整洁架构,而且还高度可插拔
对于 RPC,简单提一下,RPC(远程过程调用)是一种网络编程技术,允许程序调用远程计算机上的函数或过程,而无需了解底层网络通信的细节。RPC使得远程调用像本地调用一样简单
需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程,RPC是一个泛化的概念,严格来说一切远程过程调用手段都属于RPC范畴。
Dubbo 协议
基于 TCP 的 Dubbo2 协议 和 基于 HTTP/2 的 Triple 协议 是 Dubbo 对于 RPC 实现提供的两个核心协议
这里再说一下 Dubbo 中使用的网络通信框架有以下三个:Netty、Mina、Grizzly
Dubbo2.x 中默认采用 Dubbo 协议,Dubbo 协议是采用 单一长连接 和 NIO 异步通讯,因此适合小数据量高并发的服务调用,不适合传送大数据量的服务,一般用于 纯 Java 微服务内部通信
Dubbo 协议是 基于 TCP 传输层协议的 RPC 通信协议,基于的是 TCP 长连接,目的就是为了简化 RPC 调用的复杂性,提高通信效率,但是由于单一长连接,网络带宽在传输大数据包的时候往往会成为瓶颈。
Dubbo 协议的组成如下
优点很明显,在点对点、高并发、小数据包场景下,它的吞吐量和延迟表现优异,而且直接使用 Java 接口定义服务,使用方便
缺点也很明显,但是由于它是私有二进制协议,非 Dubbo 客户端无法直接调用,而且不支持流式通信,仅支持传统的请求响应模型
然后再说一下为什么消费者要比提供者的个数多?
- 因 dubbo 协议采用单一长连接,根据测试经验数据每条连接最多只能压满 7MByte,理论上1个服务提供者需要十八十九个服务消费者才能压满网卡
- 而且单一长连接的特点也决定了这一特点,因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步IO,复用线程池,防止C10K问题。
使用它只需要在服务提供者和服务消费者分别配置 Dubbo 协议即可
1 | dubbo: |
而且在 Dubbo 中,服务接口的定义使用 Java 接口定义服务,供服务提供者实现,消费者通过引用调用。
1 | public interface DemoService { |
然后服务提供方实现接口,注意需要配置注册中心,因为在服务器启动后,Dubbo 会自动注册到注册中心
1 | // 可选:版本/分组控制 |
有人说,消费者无需指定协议,用什么协议由 服务提供者决定,消费者自动适配。这是 RPC 的基本设计原则。然后服务消费者再用到的地方直接引用就可以了,我习惯是都自己指定一下
1 |
|
对了,Dubbo 3 默认序列化仍为
hessian2,所以说直接启动就能使用了
gRPC
gRPC 是 Google 开源的通用高性能 RPC 框架,而且被广泛的认为是云原生的事实标准
而且 gRPC 也使用 protobuf 作为序列化协议,所以也能够支持多种语言之间的通信,并且具有非常低的延迟和高吞吐量。这是因为,gRPC 是基于 HTTP/2 协议构建的,支持流式传输、压缩、请求/响应多路复用等。
而且 gRPC 支持双向流,可以在客户端和服务器之间建立流式连接,适用于需要双向实时通信的场景。
使用 gRPC 非常简单,但是需要引入 dubbo-gRPC
模块的依赖
1 | # 服务提供方 |
但是注意服务提供方和服务调用方要使用一样的协议,要不然就会出现鸡同鸭讲的情况
那么服务接口的编写和使用 Dubbo 协议是一样的,定义服务接口,并用 Java 实现它。这个接口会通过 gRPC 暴露给调用方
1 | // DemoService.java |
服务提供方需要实现该接口
1 | // DemoServiceImpl.java |
如果服务消费者需要使用这个接口,消费者端需要使用
@DubboReference 注解来引用远程服务
1 | // DemoServiceConsumer.java |
但是,gRPC 依赖于 Protocol Buffers 来进行数据的序列化和反序列化。所以,除了 Java 代码外,实际的服务接口需要通过 Protobuf 定义文件(.proto)来生成相应的 Java 类。这个等演示的时候再细说了
Triple 协议
因此 Dubbo 框架为了提升协议的通用性,可以和 Spring Cloud 生态以及其他语言应用进行通信,在 Dubbo3.x 版本推出了基于 HTTP/2 的 Triple 协议,所以说 Triple 协议是 Dubbo 3 专为性能和易用性优化的一种协议。
Triple 协议使用 HTTP/2 ,HTTP 有向下兼容能力,所以 HTTP/2 也能兼容 HTTP/1,并且性能更好,在 兼容性和 性能 上都有所提升
与 gRPC 类似,Triple 协议也是基于 Protobuf 进行序列化的,但它对传统的 Dubbo 协议进行了优化,特别是在传输效率和数据编码方面。因此它 100% 兼容 gRPC,Triple 协议最重要的内容就是原生支持 Protobuf,在多语言上非常泛用,而且跟 gRPC 一样,Triple 协议也支持双向流通信,适用于需要长时间连接的场景
Triple 最重要的是高性能,它的使用也是支撑 dubbo3 框架的高性能的原因。Triple 协议通过优化消息的编码和解码过程,提高了性能,特别是对于大规模、高频次调用的场景。它在 Dubbo 3 的实现中比 gRPC 更为高效,具有更低的延迟和更高的吞吐量。
Triple 是 Dubbo 对 gRPC 协议的增强实现:
- 完全兼容 gRPC 客户端/服务端
- 在 gRPC 基础上增加了 Dubbo 特有的服务治理能力
Triple 协议是 Dubbo 3 默认支持的协议之一,因此无需额外的配置,就可以直接使用。
1 | # 服务暴露 |
具体如何实践,下面再说
Thrift 协议
在 Dubbo 3.x 中,Thrift 协议 是作为可选扩展协议提供的,它允许你将基于 Apache Thrift 的 RPC 服务无缝集成到 Dubbo 的服务治理体系中
因为 Thrift 本身是一个独立的跨语言 RPC 框架,但 Dubbo 通过协议扩展机制,将其包装为 Dubbo 支持的一种通信方式。
- Dubbo 的 Thrift 协议 不是直接调用原生 Thrift 服务,而是 在 Dubbo 框架内嵌入 Thrift 编解码器,通过 Dubbo 的 SPI 机制实现,中间可能会出现一些 Dubbo 自己的处理,所以,与原生 Thrift 客户端不兼容
Thrift 使用 .thrift
文件定义接口(IDL),这是跨语言的基础。
1 | // demo.thrift |
然后使用 Thrift 编译器生成 Java Stub:
该文件包含:
DemoService.Iface:服务接口(需实现)DemoService.Client:客户端调用类DemoService.Processor:服务端处理器
将生成的 Java 文件复制到你的项目 src/main/java 中。
而且一般情况下,需要添加 Thrift 依赖
然后指定配置文件使用 Thtift 协议
1 | dubbo: |
- Thrift 协议在 Dubbo 中采用 单端口多服务复用 模式,即所有 Thrift 服务共享同一个端口
由于 Thrift 服务不是标准 Java 接口,不能直接使用
@DubboService。需要通过 XML 配置 或 API 编程方式
注册服务。所以说,很少有人使用这个协议
多协议配置
Dubbo 允许配置多协议,在不同服务上支持不同协议或者同一服务上同时支持多种协议。
不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议,这才是一种比较好的解决方案
Dubbo的序列化
Dubbo 中常见的序列化方式有这些
| 序列化方式 | 备注 |
|---|---|
| Hessian2 | Dubbo协议中默认的序列化实现方案。 |
| Java Serialization | JDK的序列化方式。 |
| Dubbo序列化 | 阿里的高效Java序列化实现,但是Dubbo目前不建议我们在生产环境使用它。 |
| Json序列化 | 目前有两种实现:一种是采用阿里的fastjson库;一种是采用Dubbo中自己实现的简单json库。 |
| Kryo | Java序列化方式,后续替换Hessian2,是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛使用。 |
| FST | Java序列化方式,后续替换Hessian2,是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例。 |
| 跨语言序列化方式 | ProtoBuf,Thirft,Avro,MsgPack(它更快更小。短整型被编码成一个字节)。 |
由于 JSON 大家都很熟悉,跨语言说一个 ProtoBuf 就可以了,再说一个默认的够了
而且一般说序列化就是序列化和反序列化一起带上的
序列化就是把「内存中的对象结构」转换为「可传输 / 可存储的字节序列」的过程,而反过来把「字节序列」再恢复为对象的过程叫 反序列化
那么,RPC 的传输过程中的一定涉及到序列化的,原因很明显
在 Dubbo / RPC 场景中:
- 服务提供者和消费者:
- 不在同一个 JVM
- 甚至不在同一台机器
- Java 对象不能跨进程直接传递
- 网络只能传
byte[]
因此,方法参数,返回值,异常什么的,这些都要序列化了
所以说,序列化是 Dubbo 协议层和传输层之间的关键组件
Hessian2
Hessian 是一种 二进制序列化协议,最初由 Caucho 提出。
Hessian2 是阿里在 Hessian 基础上进行的二次开发,起名为Hessian2,Dubbo < 3.2.0 版本中,默认使用 Hessian2 作为默认序列化
特点总结一句话:
“为 Java RPC 场景特制优化的二进制对象序列化”
Hessian2 的工作方式基于反射,而且是按字段序列化对象,而且通常情况下,不需要配置什么,不需要额外 IDL,不需要生成代码,在 Java 这块,使用起来成本很低了,而且兼容性还挺好
但是 Hessian2 的缺点比较致命,就是强依赖 Java 类结构,几乎不能跨语言,而且存在过反序列化攻击的历史问题,所以它只是个 Java-to-Java RPC 的默认实用型方案
使用它只需要在配置文件指定一下就可以
1 | # application.yml (Spring Boot) |
当然,在 Triple 协议中也可使用 Hessian2(非 Protobuf),便于 Java 用户平滑迁移
Protobuf
Protobuf(Protocol Buffers)是 Google 推出的,基于 IDL 的二进制序列化协议
核心思想是:先定义结构,也就是.proto文件,然后根据插件什么的生成不同语言需要的代码,然后再使用进行传输数据
Protobuf 的优点很明显,它有极强兼容性,而且天生跨语言,性能也很好
但是,使用它,开发成本一下就高了,对 Java 开发者不如 Hessian2 直观
使用它的方式先对比较复杂,这里只做简单演示,下面实例演示的时候再细说
首先要引入依赖
1 | <dependency> |
然后启用相关配置
1 | # application.yml (Spring Boot) |
Dubbo 进行 RPC 调用的机制
调用流程
首先,Dubbo3 是应用级服务发现 + 接口级发现 并存,但无论哪种模式,RPC 调用的核心执行流程基本一致。
核心角色:
- Provider:服务提供者
- Consumer:服务消费者
- Registry:注册中心(ZooKeeper/Nacos)
- Protocol:通信协议(dubbo/triple)
- Proxy:动态代理
- Cluster:集群容错与负载均衡
- Filter:调用拦截器
- Exchange/Transport:网络通信层
那么,Dubbo 3.x RPC 调用全流程 如下
- 服务提供者启动,进行服务暴露
- Spring 容器启动,扫描
@DubboService注解。 - 创建 ServiceConfig 对象,封装接口、实现类、版本、分组等元数据。
- 协议层暴露
Protocol#export():- 根据
dubbo.protocol.name(如dubbo或tri)选择协议实现。 - 开启 Netty Server(默认端口 20880 / 50051),监听 TCP 连接。
- 根据
- 注册到注册中心:
- 创建 ZooKeeper/Nacos 客户端。将 服务元数据(URL 形式)写入注册中心,一般还会注册应用维度的 metadata
- Spring 容器启动,扫描
- 服务消费者启动,进行服务引用
- Spring 容器启动,处理
@DubboReference注解。 - 创建 ReferenceConfig,解析接口、版本、超时等配置。
- 从注册中心订阅服务:
- 连接 ZooKeeper/Nacos,监听 Provider 列表变更
- 获取当前所有可用 Provider 地址列表
- 建立长连接
- 对每个 Provider 地址,创建 NettyClient。
- 与 Provider 的 NettyServer 建立 TCP 长连接(复用连接池)。
- 连接信息缓存在本地(
Directory中)。
- 此时,Consumer 持有可用 Provider 列表及连接,可随时发起调用。
- Spring 容器启动,处理
- 消费者发起调用 —— 动态代理生成
- 用户代码调用:
- ProxyFactory 创建代理对象
- Dubbo 默认使用 Javassist(高性能字节码生成)创建代理类。
- 代理类实现了 对应调用的远程服务的 接口。
- 代理方法调用
- 所有方法调用被转发到
InvokerInvocationHandler#invoke()。整套调用流程对用户来说都是无感的,这就是 RPC 协议的根本
- 所有方法调用被转发到
- 调用链构建 —— Invoker 体系
- 调用进入 Dubbo 内部后,会经过多层包装:
- ClusterInvoker(集群层):
- 负责 负载均衡(Random/LeastActive/RoundRobin)。
- 实现 容错策略(Failover/Failfast/Failsafe)。
- 从多个 Provider 中选择一个目标。
- Filter 链(过滤器链):
- 类似 Servlet Filter,按顺序执行。
- 默认包含:
MonitorFilter(监控)、TimeoutFilter、ExceptionFilter等。 - 可自定义扩展(如日志、鉴权、限流)。
- Protocol 层(协议层):
- 创建 DubboInvoker(针对 dubbo 协议)或 TripleInvoker。
- 封装目标地址、序列化方式、超时时间等。
- ClusterInvoker(集群层):
- 在这一步服务调用方就明确了,调哪个 Provider?用什么协议?走哪些 Filter?
- 调用进入 Dubbo 内部后,会经过多层包装:
- 网络传输 —— 请求发送
- 参数封装
- 方法名、参数类型、参数值 → 封装为
RpcInvocation。
- 方法名、参数类型、参数值 → 封装为
- 序列化
- 使用配置的序列化器(如
hessian2、fastjson2)将对象转为二进制。
- 使用配置的序列化器(如
- 构建 Request
- 生成唯一
requestId(用于异步匹配响应)。 - 添加协议头(Magic Number、Serialization ID、Event Type 等)。
- 生成唯一
- Netty 发送
- 通过已建立的 NettyClient Channel 异步发送字节流。
- 同时注册
DefaultFuture,用于后续阻塞等待结果(同步调用)。
- 此时数据已经通过 TCP 发送到 Provider。
- 参数封装
- 服务端接收与处理
- NettyServer 接收:
NettyServerHandler处理入站数据。
- 反序列化:
- 根据协议头识别序列化方式,反序列化出
RpcInvocation。
- 根据协议头识别序列化方式,反序列化出
- 查找 Exporter:
- 根据接口名、版本、分组 → 匹配对应的
DubboExporter。
- 根据接口名、版本、分组 → 匹配对应的
- 构建服务端 Invoker:
- 调用链:
ProtocolFilterWrapper → ListenerInvokerWrapper → AbstractProxyInvoker
- 调用链:
- 反射调用本地方法:
- 通过
ref.invoke(method, args)执行真正的业务逻辑(如DemoServiceImpl.sayHello())。
- 通过
- 此时服务端完成业务处理,准备返回。
- NettyServer 接收:
- 响应返回与结果解析
- 结果封装:
- 返回值或异常 → 封装为
AppResponse。
- 返回值或异常 → 封装为
- 序列化 & 发送:
- 同样序列化为二进制,通过原 Netty Channel 返回。
- 响应中包含相同的
requestId。
- 客户端接收:
- NettyClient 收到响应,根据
requestId唤醒对应的DefaultFuture。
- NettyClient 收到响应,根据
- 结果解析:
- 反序列化
AppResponse。 - 若是异常,则抛出;否则返回正常结果。
- 反序列化
- 同步调用返回:
- 被调用的远程接口最后返回服务消费者期望的结果,然后,用户拿到结果,一次 RPC 调用完成。
- 结果封装:
源码懒得看了,因为关键的内容还是不少的,我有很多前置的内容需要等待掌握的更清楚
服务暴露
注意,我这里说的服务暴露是将本地 Java 对象封装为可远程调用的网络服务,一般是Provider服务提供者需要做这件事
而服务注册是将服务的网络地址信息写入注册中心,也就是向 ZooKeeper/Nacos 写入 URL,这两步不太一样,请注意区分,当然,我这里肯定也混和着服务注册一起说了
服务暴露(Service Export) 与 服务引用(Service Refer) 是 Dubbo 框架最核心的两个机制,分别对应 服务提供者(Provider) 和 服务消费者(Consumer) 的启动流程。
Dubbo 提供的是一种 Client-Based 的服务发现机制,所以说,我之前一直提 Dubbo 不会自己实现注册中心,他只是提供这样的一个位置,依赖第三方注册中心组件来协调服务发现过程
以下是 Dubbo 服务发现机制的基本工作原理图:
服务发现包含提供者、消费者和注册中心三个参与角色,其中,Dubbo 提供者实例注册 URL 地址到注册中心,注册中心负责对数据进行聚合,Dubbo 消费者从注册中心读取地址列表并订阅变更,每当地址列表发生变化,注册中心将最新的列表通知到所有订阅的消费者实例
那么,服务发现中,对于 远程服务暴露,Dubbo 会做如下内容
首先,在当 Spring 容器刷新完成时,会调用
ServiceConfig.export(),它会:加载注册中心地址(如
nacos://127.0.0.1:8848)遍历所有协议(如
dubbo,tri)为每个协议生成 Provider URL:
注意,dubbo 服务的注册发现是以接口为最小粒度的,在 dubbo 中将其抽象为一个URL,大概长这样:
然后进入
RegistryProtocol.export(),这是 远程暴露的统一入口。再然后会进入协议层暴露,会开启 Netty Server,缓存远程接口,然后注册对应内容到注册中心
那么,如何编写相关内容,如何在 Dubbo
中进行服务发现,服务发现主要使用@DubboService注解来暴露服务
@DubboService注解标记在类上,表明这个类提供了一个 Dubbo 服务。它负责将服务接口的实现注册到注册中心,并处理服务请求。- 在服务提供方使用
@DubboService注解可以自动注册服务,无需手动编写注册代码。这样服务消费者可以通过注册中心发现并调用这些服务。
1 | // 定义服务接口 |
而且 Dubbo 注册中心以应用粒度聚合实例数据,消费者按消费需求精准订阅,避免了大多数开源框架如 Istio、Spring Cloud 等全量订阅带来的性能瓶颈。
区别于其他很多微服务框架的是,Dubbo3 的服务发现机制诞生于阿里巴巴超大规模微服务电商集群实践场景,因此,其在性能、可伸缩性、易用性等方面的表现大幅领先于业界大多数主流开源产品。是企业面向未来构建可伸缩的微服务集群的最佳选择。
这是人家文档自己说的))))
而且,除了与注册中心的交互,Dubbo3 的完整地址发现过程还有一条额外的元数据通路,称之为元数据服务 (MetadataService),实例地址与元数据共同组成了消费者端有效的地址列表。
- 首先,消费者从注册中心接收到地址 (ip:port) 信息
- 然后与提供者建立连接并通过元数据服务读取到对端的元数据配置信息
两部分信息共同组装成 Dubbo 消费端有效的面向服务的地址列表。而且以上两个步骤都是在实际的 RPC 服务调用发生之前。所以整体步骤如下
别忘了,元数据中存储的是服务实例中的接口信息,也就是接口元数据,所以,只有读到对应实例中的对应被暴露的远程接口,才能进行 RPC 调用,为了性能,Dubbo 才这样拆开的,因为这样,消费者只订阅 所需应用,按需从 Metadata Center 拉取远程服务接口的元数据。
服务引用
注意,服务引用也是,服务引用是在 Consumer 端生成远程服务的代理对象的完整过程,是为了让“远程服务像本地对象一样调用”,为达到 RPC 的目的,所做的一些事情
服务发现是从注册中心获取可用 Provider 地址列表的过程,是与注册中心交互的一个过程
服务引用的目的是让远程服务看起来像本地对象一样调用。
触发时机如下
- 懒加载(默认):当
@DubboReference注入的 Bean 被首次使用时。- 也就是,在 ReferenceBean 对应的服务被注入到其他类中时引用。
- 饿加载:设置
@DubboReference(init = true),在 Spring 启动时立即引用。- 也就是,在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务
然后,接着根据收集到的信息决定服务用的方式,有三种
- 引用本地 (JVM) 服务
- 通过直连方式引用远程服务
- 通过注册中心引用远程服务
但是不管是哪种引用方式,最后都会得到一个 Invoker 实例,这是服务能被引用的挂件,关键步骤如下
- 生成代理对象Invoker
ProxyFactory(默认 Javassist)创建DemoService的代理类。所有方法调用被转发到InvokerInvocationHandler.invoke()。也就是代理对象的一个反射- 这里也会涉及到服务发现的相关步骤,因为需要获取接口的远程 url,或者判断是不是本地调用
- Invoker 创建完毕后,接下来要做的事情是为服务接口生成代理对象。有了代理对象,即可进行远程调用。
- 接下来这里也会进行负载均衡,容灾和流量控制等相关服务治理的内容
对于上面说的代理对象 Invoker,它是 Dubbo 的核心模型,代表一个可执行体。
服务提供方,Invoker 用于调用服务提供类。在服务消费方,Invoker 用于执行远程调用。Invoker 是由 Protocol 实现类构建而来。
详细源码分析:https://www.cnblogs.com/jock766/p/18561200
emmmm,如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。注意,合并后的 Invoker 实例已经具备调用本地或远程服务的能力了,但并不能将此实例暴露给用户使用。。。。。
如何在 Dubbo3 中进行服务引用,也是一个注解的事情,对于一个服务提供者暴露的服务接口
@DubboReference注解用于注入远程服务代理,通过这个代理,服务消费者可以进行远程调用。- 服务消费方应用中使用
@DubboReference,可以方便地引用远程服务。这样,消费者可以透明地调用远程方法,而无需关心底层通信细节。
1 |
|
直接这样就可以,Dubbo3 会自动从注册中心获取 Provider 列表,然后负载均衡选择一个,再通过 Netty 发起远程调用
那都说了,顺手说了吧,服务发现很简单,仅对应
RegistryDirectory.subscribe() 这一步
主要是
- 向注册中心发送订阅请求(如 ZooKeeper 的
getChildrenWatches) - 首次拉取全量 Provider 列表
- 监听后续变更(Provider 上下线)
SPI 机制
Java 中的 SPI
在Dubbo 中,SPI 是一个非常重要的模块。基于SPI,我们可以很容易的对Dubbo 进行拓展。
SPI 是 Service Provider Interface 服务提供接口的缩写,是 Java 原生自带的一种服务发现机制
SPI 的本质是将接口的实现类的全限定名定义在配置文件中,并有服务器读取配置文件,并加载实现类。这样就可以在运行的时候,动态为接口替换实现类。
Java SPI 实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。
Dubbo 中的扩展机制与 JDK 标准的 SPI 扩展点 原理类似。Dubbo 对其做了一定的改造与加强
我们来编写一个 Java SPI 的例子
定义接口 Robot.java
1 | public interface Robot { |
编写两个实现
1 | public class OptimusPrime implements Robot { |
最后在 src/main/resources/META-INF/services/
目录下,创建一个名为上面定义的接口的全限定类名的文件,文件内容是每行一个实现类的全限定类名
最后写一个测试,可以看到,两个实现类被自动加载并执行。
那么,它是如何工作的
首先,
ServiceLoader.load(Robot.class)被调用,JVM 会在 classpath 下查找META-INF/services/hbnu.project.orderservice.spitest.Robot
然后,读取文件内容,得到实现类列表,通过反射(
Class.forName().newInstance())实例化每个类。最后,返回一个 Iterable 对象,供用户遍历。
而且这里使用了单例模式,是懒加载的情况,只有在
iterator().next()时才真正实例化对象。
那么,为什么 Dubbo 要重写这个机制?因为虽然 Java SPI 很有用,但它有严重缺陷:
| 缺点 | 说明 |
|---|---|
| 1. 无法按需加载 | 必须遍历所有实现类,即使你只需要其中一个(浪费资源) |
| 2. 无 IOC/AOP 支持 | 无法自动注入依赖,无法添加代理(如监控、日志) |
| 3. 获取特定实现困难 | 只能通过遍历 + instanceof 判断,不能通过名称直接获取 |
Dubbo中的SPI
Dubbo SPI 的相关逻辑被封装在了ExtensionLoader
类中,通过ExtensionLoader,我们可以加载指定的实现类。
而且与 Java SPI 实现类配置不同,Dubbo SPI
是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类,而且支持自适应扩展(@Adaptive)、条件激活(@Activate)
那么,还是上面的例子,我们进行改造
1 | import org.apache.dubbo.common.extension.SPI; |
实现类不用更改,然后,实现类声明文件需要改,在
src/main/resources/META-INF/dubbo/
目录下,创建文件,文件名就是接口全限定名文件内容是键值对模式,格式是key = full.class.Name
1 | optimusPrime = hbnu.project.orderservice.spitest.impl.OptimusPrime |
然后编写测试代码
1 | public class DubboSPITest { |
这个测试的那个项目找不到了,就放着了,我们接下来来说一下 Dubbo 中的动态编译
Dubbo 中的动态编译
实例完整的介绍 Dubbo 的 RPC 远程调用
扩展项目前要做的
先编写一些批量查询和分页会用到的内容
1 | // 包装结果DTO Result |
1 | // 分页DTO |
使用 Dubbo 默认的协议和序列化方式来进行远程调用
我们在现有基础上,新增一个方法,尽量体现真实的场景,展示:
- 批量 + 分页查询
- 返回包装结果(Result)
- 然后为了体现业务组合,在 OrderService 中调用它
我们扩展 UserService 接口
1 | List<UserDTO> listUsers(PageQuery query); |
然后在服务的提供方,实现这个接口
其中,使用注解暴露这个服务提供者,然后配置一些服务治理的内容
1 |
|
1 |
|
然后,在服务的消费者OrderController 中新增调用示例
1 |
|
其中,我们用到了
1
2
3
4
5
6
7
8
9
10
11// 使用 @DubboReference 注入远程服务
// 配置服务治理参数
private UserService userService;
但是,配置文件中,我们在服务提供者使用了 Triple 协议。但是Triple
默认期望 Protobuf,但因为你没有 .proto 文件,Dubbo 会自动
fallback 到 Hessian2
1 | protocol: |
如果想显式指定序列化方式,可以在 @DubboService 和
@DubboReference 中加:
1 | serialization = "hessian2" |
关于 @DubboService 和 @DubboReference
的完整参数
1 |
|
@DubboReference
参数和@DubboService差不太多
1 |
|
我们进行一下测试
RPC调用通了
使用 Dubbo 进行异步调用
我们基于现有的 UserService
接口和实现,新增一个异步方法,使用
CompletableFuture(Dubbo 3
推荐的异步编程模型),并展示在 OrderController
中如何调用它。
先定义接口
1 | /** |
注意:方法返回类型必须是
CompletableFuture<T>,这是 Dubbo 识别异步调用的关键。
然后在服务提供者那边实现这个接口
1 | import java.util.concurrent.CompletableFuture; |
- 这里我使用
CompletableFuture.supplyAsync()提交到独立线程执行,避免阻塞 Dubbo 的 I/O 线程
接下来在消费者这边新增异步调用接口
1 | /** |
userService.getUserByIdAsync()立即返回CompletableFuture,不阻塞 HTTP 线程,而且这里 HTTP 响应是立即返回的
来进行一下测试
可以看到,异步调用也跑通了
泛化调用
使用 Triple 协议进行多语言远程调用
所以说,我们需要使用 Protobuf 定义 IDL 接口契约,然后使用插件根据契约生成 Java / Go / Python 多语言代码,然后在现有 Dubbo 服务中 同时支持 Triple + Protobuf,这样就能实现 跨语言 RPC 调用
首先,在你的服务提供者的模块项目根目录下创建:
然后,编写 .proto 文件
那么,proto 文件如何编写呢?下面再说
1 | // proto/user_service.proto |
然后,我们需要为服务提供者的模块中配置 Maven 插件,用于自动生成代码
1 | <properties> |
这样,我们就能在对应的 maven 配置下看到有关 protobuf 的插件
这些都是支持的语言
那么,我们执行对应的代码生成命令之后,就会生成对应的代码
你执行执行 mvn compile 后,就会在
src/main/java/hbnu/project/dubbopublicapi/api/ 生成在 IDL
里对应的 Java 远程调用需要的 gRPC 代码
编写 proto 的 IDL 接口契约
首先,我们需要把如下 Java 声明的为远程服务调用提供服务的接口,进行分析
1 | public interface UserService { |
以及 DTO 类:
UserDTO: id, username, email, phone, addressPageQuery: page, size, keyword
然后开始着手编写 .proto 文件作为 IDL
首先是声明
1
2
3
4
5
6
7
8
9
10// 语法版本(必须是 proto3)
syntax = "proto3";
// 包名(对应 Java package,避免冲突)
package hbnu.project.dubbopublicapi.api;
// 3. Java 生成选项
option java_multiple_files = true; // 每个 message 生成独立 .java 文件
option java_package = "hbnu.project.dubbopublicapi.api"; // 生成的 Java 包路径
option java_outer_classname = "UserServiceProto"; // 外部类名(包含所有 descriptor)然后开始定义数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// UserDTO 对应你的 Java 类
message UserDTO {
int64 id = 1; // Long → int64
string username = 2; // ← 这个 "2" 是字段编号
string email = 3;
string phone = 4;
string address = 5;
}
// PageQuery 分页查询参数
message PageQuery {
int32 page = 1; // int → int32
int32 size = 2;
string keyword = 3;
}
// 用户列表响应(因为 gRPC 不能直接返回 List<T>)
message UserListResponse {
repeated UserDTO users = 1; // repeated = 列表
int64 total = 2; // 总数
}然后开始定义接口(定义服务service)
1
2
3
4
5
6
7
8
9
10
11
12
13service UserService {
// getUserById(Long userId) → 需要包装 Long
rpc GetUserById (Int64Value) returns (UserDTO);
// getUserByName(String username) → 需要包装 String
rpc GetUserByName (StringValue) returns (UserDTO);
// createUser(UserDTO userDTO) → 参数已是 message
rpc CreateUser (UserDTO) returns (UserDTO);
// listUsers(PageQuery query) → 参数已是 message
rpc ListUsers (PageQuery) returns (UserListResponse);
}- 其中,
- 每个方法必须是
rpc MethodName(Request) returns (Response); - 不能重载(方法名必须唯一)
- 不支持 void 返回(至少返回空 message)
- 每个方法必须是
- 其中,
对于上述使用了的简单类型包装,需要进行包装
1
2
3
4
5
6
7
8
9// 包装 int64(用于 userId)
message Int64Value {
int64 value = 1;
}
// 包装 string(用于 username)
message StringValue {
string value = 1;
}其中,为什么需要包装简单类型?因为gRPC 规定:每个 RPC 方法只能有一个参数 message。 所以:
Java 类型 Protobuf 类型 说明 int/Integerint3232位整数 long/Longint6464位整数 StringstringUTF-8 字符串 booleanbool布尔值 List<T>repeated T列表(可重复字段) floatfloat32位浮点 doubledouble64位浮点





