从 gRPC 到 Protobuf、Dubbo Triple:Spring Boot 微服务通信深度实践

欢迎你来读这篇博客,这篇博客主要围绕 gRPCProtobufDubbo 以及它们与 Spring Boot 的整合展开。

它不是一篇“背概念”的文章,而是一篇从通信演进、协议原理、工程落地、Spring Boot 集成、Dubbo Triple 兼容、服务发现、安全认证、异常治理、调试工具到生产最佳实践的完整长文。

序言

在微服务架构里,服务之间总要通信。通信方式可以很简单:一个 HTTP 接口,一个 JSON 请求,一个 JSON 响应。也可以很工程化:IDL 定义接口,编译生成强类型代码,通过 HTTP/2 进行二进制传输,再配合注册中心、负载均衡、超时、重试、鉴权、链路追踪和服务治理。

gRPC + Protobuf 就是后一种思路的典型代表。它强调“契约优先”和“强类型通信”;Dubbo 则更偏向 Java 微服务治理框架,除了 RPC 调用本身,还内置了注册发现、负载均衡、路由、治理、观测等能力。到了 Dubbo 3,Triple 协议又把 Dubbo 和 gRPC 生态进一步拉近:既可以保持 Dubbo 的治理能力,又可以兼容标准 gRPC。

这篇文章会拆成几个相对独立的部分:

  1. RPC、REST、gRPC 的演进逻辑。
  2. Protobuf 的语法、代码生成和接口演进规则。
  3. gRPC 的底层原理、通信模型和 Java 编程模型。
  4. 原生 grpc-java 项目实战。
  5. Spring Boot 集成 gRPC 实战。
  6. gRPC 接入注册中心、鉴权、异常处理和生产治理。
  7. Dubbo、Triple、gRPC 的关系与 Spring Boot 整合。
  8. 最后的选型建议和避坑清单。

文章比较长。如果你只是想快速落地,可以先看“Spring Boot 集成 gRPC 实战”和“Dubbo 与 Spring Boot 整合”两章;如果你想把这套东西吃透,那就从头看。别急,RPC 这玩意儿表面是“远程调用”,本质是“把网络复杂度藏起来又不能真的忘掉它”。这也是它有趣的地方。


一、进程间通信的演进:从 RMI、SOAP、REST 到 gRPC

1.1 什么是进程间通信

进程间通信,放在微服务语境里,通常指一个应用实例调用另一个应用实例提供的服务。两个服务运行在不同进程里,甚至不同机器、不同机房、不同语言栈里,中间通过网络交换信息。

最简单的理解是:

flowchart LR
    A[订单服务 Order Service] -->|调用| B[库存服务 Stock Service]
    B -->|返回库存结果| A

如果都在一个 JVM 里,一个方法调用就是:

1
stockService.deduct(skuId, quantity);

但如果 stockService 在另一台机器上,这个调用会变成:

  1. 把请求参数序列化。
  2. 通过网络发送。
  3. 服务端反序列化。
  4. 执行业务逻辑。
  5. 把结果序列化。
  6. 网络返回。
  7. 客户端反序列化。
  8. 像本地方法一样拿到结果。

RPC 框架就是为了把这套脏活累活包装起来。听起来很美好,实际工程里该踩的坑一个都不会少,只是坑的名字更高级了。

1.2 传统 RPC:Java RMI

早期 Java 生态里有 RMI,也就是 Remote Method Invocation。它的优点是 Java 程序员上手快,调用远程对象看起来像调用本地对象。

但 RMI 的问题也很明显:

问题 说明
语言绑定严重 主要面向 Java 到 Java,跨语言能力弱。
传输与序列化不够现代 很难和今天的多语言微服务生态融合。
编程模型复杂 远程对象、Stub、Skeleton、序列化异常等概念较重。
互联网开放性不足 不适合作为开放 API 的主流方案。

RMI 在今天更多是历史教材,真正生产里大概率不会作为新系统首选。

1.3 SOAP 与 WebService

SOAP 是早期 WebService 的重要技术路线,通常基于 HTTP 传输 XML 格式的数据包。

它的优势是标准化程度高,早期企业系统、政企系统、异构系统集成里用得很多。问题也很现实:XML 重、规范多、开发体验笨重,写起来像穿西装拧螺丝,正式但难受。

1.4 RESTful API

RESTful API 是过去很多年里最常见的服务通信方式。通常基于 HTTP,使用 URI 表达资源,用 HTTP Method 表达操作,用 JSON 表达数据。

例如:

1
2
3
4
GET /api/news/1001
POST /api/news
PUT /api/news/1001
DELETE /api/news/1001

REST 的优点:

优点 说明
简单通用 浏览器、网关、代理、调试工具都天然支持。
对外友好 很适合开放 API、前后端交互。
学习成本低 HTTP + JSON 几乎是开发者通用语言。
生态成熟 网关、鉴权、限流、缓存、监控工具非常丰富。

REST 的问题:

问题 说明
文本协议开销较大 JSON 可读性好,但体积和解析性能不如二进制协议。
缺少强类型契约 字段名写错、类型变化、兼容性问题常常运行时才暴露。
REST 风格难统一 很多项目最后写成了“伪 REST + 动词 URL”。
流式通信能力弱 虽然可以用 SSE、WebSocket、Chunked,但不是 REST 的天然优势。
跨语言代码生成不统一 OpenAPI 能解决一部分,但体验和强约束不如 IDL-first RPC。

1.5 gRPC 的出现

gRPC 是 Google 开源的现代 RPC 框架。它默认使用 Protocol Buffers 作为 IDL 和消息序列化格式,底层基于 HTTP/2,支持多语言、强类型接口、双向流、认证、超时、取消、状态码、拦截器等能力。

一句话概括:

REST 更像“围绕资源的 HTTP API 风格”;gRPC 更像“围绕服务方法的强类型远程调用框架”。

gRPC 的典型调用过程:

sequenceDiagram
    participant C as Client App
    participant S as Client Stub
    participant H as HTTP/2 Channel
    participant G as gRPC Server
    participant I as Service Impl

    C->>S: 调用 getNews(request)
    S->>S: Protobuf 序列化
    S->>H: HTTP/2 POST /news.v1.NewsService/GetNews
    H->>G: 发送 Headers + Message
    G->>I: 反序列化并调用业务实现
    I-->>G: 返回 response
    G-->>H: Headers + Message + Trailers
    H-->>S: 返回二进制响应
    S-->>C: Protobuf 反序列化后的对象

二、gRPC 适合什么,不适合什么

2.1 gRPC 的核心优势

能力 说明
强类型接口 通过 .proto 定义服务和消息,编译生成客户端和服务端代码。
多语言支持 Java、Go、Python、Node、C++、C#、Kotlin、Rust 等语言都有生态。
性能较好 Protobuf 二进制编码通常比 JSON 更紧凑,HTTP/2 也支持多路复用。
支持四种通信模式 一元、服务端流、客户端流、双向流。
适合内部微服务 服务之间调用频繁、接口稳定、性能敏感时非常合适。
支持拦截器 适合做鉴权、日志、链路追踪、指标、灰度标记。
支持 Deadline/Cancel 对分布式系统里的资源释放非常关键。

2.2 gRPC 的不足

不足 说明
浏览器直接调用不如 REST 方便 浏览器原生对 gRPC HTTP/2 Trailers 支持有限,通常需要 gRPC-Web 或网关。
可读性不如 JSON 抓包看到的是二进制,需要 grpcurl、Postman、Insomnia、反射等工具。
接口变更需要管理 .proto 是契约,字段编号不能乱改,版本治理很重要。
对团队规范要求更高 不规范地写 proto,比不规范地写 REST 更难救。
生态对外暴露弱一些 对开放平台、第三方接入,REST/OpenAPI 仍然更友好。

2.3 什么时候用 REST,什么时候用 gRPC

场景 推荐
前端调用后端 REST / GraphQL / BFF,一般不直接用原生 gRPC。
内部 Java/Go/Python 服务互调 gRPC 很合适。
移动端高频调用 gRPC 可以考虑,但要处理网关、兼容性、弱网和 SDK 维护。
大量实时推送、长连接数据流 gRPC Streaming 很合适。
开放平台 API REST + OpenAPI 更稳。
Java 微服务且需要完整服务治理 Dubbo / Dubbo Triple 很合适。
既想要 Dubbo 治理,又想兼容 gRPC Dubbo Triple。

三、Protobuf:gRPC 的接口契约与数据格式

3.1 Protobuf 是什么

Protocol Buffers,简称 Protobuf,是 Google 提供的语言无关、平台无关、可扩展的结构化数据序列化机制。

它的工作方式是:

  1. 先写 .proto 文件定义数据结构和服务接口。
  2. 使用 protoc 编译器生成目标语言代码。
  3. 业务代码使用生成的类构建对象、序列化、反序列化、调用 RPC。
flowchart TB
    A[news.proto] --> B[protoc 编译器]
    B --> C[消息类 NewsRequest / NewsResponse]
    B --> D[gRPC 服务类 NewsServiceGrpc]
    C --> E[客户端与服务端共用]
    D --> F[客户端 Stub]
    D --> G[服务端 ImplBase]

3.2 一个完整的 .proto 示例

下面用一个 NewsService 作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
syntax = "proto3";

package news.v1;

option java_package = "com.example.grpc.news";
option java_multiple_files = true;
option java_outer_classname = "NewsProto";

service NewsService {
// 一元 RPC:一个请求,一个响应
rpc GetNews(GetNewsRequest) returns (NewsResponse);

// 服务端流:一个请求,多个响应
rpc ListNews(ListNewsRequest) returns (stream NewsItem);

// 客户端流:多个请求,一个响应
rpc UploadNews(stream NewsItem) returns (UploadNewsResponse);

// 双向流:多个请求,多个响应
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message GetNewsRequest {
int64 id = 1;
}

message ListNewsRequest {
string category = 1;
int32 page_size = 2;
}

message NewsResponse {
NewsItem data = 1;
}

message NewsItem {
int64 id = 1;
string title = 2;
string content = 3;
string author = 4;
int64 publish_time = 5;
repeated string tags = 6;
NewsStatus status = 7;
}

message UploadNewsResponse {
int32 success_count = 1;
string message = 2;
}

message ChatMessage {
string sender = 1;
string content = 2;
int64 timestamp = 3;
}

enum NewsStatus {
NEWS_STATUS_UNSPECIFIED = 0;
NEWS_STATUS_DRAFT = 1;
NEWS_STATUS_PUBLISHED = 2;
NEWS_STATUS_DELETED = 3;
}

3.3 syntax = "proto3"

proto3 是目前常用的 Protobuf 语法版本。

它相比 proto2 更简化:

  • 不再使用 required
  • 字段有默认值。
  • 支持 map
  • 支持 oneof
  • 新版本支持 optional 字段存在性语义。

3.4 packagejava_package

1
2
package news.v1;
option java_package = "com.example.grpc.news";

这里有两个概念:

配置 作用
package Protobuf 层面的命名空间,也会影响 gRPC 方法路径。
java_package 生成 Java 代码时使用的包名。

建议:

  • package 使用业务域 + 版本,例如 news.v1
  • java_package 使用 Java 标准反向域名,例如 com.example.grpc.news
  • 不要把 proto package 随便改掉,因为它会影响 RPC 全限定名。

3.5 字段编号比字段名更重要

1
2
3
4
message NewsItem {
int64 id = 1;
string title = 2;
}

Protobuf 真正编码时依赖的是字段编号,不是字段名。字段名主要服务于代码生成和可读性。

所以:

  • id = 1 里的 1 不要随便改。
  • 删除字段后,原编号不要给新字段复用。
  • 字段编号一旦发布,就当成数据库表字段一样谨慎。

3.6 常用字段类型

Protobuf 类型 Java 类型 说明
double double 双精度浮点。
float float 单精度浮点。
int32 int 可变长编码,负数效率不高。
int64 long 长整型。
uint32 int 无符号 32 位。
uint64 long 无符号 64 位。
sint32 int 对负数更友好的 ZigZag 编码。
sint64 long 对负数更友好的 ZigZag 编码。
bool boolean 布尔值。
string String UTF-8 字符串。
bytes ByteString 二进制数据。

注意:如果字段可能出现大量负数,优先考虑 sint32/sint64,不要无脑 int32/int64

3.7 repeated、map、oneof

repeated

1
2
3
message NewsItem {
repeated string tags = 6;
}

生成 Java 后通常是 List 语义:

1
2
3
4
NewsItem item = NewsItem.newBuilder()
.addTags("grpc")
.addTags("protobuf")
.build();

map

1
2
3
message ExtraInfo {
map<string, string> attributes = 1;
}

适合表达简单键值结构,但不要滥用。复杂业务字段如果都塞进 map,最后 proto 会变成“二进制版 JSON”,强类型优势直接归零。

oneof

1
2
3
4
5
6
7
message SearchCondition {
oneof condition {
int64 id = 1;
string keyword = 2;
string author = 3;
}
}

oneof 表示多个字段里最多只能有一个被设置,适合互斥参数。

3.8 Protobuf 接口演进规则

这是 Protobuf 最重要的一章,甚至比语法还重要。

可以做的变更

变更 是否安全 说明
新增字段 通常安全 新字段使用新的字段编号即可。
删除字段但保留编号 安全 删除后用 reserved 保留编号和字段名。
字段改名 通常二进制兼容 编码看编号,但生成代码和 JSON 映射会受影响。
新增 enum 值 通常安全 但客户端要能处理未知枚举。
新增 service 方法 通常安全 老客户端不受影响。

危险变更

变更 风险
修改字段编号 等于换字段,极其危险。
复用已删除字段编号 老数据可能被错误解析。
改字段类型 可能导致解析错误或语义错乱。
删除 enum 默认值 0 proto3 enum 必须有 0 值。
大规模重命名 package/service 会影响 gRPC 方法路径。

推荐写法

1
2
3
4
5
6
7
8
message NewsItem {
reserved 8, 9;
reserved "old_title", "old_content";

int64 id = 1;
string title = 2;
string content = 3;
}

字段下线后,先 reserved。别图省事复用编号。省一秒,埋一年;这种技术债,比缓存穿透还会穿心。


四、gRPC 的底层通信原理

4.1 gRPC 基于 HTTP/2

gRPC 通常使用 HTTP/2 作为传输协议。一次 gRPC 调用,本质上是一次 HTTP/2 请求。

例如:

1
2
3
4
5
POST /news.v1.NewsService/GetNews HTTP/2
content-type: application/grpc
te: trailers
grpc-timeout: 2S
authorization: Bearer xxx

路径格式:

1
/{package}.{Service}/{Method}

例如:

1
/news.v1.NewsService/GetNews

4.2 HTTP/2 对 gRPC 的价值

HTTP/2 能力 对 gRPC 的意义
多路复用 一个连接上可以并发多个 RPC,减少连接开销。
Header 压缩 减少元数据传输开销。
Stream 天然支持服务端流、客户端流、双向流。
Trailers gRPC 使用 Trailers 返回最终状态码和错误信息。
二进制帧 更适合高性能通信。

4.3 gRPC 消息帧

gRPC 在 HTTP/2 Data Frame 里传输自己的消息格式。一个 gRPC 消息通常包含:

1
2
3
+----------------------+----------------------+------------------+
| Compressed Flag(1B) | Message Length(4B) | Message Bytes |
+----------------------+----------------------+------------------+

也就是说,Protobuf 序列化后的消息并不是裸奔,它会带上 gRPC 自己的前缀。

4.4 Headers、Message、Trailers

一次成功调用大致是:

1
2
3
4
5
Client -> Server: Headers
Client -> Server: Message
Server -> Client: Headers
Server -> Client: Message
Server -> Client: Trailers(grpc-status=0)

失败时,服务端可能在 Trailers 中返回:

1
2
grpc-status: 3
grpc-message: invalid id

这里的 grpc-status 不是 HTTP 状态码,而是 gRPC 自己的状态码。

4.5 常见 gRPC 状态码

状态码 含义 常见场景
OK 成功 正常返回。
INVALID_ARGUMENT 参数错误 参数格式错误、非法枚举。
NOT_FOUND 资源不存在 根据 ID 查不到数据。
ALREADY_EXISTS 资源已存在 创建时唯一键冲突。
PERMISSION_DENIED 无权限 已认证但无权限。
UNAUTHENTICATED 未认证 Token 缺失或无效。
RESOURCE_EXHAUSTED 资源耗尽 限流、配额不足。
FAILED_PRECONDITION 前置条件不满足 状态不允许当前操作。
ABORTED 操作中止 并发冲突、事务冲突。
UNAVAILABLE 服务不可用 服务下线、网络抖动。
DEADLINE_EXCEEDED 超时 调用超过 deadline。
INTERNAL 内部错误 未预期异常。

实践建议:

  • 参数错误不要一股脑返回 UNKNOWN
  • 鉴权失败区分 UNAUTHENTICATEDPERMISSION_DENIED
  • 可重试的临时错误使用 UNAVAILABLE,但非幂等操作不要随意重试。
  • 业务异常要映射成稳定状态码,否则客户端无法治理。

五、gRPC 的四种通信模式

5.1 一元 RPC:Unary RPC

一元 RPC 是最常见的模式:一个请求,一个响应。

1
rpc GetNews(GetNewsRequest) returns (NewsResponse);

适合:

  • 查询详情。
  • 创建订单。
  • 更新状态。
  • 普通同步调用。

调用模型:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: request
    S-->>C: response

5.2 服务端流式 RPC:Server Streaming RPC

客户端发送一次请求,服务端返回多个响应。

1
rpc ListNews(ListNewsRequest) returns (stream NewsItem);

适合:

  • 分批返回大量数据。
  • 日志实时拉取。
  • 订阅服务端事件。
  • 服务端持续推送处理进度。

调用模型:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: request
    S-->>C: response 1
    S-->>C: response 2
    S-->>C: response 3

5.3 客户端流式 RPC:Client Streaming RPC

客户端连续发送多个请求,服务端最终返回一个响应。

1
rpc UploadNews(stream NewsItem) returns (UploadNewsResponse);

适合:

  • 批量上传。
  • 大文件分片上传。
  • 客户端持续上报指标。
  • 多条消息聚合后统一处理。

调用模型:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: request 1
    C->>S: request 2
    C->>S: request 3
    S-->>C: response

5.4 双向流式 RPC:Bidirectional Streaming RPC

客户端和服务端都可以连续发送消息。

1
rpc Chat(stream ChatMessage) returns (stream ChatMessage);

适合:

  • 聊天。
  • 实时协作。
  • 双向事件通道。
  • 实时音视频控制信令。
  • 长连接业务流。

调用模型:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: message 1
    S-->>C: message A
    C->>S: message 2
    S-->>C: message B

5.5 Streaming 不是万能药

流式 RPC 很强,但不要看到 stream 就想上。

流式 RPC 的代价:

  • 调试比一元 RPC 难。
  • 生命周期更长,资源占用更复杂。
  • 中途失败处理更麻烦。
  • 已经建立的流不能像普通请求那样轻易重新负载均衡。
  • 对网关、代理、超时、连接池配置要求更高。

建议:

场景 建议
普通 CRUD 一元 RPC。
大量列表分页 先分页,不要上来就 stream。
确实需要连续推送 服务端流。
确实需要连续上传 客户端流。
确实需要双向实时交互 双向流。

六、gRPC Java 生成代码解析

6.1 生成哪些代码

假设我们有:

1
2
3
service NewsService {
rpc GetNews(GetNewsRequest) returns (NewsResponse);
}

编译后通常会生成:

生成类 作用
GetNewsRequest 请求消息类。
NewsResponse 响应消息类。
NewsItem 业务消息类。
NewsServiceGrpc gRPC 服务相关类集合。
NewsServiceGrpc.NewsServiceImplBase 服务端骨架类,业务实现继承它。
NewsServiceGrpc.NewsServiceBlockingStub 阻塞式客户端 Stub。
NewsServiceGrpc.NewsServiceStub 异步客户端 Stub,支持流。
NewsServiceGrpc.NewsServiceFutureStub Future 风格客户端 Stub。
MethodDescriptor 方法元数据。

6.2 Builder 模式

Protobuf 生成的 Java 消息对象是不可变对象,通常通过 Builder 构建:

1
2
3
4
5
6
7
8
9
NewsItem item = NewsItem.newBuilder()
.setId(1001L)
.setTitle("gRPC 入门")
.setContent("这是一条新闻")
.setAuthor("SuperMario")
.addTags("grpc")
.addTags("protobuf")
.setStatus(NewsStatus.NEWS_STATUS_PUBLISHED)
.build();

6.3 三种客户端 Stub

BlockingStub

1
2
NewsServiceGrpc.NewsServiceBlockingStub stub = NewsServiceGrpc.newBlockingStub(channel);
NewsResponse response = stub.getNews(request);

特点:

  • 同步阻塞。
  • 代码简单。
  • 适合普通业务调用。
  • 不要在 Netty EventLoop 或响应式线程里阻塞调用。

FutureStub

1
2
NewsServiceGrpc.NewsServiceFutureStub stub = NewsServiceGrpc.newFutureStub(channel);
ListenableFuture<NewsResponse> future = stub.getNews(request);

特点:

  • 适合异步一元调用。
  • 基于 Guava ListenableFuture
  • 不适合流式调用。

AsyncStub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NewsServiceGrpc.NewsServiceStub stub = NewsServiceGrpc.newStub(channel);
stub.getNews(request, new StreamObserver<NewsResponse>() {
@Override
public void onNext(NewsResponse value) {
// 处理响应
}

@Override
public void onError(Throwable t) {
// 处理异常
}

@Override
public void onCompleted() {
// 完成
}
});

特点:

  • 回调式异步。
  • 支持四种 RPC 模式。
  • 流式 RPC 基本都要用它。

七、原生 grpc-java 实战

这一章不依赖 Spring Boot,先用最小代码理解 gRPC 的核心工程结构。

7.1 项目结构

建议把 .proto 和生成代码放到独立 API 模块,服务端和客户端共同依赖它。

1
2
3
4
5
6
7
8
9
10
11
grpc-news-demo
├── pom.xml
├── news-api
│ ├── pom.xml
│ └── src/main/proto/news.proto
├── news-server
│ ├── pom.xml
│ └── src/main/java/com/example/grpc/server
└── news-client
├── pom.xml
└── src/main/java/com/example/grpc/client

7.2 父 POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>grpc-news-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>

<modules>
<module>news-api</module>
<module>news-server</module>
<module>news-client</module>
</modules>

<properties>
<java.version>11</java.version>
<grpc.version>1.81.0</grpc.version>
<protobuf.version>3.25.9</protobuf.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-bom</artifactId>
<version>${grpc.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>${os-maven-plugin.version}</version>
</extension>
</extensions>
</build>
</project>

说明:这里使用 grpc-bom 统一 gRPC 依赖版本,避免 grpc-stubgrpc-protobufgrpc-netty-shaded 版本不一致。

7.3 news-api 模块 POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.example</groupId>
<artifactId>grpc-news-demo</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>news-api</artifactId>

<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-maven-plugin.version}</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

常见问题:

问题 解决
Apple Silicon 编译失败 升级 protocprotoc-gen-grpc-java,确认有 osx-aarch_64 可执行包。
找不到 javax.annotation.Generated JDK 9+ 可加 javax.annotation-api
生成代码没进 classpath 执行 mvn clean compile,确认 target/generated-sources 被 IDE 标记为 Generated Sources。
protoc 与 protobuf-java 版本冲突 尽量统一版本,并由父 POM 管理。

7.4 服务端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.example.grpc.server;

import com.example.grpc.news.GetNewsRequest;
import com.example.grpc.news.ListNewsRequest;
import com.example.grpc.news.NewsItem;
import com.example.grpc.news.NewsResponse;
import com.example.grpc.news.NewsServiceGrpc;
import com.example.grpc.news.NewsStatus;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;

import java.time.Instant;
import java.util.List;

/**
* NewsService 的 gRPC 服务端实现。
*/
public class NewsServiceImpl extends NewsServiceGrpc.NewsServiceImplBase {

@Override
public void getNews(GetNewsRequest request, StreamObserver<NewsResponse> responseObserver) {
try {
if (request.getId() <= 0) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("id must be positive")
.asRuntimeException());
return;
}

NewsItem item = NewsItem.newBuilder()
.setId(request.getId())
.setTitle("gRPC 深度实践")
.setContent("这是一条通过 gRPC 返回的新闻")
.setAuthor("SuperMario")
.setPublishTime(Instant.now().toEpochMilli())
.addTags("grpc")
.addTags("protobuf")
.setStatus(NewsStatus.NEWS_STATUS_PUBLISHED)
.build();

NewsResponse response = NewsResponse.newBuilder()
.setData(item)
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (Exception ex) {
responseObserver.onError(Status.INTERNAL
.withDescription("server internal error")
.withCause(ex)
.asRuntimeException());
}
}

@Override
public void listNews(ListNewsRequest request, StreamObserver<NewsItem> responseObserver) {
List<NewsItem> items = List.of(
buildItem(1L, "gRPC 入门"),
buildItem(2L, "Protobuf 语法"),
buildItem(3L, "Spring Boot 集成 gRPC")
);

for (NewsItem item : items) {
responseObserver.onNext(item);
}

responseObserver.onCompleted();
}

private NewsItem buildItem(Long id, String title) {
return NewsItem.newBuilder()
.setId(id)
.setTitle(title)
.setContent("content of " + title)
.setAuthor("SuperMario")
.setPublishTime(Instant.now().toEpochMilli())
.setStatus(NewsStatus.NEWS_STATUS_PUBLISHED)
.build();
}
}

7.5 服务端启动器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.grpc.server;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.protobuf.services.ProtoReflectionService;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
* 原生 grpc-java 服务端启动类。
*/
public class NewsGrpcServer {

private Server server;

public void start() throws IOException {
int port = 9090;
this.server = ServerBuilder.forPort(port)
.addService(new NewsServiceImpl())
// 开启 reflection 后可以用 grpcurl list/describe,生产环境要做好访问控制
.addService(ProtoReflectionService.newInstance())
.build()
.start();

System.out.println("gRPC server started, port=" + port);

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("JVM shutdown, stopping gRPC server...");
try {
NewsGrpcServer.this.stop();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
}

public void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}

private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}

public static void main(String[] args) throws Exception {
NewsGrpcServer server = new NewsGrpcServer();
server.start();
server.blockUntilShutdown();
}
}

7.6 客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.example.grpc.client;

import com.example.grpc.news.GetNewsRequest;
import com.example.grpc.news.ListNewsRequest;
import com.example.grpc.news.NewsItem;
import com.example.grpc.news.NewsResponse;
import com.example.grpc.news.NewsServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

import java.util.Iterator;
import java.util.concurrent.TimeUnit;

/**
* 原生 grpc-java 客户端。
*/
public class NewsGrpcClient {

private final ManagedChannel channel;
private final NewsServiceGrpc.NewsServiceBlockingStub blockingStub;

public NewsGrpcClient(String host, int port) {
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
this.blockingStub = NewsServiceGrpc.newBlockingStub(channel);
}

public void getNews(Long id) {
GetNewsRequest request = GetNewsRequest.newBuilder()
.setId(id)
.build();

try {
NewsResponse response = blockingStub
.withDeadlineAfter(2, TimeUnit.SECONDS)
.getNews(request);
System.out.println("response = " + response.getData().getTitle());
} catch (StatusRuntimeException ex) {
System.err.println("RPC failed: " + ex.getStatus());
}
}

public void listNews(String category) {
ListNewsRequest request = ListNewsRequest.newBuilder()
.setCategory(category)
.setPageSize(10)
.build();

Iterator<NewsItem> iterator = blockingStub
.withDeadlineAfter(5, TimeUnit.SECONDS)
.listNews(request);

while (iterator.hasNext()) {
NewsItem item = iterator.next();
System.out.println(item.getId() + " - " + item.getTitle());
}
}

public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}

public static void main(String[] args) throws Exception {
NewsGrpcClient client = new NewsGrpcClient("127.0.0.1", 9090);
try {
client.getNews(1001L);
client.listNews("tech");
} finally {
client.shutdown();
}
}
}

7.7 grpcurl 调试

安装 grpcurl 后:

1
grpcurl -plaintext 127.0.0.1:9090 list

查看服务:

1
grpcurl -plaintext 127.0.0.1:9090 list news.v1.NewsService

调用方法:

1
2
3
4
5
grpcurl \
-plaintext \
-d '{"id": 1001}' \
127.0.0.1:9090 \
news.v1.NewsService/GetNews

如果没有开启 reflection,也可以指定 proto 文件:

1
2
3
4
5
6
7
grpcurl \
-plaintext \
-import-path ./news-api/src/main/proto \
-proto news.proto \
-d '{"id": 1001}' \
127.0.0.1:9090 \
news.v1.NewsService/GetNews

八、Spring Boot 集成 gRPC

原生 grpc-java 能让你理解底层,但业务项目通常会接入 Spring Boot。常见做法是使用 grpc-spring-boot-starter,它可以自动创建 gRPC Server、自动扫描 @GrpcService,客户端通过 @GrpcClient 注入 Stub。

8.1 版本选择建议

课程材料里常见的版本是:

1
2
3
4
5
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.13.0.RELEASE</version>
</dependency>

这个版本对入门没问题,但现在做新项目要按 Spring Boot 主版本选择:

Spring Boot JDK starter 建议
Spring Boot 2.7.x JDK 8/11/17 net.devh:grpc-spring-boot-starter:2.15.0.RELEASE
Spring Boot 3.2.x JDK 17+ net.devh:grpc-spring-boot-starter:3.1.0.RELEASE

如果你的公司项目仍是 Spring Boot 2 + JDK 11,就优先选 2.15.0.RELEASE。如果已经升级 Spring Boot 3,再选 3.x。不要混着来,否则你会被 javaxjakarta、Netty、Spring Cloud 版本兼容性来回毒打。

8.2 Spring Boot 服务端依赖

以 Spring Boot 2.7.x 为例:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>

<dependency>
<groupId>com.example</groupId>
<artifactId>news-api</artifactId>
<version>1.0.0</version>
</dependency>

如果服务端和客户端在同一个应用内,也可以直接用整合包:

1
2
3
4
5
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>

8.3 服务端配置

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8080

spring:
application:
name: news-grpc-server

grpc:
server:
port: 9090
reflection-service-enabled: true

说明:

  • server.port=8080 是 Spring Boot Web 端口。
  • grpc.server.port=9090 是 gRPC 端口。
  • gRPC 和 HTTP REST 可以在同一个 Spring Boot 应用里同时存在。
  • reflection 方便本地调试,生产环境建议关闭或加访问控制。

8.4 使用 @GrpcService 实现服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example.news.server.grpc;

import com.example.grpc.news.GetNewsRequest;
import com.example.grpc.news.NewsItem;
import com.example.grpc.news.NewsResponse;
import com.example.grpc.news.NewsServiceGrpc;
import com.example.grpc.news.NewsStatus;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

import java.time.Instant;

/**
* Spring Boot 方式实现 gRPC 服务。
*/
@GrpcService
public class NewsGrpcService extends NewsServiceGrpc.NewsServiceImplBase {

@Override
public void getNews(GetNewsRequest request, StreamObserver<NewsResponse> responseObserver) {
if (request.getId() <= 0) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("id must be positive")
.asRuntimeException());
return;
}

NewsItem item = NewsItem.newBuilder()
.setId(request.getId())
.setTitle("Spring Boot 集成 gRPC")
.setContent("通过 @GrpcService 暴露 gRPC 服务")
.setAuthor("SuperMario")
.setPublishTime(Instant.now().toEpochMilli())
.setStatus(NewsStatus.NEWS_STATUS_PUBLISHED)
.build();

responseObserver.onNext(NewsResponse.newBuilder().setData(item).build());
responseObserver.onCompleted();
}
}

8.5 客户端依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>

<dependency>
<groupId>com.example</groupId>
<artifactId>news-api</artifactId>
<version>1.0.0</version>
</dependency>

8.6 客户端配置:静态地址

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8081

spring:
application:
name: news-grpc-client

grpc:
client:
news-service:
address: static://127.0.0.1:9090
negotiationType: plaintext

8.7 使用 @GrpcClient 注入 Stub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.news.client;

import com.example.grpc.news.GetNewsRequest;
import com.example.grpc.news.NewsResponse;
import com.example.grpc.news.NewsServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
* gRPC 客户端调用封装。
*/
@Service
public class NewsClientService {

@GrpcClient("news-service")
private NewsServiceGrpc.NewsServiceBlockingStub newsStub;

public String getTitle(Long id) {
NewsResponse response = newsStub
.withDeadlineAfter(2, TimeUnit.SECONDS)
.getNews(GetNewsRequest.newBuilder().setId(id).build());
return response.getData().getTitle();
}
}

8.8 REST 转 gRPC 的 BFF 示例

很多项目会对外提供 REST,对内调用 gRPC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.news.client;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 对外 REST,对内 gRPC。
*/
@RestController
@RequestMapping("/api/news")
public class NewsController {

private final NewsClientService newsClientService;

public NewsController(NewsClientService newsClientService) {
this.newsClientService = newsClientService;
}

@GetMapping("/{id}/title")
public String getTitle(@PathVariable Long id) {
return newsClientService.getTitle(id);
}
}

这种模式很常见:

flowchart LR
    Browser[浏览器/前端] -->|HTTP JSON| BFF[Spring Boot BFF]
    BFF -->|gRPC Protobuf| News[News gRPC Service]
    News --> DB[(Database)]

九、gRPC 接入服务发现:Eureka、Nacos 与 Spring Cloud

9.1 为什么 gRPC 也需要注册中心

如果客户端写死:

1
2
3
4
grpc:
client:
news-service:
address: static://127.0.0.1:9090

这只能本地玩玩。真实微服务里,服务实例可能动态扩缩容、滚动发布、故障下线,客户端需要通过注册中心拿到可用地址。

9.2 Spring Cloud DiscoveryClient 模式

grpc-spring-boot-starter 可以结合 Spring Cloud 的 DiscoveryClient,从 Eureka、Consul、Nacos 等注册中心发现服务地址。

服务端启动后,需要把 gRPC 端口也注册到服务元数据里。starter 会帮你做很多自动化工作。

9.3 Eureka 示例

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: news-grpc-server

grpc:
server:
port: 9090

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/
instance:
prefer-ip-address: true

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: news-grpc-client

grpc:
client:
news-grpc-server:
address: discovery:///news-grpc-server
negotiationType: plaintext

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/

调用:

1
2
@GrpcClient("news-grpc-server")
private NewsServiceGrpc.NewsServiceBlockingStub newsStub;

9.4 Nacos 示例

如果使用 Spring Cloud Alibaba Nacos,服务端:

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: news-grpc-server
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

grpc:
server:
port: 9090

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: news-grpc-client
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

grpc:
client:
news-grpc-server:
address: discovery:///news-grpc-server
negotiationType: plaintext

注意:

  • Nacos 注册的是应用实例,gRPC 端口通常通过 metadata 传递。
  • HTTP 端口和 gRPC 端口可能不同,不要拿 server.port 当 gRPC 端口。
  • 如果发现客户端拿到地址但连不上,第一时间看注册中心 metadata 里有没有 gRPC port。

十、gRPC 安全认证:TLS、JWT 与拦截器

10.1 gRPC 安全分层

gRPC 常见安全方式:

层次 方案 说明
传输层 TLS 防止明文传输,服务端身份校验。
双向认证 mTLS 客户端和服务端互认证,适合内部零信任网络。
应用层 JWT/OAuth2 在 Metadata 里传 Authorization
服务治理 RBAC/ACL 判断当前用户或服务是否有方法访问权限。

10.2 Metadata 传递 Token

客户端把 JWT 放入 Metadata:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.news.client.security;

import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import com.example.grpc.news.NewsServiceGrpc;

/**
* 为 Stub 附加 Authorization Metadata。
*/
public class GrpcAuthUtil {

private static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of(
"authorization",
Metadata.ASCII_STRING_MARSHALLER
);

public static NewsServiceGrpc.NewsServiceBlockingStub attachJwt(
NewsServiceGrpc.NewsServiceBlockingStub stub,
String jwt
) {
Metadata headers = new Metadata();
headers.put(AUTHORIZATION, "Bearer " + jwt);
return MetadataUtils.attachHeaders(stub, headers);
}
}

10.3 服务端 JWT 拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.news.server.security;

import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;

/**
* gRPC 服务端 JWT 鉴权拦截器。
*/
public class JwtServerInterceptor implements ServerInterceptor {

public static final Context.Key<String> USER_ID_CTX_KEY = Context.key("userId");

private static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of(
"authorization",
Metadata.ASCII_STRING_MARSHALLER
);

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next
) {
String authorization = headers.get(AUTHORIZATION);

if (authorization == null || !authorization.startsWith("Bearer ")) {
call.close(Status.UNAUTHENTICATED.withDescription("missing token"), new Metadata());
return new ServerCall.Listener<>() {
};
}

String token = authorization.substring("Bearer ".length());

// 这里只是演示,生产里应该调用统一认证组件校验签名、过期时间、issuer、audience 等。
String userId = verifyAndGetUserId(token);
if (userId == null) {
call.close(Status.UNAUTHENTICATED.withDescription("invalid token"), new Metadata());
return new ServerCall.Listener<>() {
};
}

Context context = Context.current().withValue(USER_ID_CTX_KEY, userId);
return Contexts.interceptCall(context, call, headers, next);
}

private String verifyAndGetUserId(String token) {
if (token.isBlank()) {
return null;
}
return "10001";
}
}

10.4 Spring Boot 注册全局拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.news.server.config;

import com.example.news.server.security.JwtServerInterceptor;
import io.grpc.ServerInterceptor;
import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;
import org.springframework.context.annotation.Configuration;

/**
* gRPC 服务端全局拦截器配置。
*/
@Configuration
public class GrpcInterceptorConfig {

@GrpcGlobalServerInterceptor
public ServerInterceptor jwtServerInterceptor() {
return new JwtServerInterceptor();
}
}

10.5 在业务方法中获取用户

1
String userId = JwtServerInterceptor.USER_ID_CTX_KEY.get();

注意:

  • gRPC 的 Context 不是普通 ThreadLocal,但在 gRPC 调用链里可以传播。
  • 如果你把任务丢到线程池,仍然要考虑上下文传递。
  • 如果公司已有 TraceContext、UserContext,一定要统一封装,不要每个服务自己造轮子。

十一、异常处理与状态码映射

11.1 不要直接抛 RuntimeException

错误示例:

1
throw new RuntimeException("id error");

客户端大概率拿到的是 UNKNOWNINTERNAL,很难判断是否能重试、是否参数错误、是否鉴权失败。

推荐:

1
2
3
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("id must be positive")
.asRuntimeException());

11.2 Spring Boot 全局异常处理

grpc-spring-boot-starter 支持类似 ControllerAdvice 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.news.server.exception;

import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import net.devh.boot.grpc.server.advice.GrpcAdvice;
import net.devh.boot.grpc.server.advice.GrpcExceptionHandler;

/**
* gRPC 全局异常映射。
*/
@GrpcAdvice
public class GlobalGrpcExceptionAdvice {

@GrpcExceptionHandler(IllegalArgumentException.class)
public StatusRuntimeException handleIllegalArgument(IllegalArgumentException ex) {
return Status.INVALID_ARGUMENT
.withDescription(ex.getMessage())
.asRuntimeException();
}

@GrpcExceptionHandler(SecurityException.class)
public StatusRuntimeException handleSecurity(SecurityException ex) {
return Status.PERMISSION_DENIED
.withDescription(ex.getMessage())
.asRuntimeException();
}

@GrpcExceptionHandler(Exception.class)
public StatusRuntimeException handleException(Exception ex) {
return Status.INTERNAL
.withDescription("internal server error")
.withCause(ex)
.asRuntimeException();
}
}

11.3 状态码映射建议

Java 异常 gRPC 状态码
IllegalArgumentException INVALID_ARGUMENT
NoSuchElementException / 业务 NotFound NOT_FOUND
唯一键冲突 ALREADY_EXISTS
未登录 / Token 无效 UNAUTHENTICATED
无权限 PERMISSION_DENIED
限流 RESOURCE_EXHAUSTED
乐观锁冲突 ABORTED
下游不可用 UNAVAILABLE
未知系统异常 INTERNAL

十二、Deadline、Retry、KeepAlive 与连接治理

12.1 Deadline 必须设置

不要让 RPC 无限等。

1
2
3
NewsResponse response = newsStub
.withDeadlineAfter(2, TimeUnit.SECONDS)
.getNews(request);

没有 Deadline 的调用,就像没有还款日期的借条:理论上还能回来,现实里你最好别信。

12.2 服务端感知取消

如果客户端超时取消了请求,服务端应该尽快停止无意义计算。

伪代码:

1
2
3
4
5
6
while (hasMoreWork()) {
if (Context.current().isCancelled()) {
return;
}
doWork();
}

12.3 Retry 要谨慎

适合重试的场景:

  • 幂等查询。
  • 临时网络抖动。
  • UNAVAILABLE
  • 部分读操作。

不适合随便重试:

  • 创建订单。
  • 扣库存。
  • 支付。
  • 发送消息。
  • 任何非幂等写操作。

如果要重试,先设计幂等键。

12.4 Channel 和 Stub 要复用

推荐:

  • ManagedChannel 长生命周期复用。
  • Stub 可以复用,因为它通常是不可变轻量对象。
  • 不要每次调用都 new channel。

错误示例:

1
2
3
4
5
6
public NewsResponse call(GetNewsRequest request) {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090)
.usePlaintext()
.build();
return NewsServiceGrpc.newBlockingStub(channel).getNews(request);
}

这会造成连接频繁创建,性能和资源都很糟糕。

12.5 KeepAlive 不要乱开

KeepAlive 可以让连接空闲时通过 HTTP/2 PING 保活。但配置太激进会导致:

  • 服务端压力增加。
  • 网关或代理主动断连。
  • 大规模客户端形成无意义心跳风暴。

建议只在确实有长连接、NAT、LB 空闲断连问题时再调。


十三、Dubbo、Triple、gRPC 的关系

13.1 Dubbo 是什么

Apache Dubbo 是一个微服务 RPC 框架。它不只是通信协议,还提供:

  • 服务暴露与引用。
  • 注册发现。
  • 负载均衡。
  • 集群容错。
  • 路由与流量治理。
  • 超时与重试。
  • Filter 拦截器。
  • 泛化调用。
  • 多协议支持。
  • 可观测性能力。

gRPC 更像“协议 + 多语言 RPC 框架”;Dubbo 更像“面向微服务治理的 RPC 框架”。

13.2 Dubbo 支持哪些协议

Dubbo 常见协议:

协议 说明
dubbo Dubbo 传统协议,基于 TCP,适合 Java Dubbo 体系。
tri / Triple Dubbo 3 主推协议,基于 HTTP/2,兼容 gRPC。
grpc 可直接接入 gRPC 协议。
rest REST 风格通信。
hessian2 老系统常见二进制协议。

13.3 Triple 协议是什么

Triple 是 Dubbo 3 推出的基于 HTTP/2 的 RPC 协议。它的定位很关键:

  1. 保留 Dubbo 的服务治理能力。
  2. 兼容 gRPC 协议生态。
  3. 支持 Protobuf,也支持 Java Interface 模式。
  4. 支持流式通信、Trailers、错误详情等 gRPC 能力。
  5. 更方便做多协议暴露。

可以这样理解:

flowchart TB
    A[Dubbo 业务实现] --> B[Triple Protocol]
    B --> C[Dubbo Client]
    B --> D[标准 gRPC Client]
    B --> E[cURL / HTTP 工具]
    B --> F[浏览器/网关场景]

13.4 Dubbo 与 gRPC 怎么选

对比项 原生 gRPC Dubbo Triple
跨语言 非常强 兼容 gRPC,也支持跨语言,但 Java 生态体验最好。
服务治理 需要自己组合 Dubbo 内置治理能力更完整。
Spring Boot 集成 依赖第三方 starter 官方 starter 支持。
注册中心 需要额外整合 原生支持 Nacos/Zookeeper 等。
接口模式 IDL-first Java Interface 或 IDL 都可以。
对外暴露 原生浏览器不友好 Triple 在可访问性上更灵活。
适合场景 多语言高性能内部 RPC Java 微服务治理 + gRPC 兼容。

一句话:

  • 多语言、协议标准优先:选 gRPC。
  • Java 微服务治理优先:选 Dubbo。
  • 既要治理又要兼容 gRPC:看 Dubbo Triple。

十四、Spring Boot 整合 Dubbo Triple

14.1 项目结构

1
2
3
4
5
6
7
dubbo-news-demo
├── news-api
│ └── NewsFacade.java
├── news-provider
│ └── NewsFacadeImpl.java
└── news-consumer
└── NewsConsumerController.java

14.2 Maven 依赖

以 Dubbo 3.3.x 为例,版本可以统一放到父 POM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<properties>
<dubbo.version>3.3.6</dubbo.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Provider 和 Consumer 都引入:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>

如果你使用 Zookeeper,可以换成对应 Zookeeper starter。公司里如果已经有 Nacos,直接走 Nacos 就好,少维护一个组件,运维同学也能少掉两根头发。

14.3 API 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.dubbo.news.api;

import java.util.List;

/**
* 新闻服务 Dubbo API。
*/
public interface NewsFacade {

NewsDTO getById(Long id);

List<NewsDTO> listByCategory(String category);
}

DTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.dubbo.news.api;

import java.io.Serializable;

/**
* 新闻 DTO。
*/
public class NewsDTO implements Serializable {

private Long id;

private String title;

private String content;

public NewsDTO() {
}

public NewsDTO(Long id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

14.4 Provider 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8082

spring:
application:
name: news-dubbo-provider

dubbo:
application:
name: news-dubbo-provider
registry:
address: nacos://127.0.0.1:8848
protocol:
name: tri
port: 50051
provider:
timeout: 3000
retries: 0

说明:

  • dubbo.protocol.name=tri 表示使用 Triple 协议。
  • dubbo.protocol.port=50051 是 Dubbo Triple 暴露端口。
  • retries=0 是为了避免非幂等写操作被默认重试。读接口可以单独配置重试。

14.5 Provider 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.dubbo.news.provider;

import com.example.dubbo.news.api.NewsDTO;
import com.example.dubbo.news.api.NewsFacade;
import org.apache.dubbo.config.annotation.DubboService;

import java.util.List;

/**
* Dubbo Triple Provider。
*/
@DubboService(version = "1.0.0", group = "news")
public class NewsFacadeImpl implements NewsFacade {

@Override
public NewsDTO getById(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("id must be positive");
}
return new NewsDTO(id, "Dubbo Triple 实战", "通过 Dubbo Triple 暴露服务");
}

@Override
public List<NewsDTO> listByCategory(String category) {
return List.of(
new NewsDTO(1L, "Dubbo 入门", "Dubbo 基础内容"),
new NewsDTO(2L, "Triple 协议", "Dubbo Triple 与 gRPC 兼容")
);
}
}

启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.dubbo.news.provider;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableDubbo
@SpringBootApplication
public class NewsDubboProviderApplication {

public static void main(String[] args) {
SpringApplication.run(NewsDubboProviderApplication.class, args);
}
}

14.6 Consumer 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8083

spring:
application:
name: news-dubbo-consumer

dubbo:
application:
name: news-dubbo-consumer
registry:
address: nacos://127.0.0.1:8848
consumer:
timeout: 3000
check: false

14.7 Consumer 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.dubbo.news.consumer;

import com.example.dubbo.news.api.NewsDTO;
import com.example.dubbo.news.api.NewsFacade;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 对外 REST,对内 Dubbo Triple。
*/
@RestController
@RequestMapping("/api/dubbo/news")
public class NewsDubboConsumerController {

@DubboReference(version = "1.0.0", group = "news")
private NewsFacade newsFacade;

@GetMapping("/{id}")
public NewsDTO getById(@PathVariable Long id) {
return newsFacade.getById(id);
}
}

14.8 Dubbo Triple 与标准 gRPC 互通

Dubbo Triple 可以做两类互通:

  1. 标准 gRPC Client 调用 Dubbo Triple Server。
  2. Dubbo Client 调用标准 gRPC Server。

互通思路:

sequenceDiagram
    participant G as Standard gRPC Client
    participant T as Dubbo Triple Server
    participant B as Business Impl

    G->>T: HTTP/2 + Protobuf + gRPC path
    T->>B: 调用 Dubbo 业务实现
    B-->>T: 返回业务结果
    T-->>G: gRPC-compatible response

工程上一般要使用 Protobuf IDL 定义服务,而不是只用 Java Interface。这样标准 gRPC 客户端才能根据同一份 .proto 生成 Stub 并发起调用。

Dubbo 官方示例中可以看到两个方向:

1
2
git clone --depth=1 https://github.com/apache/dubbo-samples.git
cd dubbo-samples/2-advanced/dubbo-samples-triple-grpc

方向一:启动 Dubbo Triple Server,然后用标准 gRPC Java Client 调用。

方向二:启动标准 gRPC Server,然后用 Dubbo Triple Client 调用。

实践建议:

  • 如果只是 Java 内部服务互调,用 Java Interface + Triple 更省事。
  • 如果需要与 Go/Python/Node 等标准 gRPC 客户端互通,优先使用 .proto 作为契约。
  • 不要一边想跨语言,一边只发 Java Interface Jar;那不是跨语言,是跨不过去。

十五、gRPC、Dubbo 与网关

15.1 对外为什么通常不用原生 gRPC

原生 gRPC 对浏览器不如 REST 友好,原因包括:

  • 浏览器对 HTTP/2 Trailers 支持有限。
  • Web 前端不能像后端一样直接使用完整 gRPC 客户端能力。
  • 前端调试、缓存、代理、跨域、鉴权生态仍然是 HTTP JSON 最成熟。

15.2 常见架构

架构一:外部 REST,内部 gRPC

flowchart LR
    FE[Web/App] -->|REST JSON| Gateway[API Gateway / BFF]
    Gateway -->|gRPC| A[User Service]
    Gateway -->|gRPC| B[Order Service]
    Gateway -->|gRPC| C[News Service]

架构二:外部 REST,内部 Dubbo Triple

flowchart LR
    FE[Web/App] -->|REST JSON| Gateway[API Gateway / BFF]
    Gateway -->|Dubbo Triple| A[User Provider]
    Gateway -->|Dubbo Triple| B[Order Provider]
    Gateway -->|Dubbo Triple| C[News Provider]

架构三:内部标准 gRPC + gRPC Gateway

flowchart LR
    FE[Web/App] -->|REST JSON| GW[gRPC Gateway]
    GW -->|gRPC| S[gRPC Service]

15.3 网关层职责

网关/BFF 不只是转发:

  • 协议转换。
  • 认证鉴权。
  • 参数校验。
  • 聚合多个后端服务。
  • 限流熔断。
  • 灰度发布。
  • 返回前端友好的错误格式。
  • 屏蔽内部服务结构。

十六、生产环境最佳实践

16.1 Protobuf 最佳实践

  1. 字段编号永不复用。
  2. 删除字段必须 reserved
  3. enum 第一个值必须是 XXX_UNSPECIFIED = 0
  4. package 里带版本,例如 order.v1
  5. 不要把所有扩展字段都塞进 map<string,string>
  6. 大对象不要直接塞进 Protobuf,尤其是图片、视频、大文件。
  7. 时间字段统一规范:毫秒时间戳、秒时间戳或 google.protobuf.Timestamp,别一个团队三种写法。
  8. 金额不要用 double,用整数分/厘或定点 decimal 表达。

16.2 gRPC 调用最佳实践

  1. 客户端必须设置 Deadline。
  2. Channel 和 Stub 要复用。
  3. 非幂等写操作不要盲目重试。
  4. 错误要映射成稳定状态码。
  5. Metadata 不要放大对象。
  6. Streaming 只在真正需要时使用。
  7. 生产环境启用 TLS 或 mTLS。
  8. 使用 Health Checking 做实例健康判断。
  9. 使用 Interceptor 做日志、Trace、Metrics、Auth。
  10. Reflection 在生产环境要关闭或限制访问。

16.3 Spring Boot 集成最佳实践

  1. 分离 API 模块,服务端和客户端共同依赖。
  2. 不要在业务模块重复放 .proto
  3. grpc.server.portserver.port 明确区分。
  4. Spring Boot 2 和 3 的 starter 版本不要混用。
  5. 如果接入注册中心,确认 metadata 里有 gRPC 端口。
  6. 如果使用 Nacos/Eureka,先验证 DiscoveryClient 能拿到实例。
  7. 用统一的异常 Advice 映射状态码。
  8. 用统一的 Client 封装设置 deadline、metadata、trace。

16.4 Dubbo 最佳实践

  1. Dubbo 3 新项目优先考虑 Triple。
  2. Java 内部互调用 Java Interface 足够;跨语言互通要用 Protobuf IDL。
  3. Provider 默认重试要谨慎,写操作建议关闭重试。
  4. group、version 要规范,不要当临时字符串乱填。
  5. Nacos namespace/group 要和环境隔离策略一致。
  6. 注册模式、应用级发现、接口级发现要明确配置。
  7. Dubbo Filter 里做统一日志、Trace、Auth,不要散落业务代码。
  8. 超时时间必须按接口配置,不要全局一个值打天下。

十七、常见问题排查

17.1 客户端报 UNAVAILABLE

可能原因:

  • 服务端没启动。
  • 端口错了。
  • 明文/ TLS 配置不一致。
  • 注册中心拿到的是 HTTP 端口,不是 gRPC 端口。
  • 网关或防火墙不支持 HTTP/2。
  • 服务端健康检查失败。

排查:

1
2
nc -vz 127.0.0.1 9090
grpcurl -plaintext 127.0.0.1:9090 list

17.2 客户端一直超时

检查:

  • Deadline 是否太短。
  • 服务端是否阻塞。
  • 是否线程池耗尽。
  • 是否下游 DB/Redis 卡住。
  • 是否发生连接队列堆积。
  • 是否大消息传输过慢。

17.3 proto 改了但客户端没生效

检查:

  • API Jar 是否重新打包。
  • Consumer 是否升级了 API 依赖版本。
  • IDE 是否刷新 Maven。
  • target/generated-sources 是否重新生成。
  • 是否改了字段名但没改字段编号,导致你以为变了其实二进制兼容仍按老编号解析。

17.4 Spring Boot 启动但 gRPC 端口没起来

检查:

  • 是否引入 grpc-server-spring-boot-starter
  • @GrpcService 包是否被 Spring 扫描。
  • 是否端口冲突。
  • 是否 grpc.server.port 配置错误。
  • 是否依赖版本和 Spring Boot 版本不兼容。

17.5 Dubbo Provider 注册不上 Nacos

检查:

  • Nacos 地址是否正确。
  • Nacos namespace/group 是否正确。
  • dubbo-nacos-spring-boot-starter 是否引入。
  • Nacos 用户名密码是否正确。
  • 应用名是否为空。
  • Dubbo 版本和 Nacos client 版本是否匹配。

十八、最终选型建议

18.1 只做内部 Java 服务

优先考虑:

  • Dubbo Triple。
  • 或 Spring Cloud OpenFeign + REST,如果性能和强类型要求不高。

如果团队已有 Dubbo 体系,没必要为了“时髦”强行上原生 gRPC。

18.2 多语言内部服务

优先考虑:

  • 原生 gRPC + Protobuf。
  • 统一 proto 仓库。
  • 配套 Buf 或 CI 校验接口兼容性。

18.3 Java 服务治理很重,又要兼容 gRPC

优先考虑:

  • Dubbo Triple + Protobuf IDL。

这样可以兼顾治理能力和 gRPC 互通。

18.4 对外开放 API

优先考虑:

  • REST + OpenAPI。
  • GraphQL。
  • gRPC-Web 或 API Gateway 作为补充。

不要把原生 gRPC 直接丢给第三方用户,除非你的用户就是后端工程师且愿意维护 SDK。现实一点,世界已经够难了。


十九、总结

gRPC 的核心价值不是“比 HTTP 快”这么简单。它真正重要的地方是:

  1. .proto 把接口契约固定下来。
  2. 用 Protobuf 提供高效、可演进的二进制消息格式。
  3. 用 HTTP/2 支撑多路复用和流式通信。
  4. 用生成代码提供强类型客户端和服务端。
  5. 用 Deadline、Status、Metadata、Interceptor 提供工程治理基础。

Dubbo 的核心价值也不是“另一个 RPC 框架”这么简单。它真正强的地方是:

  1. Java 微服务治理能力完整。
  2. 注册发现、负载均衡、路由、容错、观测都更一体化。
  3. Dubbo 3 Triple 协议让它能进入 HTTP/2 和 gRPC 兼容生态。

我的建议是:

  • 先搞清楚你要解决的是“协议问题”还是“治理问题”。
  • 如果是多语言强契约通信,gRPC 很好。
  • 如果是 Java 微服务治理,Dubbo 很好。
  • 如果两个都要,Dubbo Triple 值得重点看。

技术选型最怕“看别人用什么我就用什么”。架构不是赶集,别谁吆喝声大就往哪跑。真正靠谱的选择,是能和你的团队能力、系统复杂度、运维体系、未来演进方向匹配。


参考资料

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必不争一时之快。


从 gRPC 到 Protobuf、Dubbo Triple:Spring Boot 微服务通信深度实践
https://allendericdalexander.github.io/2026/06/09/archtect/grpc_protobuf_dubbo_springboot_blog/
作者
AtLuoFu
发布于
2026年6月9日
许可协议