Java 项目实战: 交友项目 - Dubbo 入门案例全流程复盘

概述

入门案例拆成六七篇讲的时候,每篇都只解决一个问题,看完很容易"每步都懂、串不起来"。这篇不复述细节,只做一件事:把 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 的顺序搭,每一步都能在控制台或日志里找到证据,跑不通时就知道该停在哪儿排查。
相关推荐
今年下半年3 小时前
【短信发送】Spring Boot整合多种短信接口的实现(附源码)
java·spring boot·后端·sms·qcloudsms
wespaul3 小时前
VeapAI 自然语言操作实战|第 04 篇:系统数据加工——把系统自身的元数据吃成知识
java·大数据·运维·数据库·人工智能
幼儿园蛋小黑3 小时前
SPI 子系统:从原理到实践
java·前端·数据库
yangmu32033 小时前
Unity 开发过程与底层原理
java·开发语言
lexiangqicheng3 小时前
自定义表单跨端共用机制-PC设计APP渲染
java·前端·javascript
zhikouai3 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
随性而行3604 小时前
企业微信API接口开发实战:如何实现消息回调接收与业务事件处理?
java·前端·python·微信·机器人·企业微信
美味蛋炒饭.4 小时前
java八股面试(3)
java·开发语言·面试
IT研究室4 小时前
最新计算机毕业设计选题推荐-基于spring boot的美食配送平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
霸道流氓气质4 小时前
Saga模式入门与实战:编排与协同模式、补偿设计原则与Java生产级实现
java·开发语言