一、工程结构:为什么要拆三个模块
整个项目拆成三个 Maven 子模块,由一个父 POM 聚合管理:
dubbo-demo(父工程,packaging=pom) ├── dubbo-api → 接口契约层 ├── dubbo-provider → 服务提供者 └── dubbo-consumer → 服务消费者
父工程的职责只有两件事:统一管理子模块列表(<modules>)和统一锁定依赖版本(<dependencyManagement>)。它本身不产出任何 JAR,packaging必须声明为pom。
dubbo-api 独立成模块是 Dubbo 微服务的核心设计思想。接口定义和传输实体单独一个工程,Provider 和 Consumer 都依赖它。这样做的好处是:两端通过"共享契约"解耦,Provider 的实现细节对 Consumer 完全不可见;未来如果有新的消费方,只要引入这个 API JAR 就能调用服务。
二、三个核心注解
整个 Dubbo 集成只需要记住三个注解:
@EnableDubbo— 贴在启动类上。它是一切的开关,作用是触发 Dubbo 的自动配置,扫描当前包及子包下的 Dubbo 注解。Provider 和 Consumer 的启动类上都要加。没有它,@DubboService和@DubboReference都不会生效。
@DubboService— 贴在Provider 端的接口实现类上。作用是告诉 Dubbo"把这个类作为服务暴露出去,注册到注册中心"。它可以附带多个属性,最常用的是version(版本号)和loadbalance(负载均衡建议)。一个接口可以有多个实现类,通过不同的version区分。
@DubboReference— 贴在Consumer 端的字段上。作用是告诉 Dubbo"帮我生成这个接口的远程代理,从注册中心找到对应的 Provider 来调用"。它是 RPC 调用的入口,使用体验和注入一个本地 Bean 一样——字段声明后直接调方法,底层的网络通信完全透明。
容易混淆的点:@DubboService不是 Spring 的@Service,@DubboReference也不是 Spring 的@Autowired。它们是 Dubbo 自己的注解,走的是 Dubbo 的注册发现机制,不是 Spring 容器的 Bean 注入。
三、版本路由:同一接口多个实现共存
Dubbo 允许同一个接口在 Provider 端注册多个版本。本项目中UserService同时有 v1.0 和 v2.0 两个实现:
Provider 端:两个实现类分别用
version = "1.0"和version = "2.0"标注Consumer 端:通过
@DubboReference(version = "1.0")精确指定调哪个版本
如果 Consumer 端写version = "*",则表示"我不挑版本",Dubbo 会在所有可用版本中做负载均衡。
实际应用场景:灰度发布。新版本上线时,先部署一个 v2.0 的 Provider 实例,让少量流量通过version = "*"打过去观察效果,确认无误后再全量切换。
四、负载均衡策略
当同一个服务有多个 Provider 实例时(比如部署了 3 台机器),Consumer 调用时需要决定请求发给谁。Dubbo 内置了五种策略:
random(随机):默认策略,适合机器性能差不多的场景
roundrobin(轮询):请求轮流分发,保证绝对均匀
leastactive(最少活跃数):哪台机器当前处理的请求最少就调哪台,适合处理耗时差异大的场景
consistenthash(一致性哈希):相同参数的请求始终落到同一台机器,适合有缓存的场景
shortestresponse(最短响应时间):优先调用响应最快的机器
配置方式是在@DubboService或@DubboReference中设置loadbalance属性。
五、配置优先级规则(很重要)
Dubbo 中 Provider 和 Consumer 两端都可以配置timeout、retries、loadbalance等参数,但它们的优先级是不同的:
Consumer 端配置 > Provider 端配置 > Dubbo 全局默认值
也就是说,Provider 端在@DubboService上设的loadbalance = "random"只是一个"建议值"。如果 Consumer 端在@DubboReference上也设了loadbalance = "roundrobin",最终以 Consumer 为准。
设计意图是:服务的提供方提供"推荐配置",但消费方可以根据自己的业务需求覆盖。比如某个消费方对延迟特别敏感,它可以单独把timeout调短,而不影响其他消费方。
六、两个端口,别搞混
Provider 启动后实际监听了两个端口:
Spring Boot HTTP 端口(如
server.port: 8081):处理 HTTP 请求,比如 actuator 健康检查、REST 接口等Dubbo 协议端口(如
dubbo.protocol.port: 20880):处理 RPC 调用,Consumer 通过这个端口与 Provider 通信
Consumer 端只需要 HTTP 端口(对外暴露 REST 接口),不需要 Dubbo 协议端口,所以配置为port: -1。
七、注册中心的角色
ZooKeeper 在整个架构中充当的是"服务电话簿":
Provider 启动时:把自己的地址、接口名、版本号等信息写入 ZK 的节点上(注册)
Consumer 启动时:到 ZK 上查询自己需要的服务有哪些可用的 Provider 地址(订阅)
运行时:如果某个 Provider 下线,ZK 会通知所有订阅者更新地址列表(感知变化)
关键配置项dubbo.consumer.check: false的含义是:Consumer 启动时不强制要求 Provider 已经就位。设为true(默认值)时,如果找不到 Provider,Consumer 启动会直接报错。开发阶段建议设为false,生产环境视情况而定。
八、序列化的硬性要求
Dubbo RPC 调用时,参数和返回值需要经过序列化(对象 → 字节流)通过网络传输,然后在对端反序列化(字节流 → 对象)。因此所有在接口中出现的实体类都必须实现Serializable接口,并且最好显式声明serialVersionUID。
如果忘了实现Serializable,运行时调用会直接抛序列化异常。这也是为什么实体类放在 dubbo-api 模块的原因之一:两端必须使用完全相同的类定义,否则反序列化会失败。
另外 Dubbo 3.3 引入了序列化安全检查机制(serialize-check-status),默认会校验反序列化的类是否在信任白名单中。开发阶段设为WARN只打日志不阻断,生产环境建议配置信任列表。
九、Maven BOM 的管理思路
父 POM 通过<dependencyManagement>导入了两个 BOM(Bill of Materials):
Spring Boot BOM:锁定所有 Spring 相关依赖的版本
Dubbo BOM:锁定所有 Dubbo 相关依赖的版本
子模块引入依赖时不需要写<version>标签,版本号由父 POM 统一管理。这种方式的好处是避免了版本冲突,也方便日后统一升级——只需改父 POM 里的一个版本号。
特别注意 ZooKeeper 客户端的选择:ZK 3.8+ 要用dubbo-zookeeper-curator5-spring-boot-starter(Curator 5),不要用老的不带5的 Starter,否则会有兼容性问题。
十、Spring Boot 3 的特殊注意点
Spring Boot 3 基于 Spring Framework 6 和 Jakarta EE 9+,有几个容易踩的坑:
JDK 最低要求 17,不再支持 JDK 8/11
编译器必须开启
-parameters选项(即maven-compiler-plugin配置<parameters>true</parameters>),否则@RequestParam、@PathVariable等注解在运行时无法解析参数名,会报绑定错误包名从
javax.*迁移到jakarta.*,如果你在项目中直接用了 Servlet API 等,需要注意 import 路径的变化
十一、本地存根 (Stub) 和 本地伪装 (Mock),两者代码都是在消费者这边
Stub 是为了**“优化”(能不能不调?能不能先查缓存?),它是主动**的拦截器。
Mock是为了**“保命”(提供者挂了怎么办?降级使用),它是被动**的备胎。
@DubboReference(version = "1.0", loadbalance = "roundrobin", stub = "com.example.dubbo.consumer.stub.UserServiceStub", mock ="com.example.dubbo.consumer.mock.UserService_mock" ) private UserService userServiceAny;速记口诀
API 定契约,两端都依赖; Service 贴提供,Reference 贴消费; EnableDubbo 是开关,启动类上不能忘; 版本号控灰度,星号通配全都上; 负载均衡五策略,Consumer 说了算; 实体必须序列化,不然传输全白搭。