概述
入门案例拆成六七篇讲的时候,每篇都只解决一个问题,看完很容易“每步都懂、串不起来”。这篇不复述细节,只做一件事:把 Nacos 安装、父子工程划分、接口抽取、提供者暴露、消费者引用这一整条链路压成一张可照抄的 checklist,再配一张全景调用时序图,让你下次从零搭一个 Dubbo 工程时不用回翻视频。
纲要
- 案例目标与四个角色:Provider / Consumer / Registry / Monitor
- 从零到跑通 checklist(核心产出):七步,每步标注“拿什么证据证明这步成了”
- 实读工程结构与三模块依赖方向
- 一次完整调用的全景时序图(前面各篇是分镜头,这里是全景)
- 两端配置对照表,重点讲“消费端为什么不配 protocol”
- 注解清单:
@DubboService/@DubboReference/@EnableDubbo/@Method - 常见问题速查表:把前几篇踩过的坑浓缩成排查表
- 本案例的局限与下一步
案例目标与四个角色
需求很朴素:根据用户 id 查询用户。但 Dubbo 把它拆成了分布式的四方协作。
| 角色 | 类比 Spring MVC 的位置 | 在本案例中是谁 |
|---|---|---|
| Provider 服务提供者 | 传统 Service 层 | user-provider,暴露UserService |
| Consumer 服务消费者 | 传统 Controller 层 | user-consumer,通过UserController调用 |
| Registry 注册中心 | — | Nacos(127.0.0.1:8848) |
| Monitor 监控中心 | — | Dubbo Admin 管理控制台(localhost:8080),统计调用次数与耗时 |
提供者要做的动作叫暴露服务,消费者要做的动作叫引用服务。整个入门案例从头到尾就是在把这两个动作拆开、讲清楚、再拼回去。
从零到跑通 checklist
这张表是本文的主干。顺序不能乱:注册中心没起,后面所有验证都是无效的。
| # | 做了什么 | 关键文件 / 配置 | 怎么验证这步真的成了 |
|---|---|---|---|
| ① | 装 Nacos 与 Dubbo Admin | Nacos 解压到无中文路径,bin下执行startup.cmd -m standalone | 浏览器打开http://127.0.0.1:8848/nacos能登录;http://localhost:8080打开 Dubbo Admin |
| ② | 建父工程与模块划分 | 父pom.xml设packaging=pom,modules列出user-api/user-provider/user-consumer | IDEA Maven 面板下出现三个子模块,mvn -q compile不报缺模块 |
| ③ | 写接口与实体(api 模块) | UserService接口 +User实体(implements Serializable) | mvn -pl user-api install成功,本地仓库出现user-api-1.0-SNAPSHOT.jar |
| ④ | Provider:引依赖 → 暴露 → 配 yml → 启动 | dubbo-spring-boot-starter、dubbo-registry-nacos、@DubboService、dubbo.protocol/registry/scan | 启动日志出现export dubbo service cn.mf.user.service.UserService与Tomcat started on port(s): 18080;Nacos 服务列表能看到providers:cn.mf.user.service.UserService:1.0.0:20881 |
| ⑤ | Consumer:引依赖 + 依赖 api → 引用 → 配 yml → 调用 | @DubboReference,yml 只配registry(不配 protocol) | 访问http://localhost:18081/user/1返回用户 JSON;启动日志里能看到对注册中心的订阅动作 |
| ⑥ | 抽取公共接口模块 | 接口与 domain 全部下沉到user-api,两侧<dependency>指向它 | 删除任一端的本地重复接口后仍能编译,依赖方向变成单向 |
| ⑦ | 序列化检查 | 实体implements Serializable,两端类结构(包名、字段)一致 | 调用返回对象而非抛SerializationException |
几个容易漏的实读细节,顺手记一下:
- 父 pom 的
<modules>里写得比实际多——它声明了user-server,但工程里没有这个目录,留着会导致 Maven 直接报“找不到模块”。按实际存在的三个模块改。 - 讲义里提供者的
server.port写成了18081,和消费端撞号了。以实读工程为准:提供者18080,消费者18081,Dubbo 协议端口20881。两个 Spring Boot 应用不能占同一个 HTTP 端口。 - 提供者没写
@MapperScan,靠@SpringBootApplication从cn.mf.user往下扫到cn.mf.user.mapper里的@Mapper接口;dubbo.scan.base-packages填cn.mf.user.service,子包impl同样被扫到。
实读工程结构
dubbo-demo/ # 父工程,packaging=pom ├── pom.xml # 统一版本:Spring Boot 2.3.9、Dubbo 2.7.8 ├── user-api/ # 公共契约模块,只有两个类 │ ├── pom.xml │ └── src/main/java/cn/mf/user/ │ ├── service/UserService.java # 接口:queryUsername / queryById │ └── domain/User.java # 实体:implements Serializable ├── user-provider/ # 提供者 │ ├── pom.xml # 依赖 user-api + mybatis + mysql │ └── src/main/ │ ├── java/cn/mf/user/ │ │ ├── UserProviderApplication.java │ │ ├── mapper/UserMapper.java │ │ └── service/impl/ │ │ ├── UserServiceImpl.java # @DubboService(version = "1.0.0") │ │ └── UserServiceImpl2.java # @DubboService(version = "2.0.0") │ └── resources/application.yml # protocol 20881 / server 18080 └── user-consumer/ # 消费者 ├── pom.xml # 依赖 user-api + dubbo starter └── src/main/ ├── java/cn/mf/user/ │ ├── UserConsumerApplication.java │ └── controller/UserController.java # @DubboReference(version = "2.0.0") └── resources/application.yml # 只配 registry,无 protocol依赖方向是关键:user-provider与user-consumer都单向依赖user-api,两端之间没有任何直接依赖。接口与实体的唯一副本在 api 模块里,这解决的正是“两端各抄一份接口,字段一改就漏”的问题。
packagecn.mf.user.service;importcn.mf.user.domain.User;publicinterfaceUserService{StringqueryUsername(Longid);UserqueryById(Longid);}packagecn.mf.user.domain;importlombok.Data;importjava.io.Serializable;@DatapublicclassUserimplementsSerializable{privateLongid;privateStringusername;privateStringaddress;}一次调用的全景链路
各篇拆开讲的时候,注册、订阅、序列化、负载均衡是分开的。把它们合到一张图里,链路就清楚了。
两点值得盯着看:
- 订阅是拉,推送是长连接。消费者启动时向注册中心订阅自己需要的服务,注册中心返回地址列表;之后地址一变,注册中心走长连接把变更推过来,消费者不用重启。
- 选谁是消费者决定的。地址列表拿到手后,由消费者按软负载算法挑一台;本案例
@DubboReference(loadbalance = "Random")用的就是默认的按权重随机。这和 Spring Cloud 里 Ribbon 的客户端负载均衡是同一个思路。
两端配置对照
| 配置项 | Provider | Consumer | 说明 |
|---|---|---|---|
dubbo.application.name | 建议配 | 建议配 | 本工程两端都配了user-provider/user-consumer,便于在注册中心与监控里区分来源 |
dubbo.protocol.name | 必须 | 不配 | 提供者用dubbo协议监听端口 |
dubbo.protocol.port | 必须(20881) | 不配 | 消费端不监听 Dubbo 端口,只发起调用 |
dubbo.registry.address | 必须 | 必须 | 两端都填nacos://127.0.0.1:8848 |
dubbo.scan.base-packages | 必须 | 不需要 | 只扫@DubboService所在的包 |
dubbo.consumer.check | — | 可选 | 本工程设false,关了启动检查 |
dubbo.consumer.retries | — | 可选 | 本工程设0,不重试 |
“必须 / 可选”的判断依据很简单:谁监听、谁注册,谁就要配 protocol。提供者要把服务挂到一个端口上等人来调,所以必须有协议与端口;消费者只往外发调用,不对外暴露任何 Dubbo 服务,配了 protocol 只会平白多监听一个没用的端口。
这正是新手最常搞混的地方——两端 yml 看起来“应该对称”,实际是不对称的:
| 维度 | Provider | Consumer |
|---|---|---|
| 是否监听 Dubbo 端口 | 是(20881) | 否 |
是否需要scan | 是(暴露靠它找到@DubboService) | 否 |
| 是否注册到 Nacos | 注册自己(providers) | 订阅别人(consumers) |
| 核心注解 | @DubboService | @DubboReference |
# user-provider/src/main/resources/application.ymlserver:port:18080spring:datasource:url:jdbc:mysql://localhost:3306/dubbo-demo?useSSL=falseusername:rootpassword:rootdriver-class-name:com.mysql.jdbc.Driverapplication:name:user-providerdubbo:protocol:name:dubboport:20881registry:address:nacos://127.0.0.1:8848scan:base-packages:cn.mf.user.service# user-consumer/src/main/resources/application.ymlserver:port:18081spring:application:name:user-consumerdubbo:registry:address:nacos://127.0.0.1:8848consumer:check:false# 关闭启动检查,开发期允许提供者缺席retries:0packagecn.mf.user.controller;importcn.mf.user.domain.User;importcn.mf.user.service.UserService;importlombok.extern.slf4j.Slf4j;importorg.apache.dubbo.config.annotation.DubboReference;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@Slf4j@RestController@RequestMapping("/user")publicclassUserController{// 引用远程服务,指定版本、关闭重试、按权重随机@DubboReference(version="2.0.0",retries=0,loadbalance="Random")privateUserServiceuserService;@GetMapping("/usernames/{id}")publicStringqueryUsername(@PathVariable("id")Longid){returnuserService.queryUsername(id);}@GetMapping("/{id}")publicUserqueryById(@PathVariable("id")Longid){returnuserService.queryById(id);}}注解清单
| 注解 | 作用 | 本工程是否使用 | 版本差异 |
|---|---|---|---|
@DubboService | 暴露服务,标注在实现类上 | 是,两个实现分别打了1.0.0/2.0.0 | Dubbo 2.7.x 起用org.apache.dubbo...DubboService;2.6 及以前是com.alibaba.dubbo.config.annotation.Service |
@DubboReference | 引用远程服务,标注在字段上 | 是,version=2.0.0, retries=0, loadbalance=Random | 旧版为@Reference,包名同样从com.alibaba迁到org.apache |
@EnableDubbo | 开启注解扫描 | 未使用 | 可选;本工程靠 yml 的dubbo.scan.base-packages达到同样效果,二选一即可 |
@Method | 方法级配置(超时、重试等) | 未使用 | 2.7.x 提供,用于给单个方法覆盖接口级配置 |
一个容易踩的点:@DubboService是给它所在包被扫到才生效的。改了scan.base-packages却没同步改包名,服务会静默地注册不上——Nacos 里什么都看不到。
常见问题速查表
| 现象 | 原因 | 定位方法 |
|---|---|---|
启动直接失败,日志里有No provider available | 默认check=true,启动时检查依赖服务,提供者还没起 | 看消费端启动异常栈;开发期先dubbo.consumer.check=false |
运行时报No provider available for the service | 版本、接口全限定名或 group 三处不一致 | 逐字比对@DubboService与@DubboReference的version;在 Nacos 服务列表核对 |
| Nacos 服务列表始终为空 | registry.address写错(如漏nacos://),或 Nacos 根本没启动 | 浏览器打开8848/nacos;搜启动日志里的registry一行 |
| 调用抛序列化异常 | 实体没implements Serializable,或两端类结构/包名不一致 | 异常栈里搜SerializationException,回查 api 模块是否被两端共用 |
| 提供者注册上了但调用连不通 | 注册的是内网 IP,或 Dubbo 端口20881未监听、被防火墙拦 | Nacos 详情页看注册 IP;telnet 127.0.0.1 20881试连通 |
| 改了配置不生效 | 消费端误配了dubbo.protocol,或版本号没跟着改 | 检查 consumer yml 是否只有一个registry段 |
本案例的局限与下一步
这是最小可运行形态:一个提供者、一个消费者、一次调用。生产环境还差得远——
- 超时与重试怎么配才不会雪崩;
- 多版本(本案例已经埋了
1.0.0/2.0.0两个实现)如何做灰度发布; - 集群部署时的四种负载均衡策略怎么选;
- 调用链路要不要接监控告警。
这些正是紧跟着的「Dubbo 高级特性」要讲的内容,本案例只是它们的地基。
再往远看,项目项目最终不会让 Dubbo 单打独斗,而是把它作为 Spring Cloud 的二进制通信方案整合进去——用 Dubbo 替掉 Feign/Ribbon 完成远程调用,同时保留 Spring Cloud 的生态。这部分对应讲义第 4 章。
官方文档
- Apache Dubbo 官方文档
- Nacos 官网
- Dubbo Admin 管理控制台
总结
- 入门案例的全部内容就是两句话:提供者用
@DubboService暴露,消费者用@DubboReference引用,中间靠 Nacos 牵线。 - 两端配置不对称,消费端不配
protocol、不需要scan,这是最容易配错的一处。 - 抽取
user-api模块让依赖单向化,是这套结构能长期维护下去的前提。 - 传对象要序列化,两端类结构必须一致——Dubbo 走 TCP 二进制,不认“看起来一样”。
- 按 checklist 的顺序搭,每一步都能在控制台或日志里找到证据,跑不通时就知道该停在哪儿排查。