☰
Java 项目实战: 交友项目 - Dubbo 入门案例全流程复盘
2026/10/14 3:28:40 网站建设 项目流程

概述

入门案例拆成六七篇讲的时候,每篇都只解决一个问题,看完很容易“每步都懂、串不起来”。这篇不复述细节,只做一件事:把 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 AdminNacos 解压到无中文路径,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-consumerIDEA 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;}

一次调用的全景链路

各篇拆开讲的时候,注册、订阅、序列化、负载均衡是分开的。把它们合到一张图里,链路就清楚了。

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)启动,扫描 @DubboService启动,扫描 @DubboReferenceUserServiceImpl2 执行,查库调用次数/耗时累计,每分钟上报 Monitor注册服务 providers:...UserService:1.0.0:20881订阅 UserService(version=2.0.0)推送提供者地址列表(长连接)Dubbo 协议(TCP)调用 queryById(1)序列化二进制返回 User

两点值得盯着看:

  • 订阅是拉,推送是长连接。消费者启动时向注册中心订阅自己需要的服务,注册中心返回地址列表;之后地址一变,注册中心走长连接把变更推过来,消费者不用重启。
  • 选谁是消费者决定的。地址列表拿到手后,由消费者按软负载算法挑一台;本案例@DubboReference(loadbalance = "Random")用的就是默认的按权重随机。这和 Spring Cloud 里 Ribbon 的客户端负载均衡是同一个思路。

两端配置对照

配置项ProviderConsumer说明
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 看起来“应该对称”,实际是不对称的:

维度ProviderConsumer
是否监听 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:0
packagecn.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.0Dubbo 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 的顺序搭,每一步都能在控制台或日志里找到证据,跑不通时就知道该停在哪儿排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询