欢迎你来读这篇博客,这篇博客主要是关于 Nacos + Spring Boot 3 + Dubbo 3 + gRPC/Triple 的完整整合实践。
这不是一篇“贴几个依赖就结束”的文章,而是按真实项目的方式,从架构选型、版本选择、工程结构、Proto 契约、Provider、Consumer、Nacos 注册发现、gRPC 兼容访问、参数治理、排错清单一路跑通。
序言 在 Java 微服务体系里,Spring Boot 3 解决的是应用开发与自动装配问题,Dubbo 3 解决的是 RPC 调用、服务治理、负载均衡、集群容错、协议抽象问题,Nacos 解决的是注册发现和配置管理问题,gRPC/Protobuf 解决的是跨语言、强类型、高性能接口契约问题。
把它们放在一起,最推荐的落地方式不是“Spring Boot 起一个原生 gRPC 服务,然后再旁边接一个 Dubbo”,而是使用 Dubbo 3 Triple。
Triple 是 Dubbo 3 提出的基于 HTTP/2 的 RPC 协议,它兼容 gRPC 生态。也就是说:
Java 内部服务可以用 Dubbo 的 @DubboService、@DubboReference、Nacos 注册发现、负载均衡、超时、重试、分组、版本等能力。
跨语言调用可以基于 Protobuf IDL 生成客户端,按 gRPC 风格调用。
Spring Boot 3 只负责应用启动、Web 调试入口、配置管理、Actuator 观测等外围能力。
一句话:Dubbo 3 Triple = Dubbo 服务治理能力 + gRPC/HTTP2/Protobuf 互通能力。
当然,名字里有 gRPC,就很容易让人头大:到底是 Dubbo Triple?还是原生 gRPC?还是 Dubbo 的 grpc protocol?别急,本文会拆开讲清楚,不然这几个概念站一起像开了个技术圆桌,谁也不让谁先发言。
正文 一、整体架构 本文实现一个最小但完整的业务案例:
rpc-api:定义 .proto 契约,并生成 Protobuf 消息类和 Dubbo Triple Stub。
user-provider:用户服务提供方,通过 Dubbo 3 Triple 暴露 RPC 服务,并注册到 Nacos。
order-consumer:订单服务消费方,通过 Nacos 发现 Provider,并使用 Dubbo 3 调用远程用户服务。
grpcurl:模拟标准 gRPC 客户端,直接调用 Dubbo Triple 暴露的服务。
flowchart LR
A[order-consumer<br/>Spring Boot 3] -->|DubboReference| B[Nacos Registry]
C[user-provider<br/>Spring Boot 3] -->|register instance| B
A -->|Triple / HTTP2 / Protobuf| C
D[grpcurl / Go / Node / Python gRPC Client] -->|gRPC compatible call| C
subgraph api[rpc-api]
P[user.proto]
G[Generated Java Message + Dubbo Stub]
end
api --> A
api --> C
1.1 为什么不用原生 gRPC Starter 作为主线? 原生 gRPC 在 Java 中当然可以直接用,比如 grpc-java 或 grpc-spring-boot-starter。但是一旦进入企业微服务,你马上会遇到这些问题:
服务注册发现怎么接?
多环境隔离怎么做?
超时、重试、负载均衡、版本、分组怎么统一治理?
Java 内部调用如何保持 Dubbo 生态?
以后要不要接 Dubbo Admin、路由规则、权重、标签路由?
如果你的系统本来就在 Dubbo 体系里,那直接用 Dubbo 3 Triple + Nacos 会顺很多。原生 gRPC 更适合:
多语言服务占主导;
服务治理靠 Kubernetes / Service Mesh / xDS / Envoy;
项目不需要 Dubbo 的生态能力;
对 Dubbo 完全没有历史包袱。
本文主线是:Spring Boot 3 + Dubbo 3 + Triple + Protobuf + Nacos 。
二、版本选择 本文示例版本如下:
组件
示例版本
说明
JDK
17+
Spring Boot 3 最低要求 Java 17
Spring Boot
3.5.x
本文使用 Spring Boot 3 系列
Dubbo
3.3.x
使用 Dubbo 3 的官方 Spring Boot Starter
Nacos Server
3.2.x 或 2.3.x+
本地已有 Nacos 3.2.x 可以使用;保守演示可用 2.3.x
Protobuf
3.25.x
用于 IDL 和消息生成
Maven
3.8+
Spring Boot 3 与 Dubbo 3 项目常规构建
建议固定版本,不要所有东西都写 latest。微服务版本管理不是买彩票,中奖概率不高,但踩坑概率挺稳定。
2.1 关于 Nacos 版本的说明 Dubbo 3.3 官方文档给出的 Nacos 推荐客户端版本是 2.3.0,兼容范围是 Nacos 2.x。Nacos 3.x 官方升级文档说明,Nacos 3.x Server 对 2.x Client 和 3.x Client 是兼容的,但不再兼容 0.x/1.x Client。
所以你有两种选择:
保守稳定版 :Dubbo 3.3.x + Nacos Server 2.3.x/2.4.x。
较新版 :Dubbo 3.3.x + Nacos Server 3.2.x,先在本地和测试环境验证注册发现、配置中心、鉴权、namespace、group。
本文配置以 nacos://127.0.0.1:8848 为例。
三、启动 Nacos 3.1 Docker 启动 Nacos 3.2.x 本地开发可以直接用 standalone 模式:
1 2 3 4 5 6 7 8 docker run -d \ --name nacos-standalone \ -e MODE=standalone \ -e NACOS_AUTH_ENABLE=false \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ nacos/nacos-server:v3.2.2
访问控制台:
1 http://127.0.0.1:8848/nacos
如果你开启鉴权:
1 2 3 4 5 6 7 8 9 10 11 docker run -d \ --name nacos-standalone \ -e MODE=standalone \ -e NACOS_AUTH_ENABLE=true \ -e NACOS_AUTH_TOKEN='your-base64-secret-token-must-be-long-enough' \ -e NACOS_AUTH_IDENTITY_KEY='serverIdentity' \ -e NACOS_AUTH_IDENTITY_VALUE='security' \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ nacos/nacos-server:v3.2.2
然后 Dubbo 注册地址要带上账号密码:
1 2 3 dubbo: registry: address: nacos://127.0.0.1:8848?username=nacos&password=nacos
3.2 Zip 包方式启动 如果你是 zip 包安装:
1 2 cd nacos/bin sh startup.sh -m standalone
停止:
3.3 端口说明
端口
作用
8848
Nacos 控制台、OpenAPI、客户端访问主端口
9848
Nacos 2.x/3.x 客户端 gRPC 通信端口
9849
Nacos 服务端集群间 gRPC 通信端口
本地 Docker 如果只暴露 8848,有时控制台能打开,但客户端注册发现会出问题。这个坑很常见,像插头只插了一半,看起来通电,实际上 CPU 已经在心里骂人。
四、工程结构 建议使用 Maven 多模块工程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 nacos-dubbo3-grpc-demo ├── pom.xml ├── rpc-api │ ├── pom.xml │ └── src/main/proto/user.proto ├── user-provider │ ├── pom.xml │ └── src/main/java/com/cybermario/demo/user │ ├── UserProviderApplication.java │ └── rpc/UserRpcServiceImpl.java └── order-consumer ├── pom.xml └── src/main/java/com/cybermario/demo/order ├── OrderConsumerApplication.java ├── controller/OrderController.java └── service/OrderService.java
为什么要把 proto 放到 rpc-api?
因为接口契约应该是独立模块,Provider 和 Consumer 都依赖它。这样能避免 Provider 和 Consumer 互相引用业务模块,后面做多语言接入时,也可以把 proto 文件单独发布给 Go、Node、Python、Rust 等客户端。
五、父工程 pom.xml 根目录 pom.xml:
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 <?xml version="1.0" encoding="UTF-8" ?> <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.cybermario.demo</groupId > <artifactId > nacos-dubbo3-grpc-demo</artifactId > <version > 1.0.0-SNAPSHOT</version > <packaging > pom</packaging > <modules > <module > rpc-api</module > <module > user-provider</module > <module > order-consumer</module > </modules > <properties > <java.version > 17</java.version > <spring-boot.version > 3.5.14</spring-boot.version > <dubbo.version > 3.3.6</dubbo.version > <protobuf.version > 3.25.5</protobuf.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 > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo-bom</artifactId > <version > ${dubbo.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <build > <pluginManagement > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.13.0</version > <configuration > <release > ${java.version}</release > </configuration > </plugin > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > ${spring-boot.version}</version > </plugin > </plugins > </pluginManagement > </build > </project >
注意两点:
Spring Boot 3 必须用 JDK 17+。
Dubbo 版本统一交给 dubbo-bom 管理,子模块里不要东一个 3.2.x、西一个 3.3.x。微服务最怕“版本自由发挥”,最后像乐队没有指挥,鼓手开始拉小提琴。
六、rpc-api:定义 Protobuf IDL 6.1 rpc-api/pom.xml 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 <?xml version="1.0" encoding="UTF-8" ?> <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.cybermario.demo</groupId > <artifactId > nacos-dubbo3-grpc-demo</artifactId > <version > 1.0.0-SNAPSHOT</version > </parent > <artifactId > rpc-api</artifactId > <packaging > jar</packaging > <dependencies > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo</artifactId > </dependency > <dependency > <groupId > com.google.protobuf</groupId > <artifactId > protobuf-java</artifactId > <version > ${protobuf.version}</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo-maven-plugin</artifactId > <version > ${dubbo.version}</version > <configuration > <dubboVersion > ${dubbo.version}</dubboVersion > <dubboGenerateType > tri</dubboGenerateType > <protoSourceDir > ${project.basedir}/src/main/proto</protoSourceDir > <outputDir > ${project.build.directory}/generated-sources/protobuf/java</outputDir > </configuration > <executions > <execution > <phase > generate-sources</phase > <goals > <goal > compile</goal > </goals > </execution > </executions > </plugin > <plugin > <groupId > org.codehaus.mojo</groupId > <artifactId > build-helper-maven-plugin</artifactId > <version > 3.6.0</version > <executions > <execution > <id > add-source</id > <phase > generate-sources</phase > <goals > <goal > add-source</goal > </goals > <configuration > <sources > <source > ${project.build.directory}/generated-sources/protobuf/java</source > </sources > </configuration > </execution > </executions > </plugin > </plugins > </build > </project >
如果你使用某些 Dubbo 版本时发现 dubbo-maven-plugin 没有生成消息类,可以切换到经典的 protobuf-maven-plugin + dubbo-compiler 方式。本文后面会给备用配置。
6.2 user.proto 文件位置:rpc-api/src/main/proto/user.proto
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 syntax = "proto3" ;package cybermario.user.v1;option java_multiple_files = true ;option java_package = "com.cybermario.demo.rpc.user" ;option java_outer_classname = "UserProto" ;service UserRpc { rpc GetUser (GetUserRequest) returns (GetUserReply) ; rpc CreateUser (CreateUserRequest) returns (CreateUserReply) ; }message GetUserRequest { string user_id = 1 ; }message GetUserReply { string user_id = 1 ; string username = 2 ; string nickname = 3 ; string mobile = 4 ; int64 created_at = 5 ; }message CreateUserRequest { string username = 1 ; string nickname = 2 ; string mobile = 3 ; }message CreateUserReply { string user_id = 1 ; bool success = 2 ; string message = 3 ; }
6.3 Proto 设计规范 几个非常重要的习惯:
字段编号一旦发布,不要随便改。
删除字段时,不要复用老字段编号。
新增字段要保证默认值安全。
RPC 方法尽量保持“一入参一出参”。
Java 包名和 proto package 分开设计。
示例:
1 2 3 4 5 6 7 8 9 message GetUserReply { string user_id = 1 ; string username = 2 ; string nickname = 3 ; reserved 4 ; reserved "mobile" ; int64 created_at = 5 ; }
6.4 编译 rpc-api 在根目录执行:
1 mvn clean install -pl rpc-api
成功后,你会在 target/generated-sources/protobuf/java 看到类似这些生成类:
1 2 3 4 5 com.cybermario.demo.rpc.user.GetUserRequest com.cybermario.demo.rpc.user.GetUserReply com.cybermario.demo.rpc.user.CreateUserRequest com.cybermario.demo.rpc.user.CreateUserReply com.cybermario.demo.rpc.user.UserRpcDubbo
不同 Dubbo 插件版本生成的服务接口名字可能略有不同。常见两类:
1 2 3 4 5 UserRpcDubbo.IUserRpc UserRpc
本文示例以后者不确定时以前者为主:UserRpcDubbo.IUserRpc。如果你的版本生成的是直接接口,把代码里的 UserRpcDubbo.IUserRpc 替换成 UserRpc 即可。
七、user-provider:服务提供方 7.1 user-provider/pom.xml 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 <?xml version="1.0" encoding="UTF-8" ?> <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.cybermario.demo</groupId > <artifactId > nacos-dubbo3-grpc-demo</artifactId > <version > 1.0.0-SNAPSHOT</version > </parent > <artifactId > user-provider</artifactId > <dependencies > <dependency > <groupId > com.cybermario.demo</groupId > <artifactId > rpc-api</artifactId > <version > ${project.version}</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <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 > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
7.2 application.yml 文件位置:user-provider/src/main/resources/application.yml
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 server: port: 8081 spring: application: name: user-provider management: endpoints: web: exposure: include: health,info,metrics logging: level: root: info org.apache.dubbo: info dubbo: application: name: user-provider logger: slf4j qos-enable: false protocol: name: tri port: 50051 serialization: protobuf registry: address: nacos://127.0.0.1:8848 register-mode: instance provider: timeout: 3000 retries: 0
如果 Nacos 开启鉴权:
1 2 3 dubbo: registry: address: nacos://127.0.0.1:8848?username=nacos&password=nacos
如果需要 namespace 隔离:
1 2 3 4 5 dubbo: registry: address: nacos://127.0.0.1:8848 parameters: namespace: dev-namespace-id
如果需要 group 隔离:
1 2 3 4 dubbo: registry: address: nacos://127.0.0.1:8848 group: DUBBO_DEV_GROUP
7.3 启动类 文件位置:user-provider/src/main/java/com/cybermario/demo/user/UserProviderApplication.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.cybermario.demo.user;import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@EnableDubbo(scanBasePackages = "com.cybermario.demo.user") @SpringBootApplication public class UserProviderApplication { public static void main (String[] args) { SpringApplication.run(UserProviderApplication.class, args); } }
@EnableDubbo 很关键。没加它,@DubboService 就像穿了隐身衣,代码在那儿,Dubbo 看不见。
7.4 实现 UserRpc 服务 文件位置:user-provider/src/main/java/com/cybermario/demo/user/rpc/UserRpcServiceImpl.java
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package com.cybermario.demo.user.rpc;import com.cybermario.demo.rpc.user.CreateUserReply;import com.cybermario.demo.rpc.user.CreateUserRequest;import com.cybermario.demo.rpc.user.GetUserReply;import com.cybermario.demo.rpc.user.GetUserRequest;import com.cybermario.demo.rpc.user.UserRpcDubbo;import java.time.Instant;import java.util.Map;import java.util.UUID;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ConcurrentHashMap;import org.apache.dubbo.config.annotation.DubboService;import org.apache.dubbo.rpc.RpcContext;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@DubboService( version = "1.0.0", group = "dev", timeout = 3000 ) public class UserRpcServiceImpl implements UserRpcDubbo .IUserRpc { private static final Logger log = LoggerFactory.getLogger(UserRpcServiceImpl.class); private final Map<String, UserRecord> userStore = new ConcurrentHashMap <>(); public UserRpcServiceImpl () { userStore.put("1001" , new UserRecord ("1001" , "mario" , "SuperMario" , "13800000001" , 1717776000000L )); userStore.put("1002" , new UserRecord ("1002" , "luigi" , "Luigi" , "13800000002" , 1717776001000L )); } @Override public GetUserReply getUser (GetUserRequest request) { String traceId = RpcContext.getServerAttachment().getAttachment("x-trace-id" ); log.info("receive getUser request, userId={}, traceId={}" , request.getUserId(), traceId); UserRecord record = userStore.get(request.getUserId()); if (record == null ) { return GetUserReply.newBuilder() .setUserId(request.getUserId()) .setUsername("unknown" ) .setNickname("用户不存在" ) .setMobile("" ) .setCreatedAt(0L ) .build(); } return GetUserReply.newBuilder() .setUserId(record.userId()) .setUsername(record.username()) .setNickname(record.nickname()) .setMobile(record.mobile()) .setCreatedAt(record.createdAt()) .build(); } @Override public CompletableFuture<GetUserReply> getUserAsync (GetUserRequest request) { return CompletableFuture.completedFuture(getUser(request)); } @Override public CreateUserReply createUser (CreateUserRequest request) { String userId = UUID.randomUUID().toString().replace("-" , "" ); long now = Instant.now().toEpochMilli(); userStore.put(userId, new UserRecord ( userId, request.getUsername(), request.getNickname(), request.getMobile(), now )); return CreateUserReply.newBuilder() .setUserId(userId) .setSuccess(true ) .setMessage("create user success" ) .build(); } @Override public CompletableFuture<CreateUserReply> createUserAsync (CreateUserRequest request) { return CompletableFuture.completedFuture(createUser(request)); } private record UserRecord ( String userId, String username, String nickname, String mobile, long createdAt ) { } }
如果你的 Dubbo 插件生成的是直接接口 UserRpc,那实现类改成:
1 2 3 public class UserRpcServiceImpl implements UserRpc { }
八、order-consumer:服务消费方 8.1 order-consumer/pom.xml 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 <?xml version="1.0" encoding="UTF-8" ?> <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.cybermario.demo</groupId > <artifactId > nacos-dubbo3-grpc-demo</artifactId > <version > 1.0.0-SNAPSHOT</version > </parent > <artifactId > order-consumer</artifactId > <dependencies > <dependency > <groupId > com.cybermario.demo</groupId > <artifactId > rpc-api</artifactId > <version > ${project.version}</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <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 > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
8.2 application.yml 文件位置:order-consumer/src/main/resources/application.yml
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 server: port: 8082 spring: application: name: order-consumer management: endpoints: web: exposure: include: health,info,metrics logging: level: root: info org.apache.dubbo: info dubbo: application: name: order-consumer logger: slf4j qos-enable: false protocol: name: tri port: -1 serialization: protobuf registry: address: nacos://127.0.0.1:8848 register-mode: instance parameters: register-consumer-url: true consumer: check: false timeout: 3000 retries: 0
consumer.check=false 的意思是:启动时即使 Provider 暂时不存在,Consumer 也先启动成功。开发环境非常实用。生产环境则要根据系统要求决定,有些核心服务应该启动强校验,避免上线后才发现依赖没了。
8.3 启动类 文件位置:order-consumer/src/main/java/com/cybermario/demo/order/OrderConsumerApplication.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.cybermario.demo.order;import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@EnableDubbo(scanBasePackages = "com.cybermario.demo.order") @SpringBootApplication public class OrderConsumerApplication { public static void main (String[] args) { SpringApplication.run(OrderConsumerApplication.class, args); } }
8.4 OrderService 文件位置:order-consumer/src/main/java/com/cybermario/demo/order/service/OrderService.java
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 package com.cybermario.demo.order.service;import com.cybermario.demo.rpc.user.GetUserReply;import com.cybermario.demo.rpc.user.GetUserRequest;import com.cybermario.demo.rpc.user.UserRpcDubbo;import java.util.LinkedHashMap;import java.util.Map;import java.util.UUID;import org.apache.dubbo.config.annotation.DubboReference;import org.apache.dubbo.rpc.RpcContext;import org.springframework.stereotype.Service;@Service public class OrderService { @DubboReference( version = "1.0.0", group = "dev", check = false, timeout = 3000, retries = 0 ) private UserRpcDubbo.IUserRpc userRpc; public Map<String, Object> getOrderDetail (String userId) { String traceId = UUID.randomUUID().toString().replace("-" , "" ); RpcContext.getClientAttachment().setAttachment("x-trace-id" , traceId); GetUserRequest request = GetUserRequest.newBuilder() .setUserId(userId) .build(); GetUserReply user = userRpc.getUser(request); Map<String, Object> result = new LinkedHashMap <>(); result.put("orderId" , "ORDER-" + System.currentTimeMillis()); result.put("traceId" , traceId); result.put("userId" , user.getUserId()); result.put("username" , user.getUsername()); result.put("nickname" , user.getNickname()); result.put("mobile" , user.getMobile()); result.put("createdAt" , user.getCreatedAt()); return result; } }
8.5 OrderController 文件位置:order-consumer/src/main/java/com/cybermario/demo/order/controller/OrderController.java
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 package com.cybermario.demo.order.controller;import com.cybermario.demo.order.service.OrderService;import java.util.Map;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;@RestController @RequestMapping("/orders") public class OrderController { private final OrderService orderService; public OrderController (OrderService orderService) { this .orderService = orderService; } @GetMapping("/{userId}") public Map<String, Object> getOrder (@PathVariable String userId) { return orderService.getOrderDetail(userId); } }
九、启动与验证 9.1 编译整个项目 在根目录执行:
9.2 启动 Provider 1 mvn -pl user-provider spring-boot:run
看到类似日志:
1 2 3 DubboBootstrap has started. Export dubbo service com.cybermario.demo.rpc.user.UserRpc... Register instance user-provider to nacos...
到 Nacos 控制台查看服务列表,应该能看到:
如果 register-mode=all,可能还能看到接口级服务名。
9.3 启动 Consumer 1 mvn -pl order-consumer spring-boot:run
9.4 通过 HTTP 调用 Consumer 1 curl http://127.0.0.1:8082/orders/1001
返回示例:
1 2 3 4 5 6 7 8 9 { "orderId" : "ORDER-1718888888888" , "traceId" : "0db82df650104f4ca93c5f4df8b1914a" , "userId" : "1001" , "username" : "mario" , "nickname" : "SuperMario" , "mobile" : "13800000001" , "createdAt" : 1717776000000 }
此时调用链路是:
1 浏览器/curl -> order-consumer HTTP Controller -> DubboReference -> Nacos 发现 user-provider -> Triple RPC -> user-provider
十、使用 grpcurl 直接调用 Dubbo Triple 服务 安装 grpcurl:
因为我们没有开启 gRPC Server Reflection,所以要指定 proto 文件:
1 2 3 4 5 6 7 grpcurl \ -plaintext \ -import-path rpc-api/src/main/proto \ -proto user.proto \ -d '{"user_id":"1001"}' \ 127.0.0.1:50051 \ cybermario.user.v1.UserRpc/GetUser
返回类似:
1 2 3 4 5 6 7 { "userId" : "1001" , "username" : "mario" , "nickname" : "SuperMario" , "mobile" : "13800000001" , "createdAt" : "1717776000000" }
创建用户:
1 2 3 4 5 6 7 grpcurl \ -plaintext \ -import-path rpc-api/src/main/proto \ -proto user.proto \ -d '{"username":"peach","nickname":"Princess Peach","mobile":"13800000003"}' \ 127.0.0.1:50051 \ cybermario.user.v1.UserRpc/CreateUser
这一步非常关键,它证明 Provider 虽然是 Dubbo 3 服务,但可以通过 gRPC 生态工具访问。
十一、经典 protobuf-maven-plugin 备用配置 如果你的 Dubbo 版本中 dubbo-maven-plugin 行为不符合预期,可以在 rpc-api 使用经典配置:
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 <build > <extensions > <extension > <groupId > kr.motd.maven</groupId > <artifactId > os-maven-plugin</artifactId > <version > 1.7.1</version > </extension > </extensions > <plugins > <plugin > <groupId > org.xolstice.maven.plugins</groupId > <artifactId > protobuf-maven-plugin</artifactId > <version > 0.6.1</version > <configuration > <protocArtifact > com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} </protocArtifact > <protocPlugins > <protocPlugin > <id > dubbo</id > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo-compiler</artifactId > <version > ${dubbo.version}</version > <mainClass > org.apache.dubbo.gen.tri.Dubbo3TripleGenerator</mainClass > </protocPlugin > </protocPlugins > </configuration > <executions > <execution > <goals > <goal > compile</goal > <goal > compile-custom</goal > </goals > </execution > </executions > </plugin > </plugins > </build >
这个方式会:
compile:生成 Protobuf Java 消息类。
compile-custom:调用 Dubbo Triple Generator 生成 Dubbo Stub。
如果 IDE 找不到生成类,记得把生成目录标记为 Generated Sources Root,或者执行:
十二、Java Interface + Triple 的简化方案 如果你只是 Java 内部服务互调,不需要跨语言,不需要标准 gRPC 客户端访问,也可以不用 Protobuf,直接使用 Java 接口。
12.1 api 定义 1 2 3 4 5 package com.cybermario.demo.api;public interface HelloService { String sayHello (String name) ; }
12.2 Provider 1 2 3 4 5 6 7 @DubboService(version = "1.0.0") public class HelloServiceImpl implements HelloService { @Override public String sayHello (String name) { return "hello " + name; } }
12.3 Consumer 1 2 @DubboReference(version = "1.0.0") private HelloService helloService;
12.4 配置 Triple 1 2 3 4 dubbo: protocol: name: tri port: 50051
这个方案非常适合 Dubbo2 老系统迁移到 Dubbo3 Triple:改协议配置即可开始往 Triple 迁移。但它不适合拿给 Go、Node、Python 原生 gRPC 客户端直接调用,因为没有 .proto 契约。
选型建议:
场景
推荐方案
纯 Java 内部服务
Java Interface + Triple
有跨语言需求
Protobuf IDL + Triple
要用 grpcurl / Go / Node 调用
Protobuf IDL + Triple
老 Dubbo2 平滑迁移
Java Interface + Triple 起步,再逐步 IDL 化
十三、Dubbo 配置详解 13.1 application.name 1 2 3 dubbo: application: name: user-provider
Dubbo3 应用级服务发现中,应用名就是注册到 Nacos 的服务名。这个名字要稳定,不要每次构建都带随机后缀。
13.2 protocol.name = tri 1 2 3 dubbo: protocol: name: tri
tri 表示 Dubbo Triple 协议。它基于 HTTP/2,兼容 gRPC。
13.3 protocol.port 1 2 3 dubbo: protocol: port: 50051
Provider 端建议固定端口,方便排查、压测、网关暴露、Kubernetes Service 映射。Consumer 端一般写 -1,随机端口即可。
13.4 registry.address 1 2 3 dubbo: registry: address: nacos://127.0.0.1:8848
Dubbo 通过 Nacos 做注册发现。Provider 注册自己,Consumer 订阅 Provider 地址。
13.5 register-mode 1 2 3 dubbo: registry: register-mode: instance
常见取值:
值
说明
instance
应用级注册,Dubbo3 推荐方向
interface
接口级注册,兼容 Dubbo2 老模型
all
应用级 + 接口级都注册
老系统迁移期间可以用 all,新系统建议优先 instance。
13.6 group 和 version Provider:
1 @DubboService(group = "dev", version = "1.0.0")
Consumer:
1 @DubboReference(group = "dev", version = "1.0.0")
group 通常用于环境、租户、业务域隔离;version 用于接口版本隔离。两边必须匹配,不然 Consumer 会找不到 Provider。
13.7 timeout 和 retries 1 2 3 4 5 6 7 dubbo: provider: timeout: 3000 retries: 0 consumer: timeout: 3000 retries: 0
对写操作,默认建议 retries=0,否则可能出现重复下单、重复扣款、重复创建数据。读操作可以根据幂等性设置重试。
十四、链路上下文传递 Consumer 设置附件:
1 2 String traceId = UUID.randomUUID().toString().replace("-" , "" ); RpcContext.getClientAttachment().setAttachment("x-trace-id" , traceId);
Provider 读取附件:
1 String traceId = RpcContext.getServerAttachment().getAttachment("x-trace-id" );
这个方式适合传递:
traceId
requestId
tenantId
operatorId
grayTag
language
clientVersion
但不要把大对象塞进 attachment。RPC 上下文不是移动硬盘,别把它当仓库用。
十五、异常处理建议 Dubbo 调用常见异常可以按层分:
异常类型
常见原因
处理建议
No provider available
Provider 未启动、注册失败、group/version 不匹配
看 Nacos 服务列表和 Dubbo 配置
TimeoutException
Provider 慢、网络慢、线程池耗尽
加日志、看耗时、调 timeout、优化业务
SerializationException
Protobuf 类不一致、字段类型变更不兼容
保持 api 包版本一致,不复用字段编号
NacosException
Nacos 地址错误、鉴权错误、9848 未开放
检查 address、username/password、端口
RpcException
业务异常或网络异常包装
统一转换错误码,不要直接暴露堆栈
建议在业务层封装远程调用:
1 2 3 4 5 6 7 8 9 10 11 12 public GetUserReply safeGetUser (String userId) { try { return userRpc.getUser(GetUserRequest.newBuilder().setUserId(userId).build()); } catch (Exception ex) { return GetUserReply.newBuilder() .setUserId(userId) .setUsername("fallback" ) .setNickname("用户服务暂不可用" ) .build(); } }
注意:不是所有远程调用都应该降级。比如支付、库存扣减、合同签署这类关键链路,乱降级比失败更危险。
十六、生产配置建议 16.1 Provider 端建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 dubbo: provider: timeout: 3000 retries: 0 executes: 200 protocol: name: tri port: 50051 serialization: protobuf registry: address: nacos://nacos.prod:8848?username=${NACOS_USERNAME}&password=${NACOS_PASSWORD} register-mode: instance parameters: namespace: ${NACOS_NAMESPACE}
16.2 Consumer 端建议 1 2 3 4 5 6 7 8 9 10 11 dubbo: consumer: timeout: 3000 retries: 0 check: true registry: address: nacos://nacos.prod:8848?username=${NACOS_USERNAME}&password=${NACOS_PASSWORD} register-mode: instance parameters: namespace: ${NACOS_NAMESPACE}
16.3 环境变量注入 1 2 3 export NACOS_USERNAME=nacosexport NACOS_PASSWORD=nacosexport NACOS_NAMESPACE=prod-namespace-id
application.yml:
1 2 3 4 5 dubbo: registry: address: nacos://nacos.prod:8848?username=${NACOS_USERNAME}&password=${NACOS_PASSWORD} parameters: namespace: ${NACOS_NAMESPACE}
不要把生产密码写死在 Git 仓库里。否则以后安全审计来了,你的 commit history 会像犯罪现场一样诚实。
十七、Nacos 控制台怎么看 启动 Provider 后,在 Nacos 控制台进入:
你可能看到:
如果配置了 register-mode=all,还可能看到接口级服务。
重点看:
服务是否存在;
实例 IP 是否正确;
端口是否是 Dubbo Triple 端口,而不是 Spring Boot Web 端口;
分组是否正确;
命名空间是否正确;
实例是否健康。
如果本机有多网卡、Docker、VPN,Provider 注册 IP 可能不对。可以指定:
1 2 3 dubbo: protocol: host: 127.0 .0 .1
生产不要随便写死 127.0.0.1,否则 Consumer 会发现一个“只会指向自己”的 Provider,像问路问到了镜子。
十八、多 Provider 负载均衡 启动两个 Provider:
1 2 3 mvn -pl user-provider spring-boot:run -Dspring-boot.run.arguments="--server.port=8081 --dubbo.protocol.port=50051" mvn -pl user-provider spring-boot:run -Dspring-boot.run.arguments="--server.port=8083 --dubbo.protocol.port=50052"
Consumer 配置负载均衡:
1 2 3 4 5 6 7 8 @DubboReference( version = "1.0.0", group = "dev", loadbalance = "roundrobin", timeout = 3000, retries = 0 ) private UserRpcDubbo.IUserRpc userRpc;
常见负载均衡策略:
策略
说明
random
随机,默认常用
roundrobin
轮询
leastactive
最少活跃调用数
consistenthash
一致性哈希,适合有状态或缓存亲和场景
十九、灰度与版本治理 19.1 通过 version 做接口版本隔离 Provider V1:
1 2 3 @DubboService(version = "1.0.0", group = "dev") public class UserRpcV1ServiceImpl implements UserRpcDubbo .IUserRpc { }
Provider V2:
1 2 3 @DubboService(version = "2.0.0", group = "dev") public class UserRpcV2ServiceImpl implements UserRpcDubbo .IUserRpc { }
Consumer 指定版本:
1 2 @DubboReference(version = "2.0.0", group = "dev") private UserRpcDubbo.IUserRpc userRpc;
19.2 通过 group 做环境隔离 开发环境:
1 @DubboService(group = "dev")
测试环境:
1 @DubboService(group = "test")
生产环境:
1 @DubboService(group = "prod")
一般来说,环境隔离更推荐使用 Nacos namespace;业务分组或特殊灰度可以使用 Dubbo group。不要把所有隔离能力都揉成一坨,不然后面排查问题时就像拆耳机线,越急越乱。
二十、Dubbo Triple 与原生 gRPC 的边界 20.1 Dubbo Triple 能做什么
使用 HTTP/2 传输。
使用 Protobuf IDL。
兼容 gRPC 客户端访问。
接入 Dubbo 注册发现、负载均衡、超时、重试、路由、分组、版本。
适合 Java Dubbo 体系与多语言体系并存的团队。
20.2 原生 gRPC 能做什么
使用 Google gRPC 标准栈。
生态中有非常多语言 SDK。
Kubernetes、Envoy、xDS、Service Mesh 体系更常见。
不依赖 Dubbo。
20.3 不建议混用的方式 不要在同一个服务里随意同时开启:
一个 Dubbo Triple 端口;
一个原生 gRPC 端口;
一个 Dubbo 协议端口;
一个 Spring Web 端口;
然后所有端口都想注册到一个 Nacos 服务里。
不是不能做,而是治理复杂度会暴涨。除非你非常明确每个端口的职责,否则先收敛:内部 Dubbo 调用 + 外部 gRPC 兼容访问,都走 Triple。
二十一、常见问题排查 21.1 Nacos 控制台看不到服务 检查:
1 2 3 dubbo: registry: address: nacos://127.0.0.1:8848
检查 Provider 是否有:
1 2 @EnableDubbo @DubboService
检查启动日志是否有:
1 Register instance user-provider to nacos
21.2 Consumer 报 No provider available 重点检查:
Provider 是否启动;
Nacos 服务是否健康;
Consumer 和 Provider 的 namespace 是否一致;
group 是否一致;
version 是否一致;
接口包是否是同一个 rpc-api 版本;
register-mode 是否导致订阅模型不匹配。
21.3 grpcurl 调不通 检查 Provider 的 Dubbo 协议端口:
1 2 3 dubbo: protocol: port: 50051
检查命令是否带 proto:
1 2 3 4 5 6 grpcurl -plaintext \ -import-path rpc-api/src/main/proto \ -proto user.proto \ -d '{"user_id":"1001"}' \ 127.0.0.1:50051 \ cybermario.user.v1.UserRpc/GetUser
如果你直接执行:
1 grpcurl -plaintext 127.0.0.1:50051 list
可能失败,因为服务端没有开启 reflection。没有 reflection 时,grpcurl 不知道服务有哪些方法,必须给它 .proto 文件。
21.4 generated-sources 下面有代码,但 IDE 报红 执行:
然后在 IDEA 中:
1 右键 target/generated-sources/protobuf/java -> Mark Directory as -> Generated Sources Root
21.5 Protobuf 字段没有值 检查 JSON 字段名和 proto 字段名:
grpcurl JSON 入参应该写:
而 Java 代码里是:
21.6 Nacos 连接失败 Docker 下检查端口:
需要看到:
1 2 0.0.0.0:8848->8848/tcp 0.0.0.0:9848->9848/tcp
再检查日志:
1 docker logs -f nacos-standalone
二十二、生产落地清单 上线前建议确认:
二十三、完整启动顺序 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 sh nacos/bin/startup.sh -m standalone mvn clean install -pl rpc-api mvn clean install mvn -pl user-provider spring-boot:run mvn -pl order-consumer spring-boot:run curl http://127.0.0.1:8082/orders/1001 grpcurl \ -plaintext \ -import-path rpc-api/src/main/proto \ -proto user.proto \ -d '{"user_id":"1001"}' \ 127.0.0.1:50051 \ cybermario.user.v1.UserRpc/GetUser
二十四、总结 本文完成了一个完整的 Nacos + Spring Boot 3 + Dubbo 3 + gRPC/Triple 整合实践:
用 Spring Boot 3 管理应用启动与 Web 调试入口;
用 Dubbo 3 负责 RPC、注册发现、服务治理;
用 Nacos 作为注册中心;
用 Triple 协议承载 HTTP/2 + Protobuf;
用 Protobuf IDL 定义跨语言契约;
用 grpcurl 验证 gRPC 兼容访问。
我的建议很直接:
如果你已经在 Dubbo 体系里,又想要 gRPC/Protobuf 的跨语言能力,优先选择 Dubbo 3 Triple + Protobuf IDL + Nacos。它不会让你丢掉 Dubbo 的治理能力,也能向 gRPC 生态靠拢。
如果你的系统完全是 Kubernetes + Envoy + 多语言原生 gRPC,那可以直接走原生 gRPC,不必强行套 Dubbo。
技术选型不怕复杂,怕的是边界不清。边界一清楚,复杂度就从“玄学现场”变成“工程问题”了。
参考资料
启示录 RPC 的本质不是远程调用,而是边界治理。
协议只是路,契约才是秩序。