概述
入门案例拆成六七篇讲的时候,每篇都只解决一个问题,看完很容易"每步都懂、串不起来"。这篇不复述细节,只做一件事:把 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同样被扫到。
实读工程结构
tree
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 模块里,这解决的正是"两端各抄一份接口,字段一改就漏"的问题。
java
package cn.mf.user.service;
import cn.mf.user.domain.User;
public interface UserService {
String queryUsername(Long id);
User queryById(Long id);
}
java
package cn.mf.user.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
private String address;
}
一次调用的全景链路
各篇拆开讲的时候,注册、订阅、序列化、负载均衡是分开的。把它们合到一张图里,链路就清楚了。
Provider(user-provider:18080 / dubbo:20881) Registry(Nacos:8848) Consumer(user-consumer:18081) Provider(user-provider:18080 / dubbo:20881) Registry(Nacos:8848) Consumer(user-consumer:18081) #mermaid-svg-nPFh68Zw57uRxeFL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nPFh68Zw57uRxeFL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nPFh68Zw57uRxeFL .error-icon{fill:#552222;}#mermaid-svg-nPFh68Zw57uRxeFL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nPFh68Zw57uRxeFL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nPFh68Zw57uRxeFL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nPFh68Zw57uRxeFL .marker.cross{stroke:#333333;}#mermaid-svg-nPFh68Zw57uRxeFL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nPFh68Zw57uRxeFL p{margin:0;}#mermaid-svg-nPFh68Zw57uRxeFL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nPFh68Zw57uRxeFL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-nPFh68Zw57uRxeFL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nPFh68Zw57uRxeFL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-nPFh68Zw57uRxeFL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-nPFh68Zw57uRxeFL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-nPFh68Zw57uRxeFL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-nPFh68Zw57uRxeFL .sequenceNumber{fill:white;}#mermaid-svg-nPFh68Zw57uRxeFL #sequencenumber{fill:#333;}#mermaid-svg-nPFh68Zw57uRxeFL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-nPFh68Zw57uRxeFL .messageText{fill:#333;stroke:none;}#mermaid-svg-nPFh68Zw57uRxeFL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nPFh68Zw57uRxeFL .labelText,#mermaid-svg-nPFh68Zw57uRxeFL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-nPFh68Zw57uRxeFL .loopText,#mermaid-svg-nPFh68Zw57uRxeFL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-nPFh68Zw57uRxeFL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nPFh68Zw57uRxeFL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-nPFh68Zw57uRxeFL .noteText,#mermaid-svg-nPFh68Zw57uRxeFL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-nPFh68Zw57uRxeFL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nPFh68Zw57uRxeFL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nPFh68Zw57uRxeFL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nPFh68Zw57uRxeFL .actorPopupMenu{position:absolute;}#mermaid-svg-nPFh68Zw57uRxeFL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-nPFh68Zw57uRxeFL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nPFh68Zw57uRxeFL .actor-man circle,#mermaid-svg-nPFh68Zw57uRxeFL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-nPFh68Zw57uRxeFL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动,扫描 @DubboService 启动,扫描 @DubboReference UserServiceImpl2 执行,查库 调用次数/耗时累计,每分钟上报 Monitor 注册服务 providers:...UserService:1.0.0:20881 订阅 UserService(version=2.0.0) 推送提供者地址列表(长连接) Dubbo 协议(TCP)调用 queryById(1) 序列化二进制返回 User
两点值得盯着看:
- 订阅是拉,推送是长连接。消费者启动时向注册中心订阅自己需要的服务,注册中心返回地址列表;之后地址一变,注册中心走长连接把变更推过来,消费者不用重启。
- 选谁是消费者决定的 。地址列表拿到手后,由消费者按软负载算法挑一台;本案例
@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 |
yaml
# user-provider/src/main/resources/application.yml
server:
port: 18080
spring:
datasource:
url: jdbc:mysql://localhost:3306/dubbo-demo?useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
application:
name: user-provider
dubbo:
protocol:
name: dubbo
port: 20881
registry:
address: nacos://127.0.0.1:8848
scan:
base-packages: cn.mf.user.service
yaml
# user-consumer/src/main/resources/application.yml
server:
port: 18081
spring:
application:
name: user-consumer
dubbo:
registry:
address: nacos://127.0.0.1:8848
consumer:
check: false # 关闭启动检查,开发期允许提供者缺席
retries: 0
java
package cn.mf.user.controller;
import cn.mf.user.domain.User;
import cn.mf.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
// 引用远程服务,指定版本、关闭重试、按权重随机
@DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")
private UserService userService;
@GetMapping("/usernames/{id}")
public String queryUsername(@PathVariable("id") Long id) {
return userService.queryUsername(id);
}
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.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 章。
官方文档
总结
- 入门案例的全部内容就是两句话:提供者用
@DubboService暴露,消费者用@DubboReference引用,中间靠 Nacos 牵线。 - 两端配置不对称 ,消费端不配
protocol、不需要scan,这是最容易配错的一处。 - 抽取
user-api模块让依赖单向化,是这套结构能长期维护下去的前提。 - 传对象要序列化,两端类结构必须一致------Dubbo 走 TCP 二进制,不认"看起来一样"。
- 按 checklist 的顺序搭,每一步都能在控制台或日志里找到证据,跑不通时就知道该停在哪儿排查。