Java 项目实战: 交友项目 - 入门案例:服务提供者搭建总结

概述

Dubbo 服务提供者写完不算完,真正决定"能不能被别人调用"的是注册与暴露这两件事。这篇把 user-provider 的搭建动作收敛成一份可复用清单,并把验证手段和最容易翻车的内网 IP 坑讲透。

纲要

  • 服务提供者的职责边界:暴露服务、注册自己
  • 五步搭建清单
    • 引依赖:dubbo-spring-boot-starter + dubbo-registry-nacos
    • 启动类开启 Dubbo:@EnableDubbo 与 @DubboComponentScan 的区别
    • 实现类加 @DubboService(2.7+ 新注解,老版本是 Spring 的 @Service)
    • 配置 application name / protocol / registry / scan
    • 启动并验证注册结果
  • 三种验证手段的分工:启动日志、Nacos 控制台、dubbo-admin
  • 核心坑:注册上去的 IP 必须是消费者能访问到的地址
  • 常见启动报错归因表
  • 本步骤的工程状态与下一篇衔接

提供者到底干了什么

Dubbo 的调用链条里,提供者只承担两个动作:把接口实现暴露 成一个可远程调用的服务,然后把自己的地址注册到注册中心让消费者能找到。前者靠注解,后者靠配置。
#mermaid-svg-nYgf2anz2LBppz6t{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-nYgf2anz2LBppz6t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nYgf2anz2LBppz6t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nYgf2anz2LBppz6t .error-icon{fill:#552222;}#mermaid-svg-nYgf2anz2LBppz6t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nYgf2anz2LBppz6t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nYgf2anz2LBppz6t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nYgf2anz2LBppz6t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nYgf2anz2LBppz6t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nYgf2anz2LBppz6t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nYgf2anz2LBppz6t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nYgf2anz2LBppz6t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nYgf2anz2LBppz6t .marker.cross{stroke:#333333;}#mermaid-svg-nYgf2anz2LBppz6t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nYgf2anz2LBppz6t p{margin:0;}#mermaid-svg-nYgf2anz2LBppz6t .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nYgf2anz2LBppz6t .cluster-label text{fill:#333;}#mermaid-svg-nYgf2anz2LBppz6t .cluster-label span{color:#333;}#mermaid-svg-nYgf2anz2LBppz6t .cluster-label span p{background-color:transparent;}#mermaid-svg-nYgf2anz2LBppz6t .label text,#mermaid-svg-nYgf2anz2LBppz6t span{fill:#333;color:#333;}#mermaid-svg-nYgf2anz2LBppz6t .node rect,#mermaid-svg-nYgf2anz2LBppz6t .node circle,#mermaid-svg-nYgf2anz2LBppz6t .node ellipse,#mermaid-svg-nYgf2anz2LBppz6t .node polygon,#mermaid-svg-nYgf2anz2LBppz6t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nYgf2anz2LBppz6t .rough-node .label text,#mermaid-svg-nYgf2anz2LBppz6t .node .label text,#mermaid-svg-nYgf2anz2LBppz6t .image-shape .label,#mermaid-svg-nYgf2anz2LBppz6t .icon-shape .label{text-anchor:middle;}#mermaid-svg-nYgf2anz2LBppz6t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nYgf2anz2LBppz6t .rough-node .label,#mermaid-svg-nYgf2anz2LBppz6t .node .label,#mermaid-svg-nYgf2anz2LBppz6t .image-shape .label,#mermaid-svg-nYgf2anz2LBppz6t .icon-shape .label{text-align:center;}#mermaid-svg-nYgf2anz2LBppz6t .node.clickable{cursor:pointer;}#mermaid-svg-nYgf2anz2LBppz6t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nYgf2anz2LBppz6t .arrowheadPath{fill:#333333;}#mermaid-svg-nYgf2anz2LBppz6t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nYgf2anz2LBppz6t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nYgf2anz2LBppz6t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nYgf2anz2LBppz6t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nYgf2anz2LBppz6t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nYgf2anz2LBppz6t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nYgf2anz2LBppz6t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nYgf2anz2LBppz6t .cluster text{fill:#333;}#mermaid-svg-nYgf2anz2LBppz6t .cluster span{color:#333;}#mermaid-svg-nYgf2anz2LBppz6t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nYgf2anz2LBppz6t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nYgf2anz2LBppz6t rect.text{fill:none;stroke-width:0;}#mermaid-svg-nYgf2anz2LBppz6t .icon-shape,#mermaid-svg-nYgf2anz2LBppz6t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nYgf2anz2LBppz6t .icon-shape p,#mermaid-svg-nYgf2anz2LBppz6t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nYgf2anz2LBppz6t .icon-shape .label rect,#mermaid-svg-nYgf2anz2LBppz6t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nYgf2anz2LBppz6t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nYgf2anz2LBppz6t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nYgf2anz2LBppz6t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} user-provider 启动
Dubbo 扫描 @DubboService
启动 Netty 服务器, 监听 20881
把 服务名 + IP + 端口 写入 Nacos
Nacos 服务列表出现该服务
consumer 订阅并拿到地址列表
consumer 直连 Netty 端口调用

注意最后一条边:消费者的 RPC 请求走的是 Netty 端口,不是 Tomcat 端口。这个结论直接决定了"配置里为什么有两个端口"以及"防火墙该放开哪个口"。

五步搭建清单

真实工程 dubbo-demo/user-provider 的结构(磁盘快照实际只有三个业务模块,父 pom 的 <modules> 里还写着早已删掉的 user-server,这里按磁盘真实内容列):

tree 复制代码
dubbo/dubbo-demo
├── pom.xml                       # 父工程, spring-boot 2.3.9.RELEASE
├── user-api                      # 纯接口 + 实体, 无 Spring 依赖
│   └── src/main/java/cn/mf/user
│       ├── domain/User.java      # implements Serializable
│       └── service/UserService.java
├── user-provider                 # 本篇主角
│   └── src/main
│       ├── java/cn/mf/user
│       │   ├── UserProviderApplication.java
│       │   ├── mapper/UserMapper.java
│       │   └── service/impl
│       │       ├── UserServiceImpl.java    # version = 1.0.0
│       │       └── UserServiceImpl2.java   # version = 2.0.0
│       └── resources/application.yml
└── user-consumer                 # 下一篇主角
    └── src/main/...

引依赖

user-provider/pom.xml 里两块依赖缺一不可,版本必须一致地对齐 Dubbo 版本,工程实测用的是 2.7.8:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <!--dubbo的起步依赖-->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.8</version>
    </dependency>

    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-registry-nacos</artifactId>
        <version>2.7.8</version>
    </dependency>

    <dependency>
        <groupId>cn.mf</groupId>
        <artifactId>user-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

dubbo-registry-nacos 是注册中心的客户端实现,把它换成 dubbo-registry-zookeeper 就能切到 ZooKeeper。两个 Dubbo 依赖版本别写岔,starter 和 registry 版本不一致时经常在启动阶段抛链路冲突。

启动类开启 Dubbo

磁盘上的启动类很干净,只有一个 @SpringBootApplication:

java 复制代码
package cn.mf.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserProviderApplication.class, args);
    }
}

这里有个容易踩的点:没有 @EnableDubbo 的工程凭什么能暴露服务 。取决于 Dubbo 版本对 Spring Boot 的自动配置程度------2.7.x 的 dubbo-spring-boot-starter 已经带了自动配置,注解靠 yml 里的 dubbo.scan.base-packages 扫到。但显式开启更稳,也更好读:

java 复制代码
package cn.mf.user;

import org.apache.dubbo.config.spring.context.annotation.DubboComponentScan;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 方式一:@EnableDubbo 开启全部 Dubbo 能力
@EnableDubbo
// 方式二:只指定要扫描的包,等价于把 scan 配置搬到注解上
// @DubboComponentScan(basePackages = "cn.mf.user.service.impl")
@SpringBootApplication
public class UserProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserProviderApplication.class, args);
    }
}
注解 作用 用在哪
@EnableDubbo 打开 Dubbo 全部自动装配,扫描包默认取启动类所在包及其子包 想省事、包结构规整时
@DubboComponentScan 只指定 @DubboService / @DubboReference 的扫描包 实现类不在启动类子包下时

两个注解不要同时写,会重复扫描。工程里选的是 yml 的 dubbo.scan.base-packages,写法等价于 @DubboComponentScan。

讲义 2.4.1 的引导类上还挂了一个 @MapperScan("cn.mf.user.mapper"),磁盘上的启动类没有它------因为 UserMapper 自己加了 @Mapper,MyBatis 的 starter 能直接识别。两者不必都写。

实现类标注 @DubboService

java 复制代码
package cn.mf.user.service.impl;

import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;

// 暴露 dubbo 服务
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    public String queryUsername(Long id) {
        return userMapper.findById(id).getUsername();
    }

    @Override
    public User queryById(Long id) {
        User user = userMapper.findById(id);
        user.setUsername(user.getUsername() + "v1.0");
        return user;
    }
}

关键结论:@DubboService 是 Dubbo 2.7 之后新增的注解,老版本(2.6 及以前)用的是 Spring 自己的 @Service 。如果你在 2.7 项目里沿用 @Service,服务既不会注册也不会暴露,启动日志里也不会有任何报错------只是静默地少了一个 provider,非常难查。

version 属性这次写了 "1.0.0",工程里还平行放了一个 UserServiceImpl2,标的是 "2.0.0",两者实现的都是 UserService,只是 queryById 返回值分别拼了 v1.0 / v2.0。这就是多版本共存的最小形态:同一个接口可以在注册中心挂多个不同 version 的提供者,消费者按 version 引用。关于灰度发布和版本路由,留给后面的"多版本支持"部分展开,这里只要记住 version 是写在暴露端的。

配置 protocol / registry / scan

yaml 复制代码
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
logging:
  level:
    cn.mf: debug
  pattern:
    dateformat: HH:mm:ss:SSS
# 配置 dubbo 提供者
# dubbo 协议与对外访问端口
dubbo:
  protocol:
    name: dubbo
    port: 20881
  # 注册中心地址
  registry:
    address: nacos://127.0.0.1:8848
  # dubbo 注解的包扫描
  scan:
    base-packages: cn.mf.user.service
配置项 含义 写错的后果
spring.application.name 应用名,日志与监控里的标识 与 dubbo.application.name 混用时容易只配一个
dubbo.application.name 注册中心里显示的服务名,dubbo-admin 按它分类 不配时 Dubbo 会用默认名,控制台里认不出是谁
dubbo.protocol.name 协议,默认 dubbo(Dubbo 2 私有协议,基于 Netty) 与消费端协议不一致会调用失败
dubbo.protocol.port 对外暴露的 RPC 端口 被占用直接启动失败
dubbo.registry.address 注册中心地址 写错则连不上,启动报错
dubbo.scan.base-packages @DubboService 的扫描范围 写窄了注解不生效

关于讲义与工程的差异,说清楚两点:讲义里 server.port 写的是 18081,磁盘工程是 18080,这个端口只是 Spring Boot 的 HTTP 端口,与 Dubbo 无关,两者都能跑;讲义里 @DubboService 没带 version,工程带了,version 是可选项,不带就是默认版本 0.0.0。

为什么会有两个端口

这是入门时最容易被问住的地方。18080 是 Tomcat 的 HTTP 端口,20881 是 Dubbo 的 RPC 端口,消费者调用走的是后者。

Dubbo 协议在暴露服务时,框架内部会启动一个 Netty 服务器,本质上就是一个 socket 服务端,用来做网络通信。Tomcat 在这里的角色只是"让 Spring Boot 应用跑起来的容器",业务接口并不从 Tomcat 出去。所以:

  • 消费者连的是 Netty 的 20881,不是 Tomcat 的 18080。
  • 部署时防火墙、安全组要放行的是 Dubbo 协议端口。
  • 多个 provider 部署在同一台机器时,dubbo.protocol.port 必须错开,Tomcat 端口冲突和 Dubbo 端口冲突是两回事。

怎么确认注册成功

启动成功不等于注册成功。下面三种手段各自能证明的东西不一样,建议按顺序看。
dubbo-admin Nacos user-provider dubbo-admin Nacos user-provider #mermaid-svg-DrFq9FnOpuyBmu0R{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-DrFq9FnOpuyBmu0R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DrFq9FnOpuyBmu0R .error-icon{fill:#552222;}#mermaid-svg-DrFq9FnOpuyBmu0R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DrFq9FnOpuyBmu0R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DrFq9FnOpuyBmu0R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DrFq9FnOpuyBmu0R .marker.cross{stroke:#333333;}#mermaid-svg-DrFq9FnOpuyBmu0R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DrFq9FnOpuyBmu0R p{margin:0;}#mermaid-svg-DrFq9FnOpuyBmu0R .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DrFq9FnOpuyBmu0R text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DrFq9FnOpuyBmu0R .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DrFq9FnOpuyBmu0R .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DrFq9FnOpuyBmu0R #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DrFq9FnOpuyBmu0R .sequenceNumber{fill:white;}#mermaid-svg-DrFq9FnOpuyBmu0R #sequencenumber{fill:#333;}#mermaid-svg-DrFq9FnOpuyBmu0R #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DrFq9FnOpuyBmu0R .messageText{fill:#333;stroke:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DrFq9FnOpuyBmu0R .labelText,#mermaid-svg-DrFq9FnOpuyBmu0R .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .loopText,#mermaid-svg-DrFq9FnOpuyBmu0R .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .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-DrFq9FnOpuyBmu0R .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DrFq9FnOpuyBmu0R .noteText,#mermaid-svg-DrFq9FnOpuyBmu0R .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DrFq9FnOpuyBmu0R .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DrFq9FnOpuyBmu0R .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DrFq9FnOpuyBmu0R .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DrFq9FnOpuyBmu0R .actorPopupMenu{position:absolute;}#mermaid-svg-DrFq9FnOpuyBmu0R .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-DrFq9FnOpuyBmu0R .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DrFq9FnOpuyBmu0R .actor-man circle,#mermaid-svg-DrFq9FnOpuyBmu0R line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DrFq9FnOpuyBmu0R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动 Spring 容器 Dubbo 扫描 @DubboService 并导出服务 注册 服务名/IP/端口/元数据 注册成功, 保持长连接心跳 拉取服务列表 返回 provider 列表

验证项 在哪看 预期看到什么 看不到说明什么
服务导出日志 应用控制台 / 日志文件 启动日志里会打印导出 Dubbo 服务与协议端口的信息,能看到协议名 dubbo 和端口 20881 只启动完 Spring 就结束、没有导出记录,说明 @DubboService 没被扫到
注册结果日志 应用控制台 注册到 Nacos 的提示,包含注册中心地址与本地地址 日志里出现连接异常,说明 registry.address 写错或 Nacos 没起
Nacos 控制台 http://127.0.0.1:8848/nacos → 服务列表 出现 providers:cn.mf.user.service.UserService:1.0.0: 这类服务名,实例数为 1 服务列表为空,说明服务没注册上(回到前面两条日志排查)
Nacos 实例详情 服务详情 → 实例 能看到实例 IP、端口 20881、healthy 状态、以及 protocol、application 等元数据 实例在但 IP 明显不对,就是下面那个内网 IP 坑
dubbo-admin http://127.0.0.1:8080 → 服务查询 按应用/服务名能查到 provider 列表,含方法名与参数类型 dubbo-admin 里查不到但 Nacos 里有,通常是 admin 的注册中心配置指向了别的环境

Nacos 服务名那串不是随便生成的,格式是 providers:{interface}:{version}:{group}。version 为空时中间是空的,所以上面那行末尾有两个冒号------看到双冒号别以为配错了。

日志里搜关键字时,Dubbo 的导出与注册日志都带 dubbo 前缀,logging.level.cn.mf=debug 只影响业务包,想看更细的 Dubbo 日志要单独开 logging.level.org.apache.dubbo=info。

那个必须讲清的坑:注册的 IP 必须是消费者能通的

Nacos 里存的不是"服务名",而是服务名加上一个具体的 IP + 端口 。这个 IP 是 Dubbo 自动探测本机网卡得到的,在单机开发时几乎总是 127.0.0.1 或本机局域网地址,没问题。但下面这些环境里它会探错:

  • Docker 容器:容器内看到的网卡是 172.17.x.x,注册上去之后宿主机和别的容器未必路由得到。
  • 虚拟机 / 多网卡机器:可能挑到那张只对内网可见的虚拟网卡(VMware、VirtualBox 的 host-only 网卡)。
  • 云主机:注册了内网 IP,跨 VPC 的消费者连不上。

表现 :提供者自己启动一切正常,Nacos 里也能看到实例,但消费者调用时超时,日志里是连接超时或 No provider available 之后重试耗尽。因为消费者拿到的是一个"看着像个正常 IP、实际连不通"的地址。

排查思路(按顺序):

  1. 去 Nacos 实例详情看注册上去的 IP 到底是多少,跟实际能通的地址比。
  2. 在消费者所在机器上 ping 这个 IP、telnet IP 20881 测端口,确认是不是网络不通而不是 Dubbo 的问题。
  3. 确认是本机 ip addr(Windows 用 ipconfig)里哪一张网卡,判断 Dubbo 挑错了哪张。

解决办法是显式指定对外地址,别让它自己猜:

yaml 复制代码
dubbo:
  protocol:
    name: dubbo
    port: 20881
    # 显式指定注册到注册中心的 IP,多网卡/Docker 环境必配
    host: 192.168.150.1
  registry:
    address: nacos://127.0.0.1:8848

dubbo.protocol.host 一配,注册上去的就是这个值。选哪个 IP 的原则很简单:从消费者所在机器能 ping 通的那一个 。测试环境常用宿主机的局域网地址,Docker 里则通常用 host.docker.internal 或宿主机 IP,具体看编排方式。

常见启动报错及归因

现象 大概率原因 怎么确认
启动正常但服务查不到 实现类没被扫描到;或者用了 Spring 的 @Service 而非 @DubboService 看包是否落在 scan.base-packages 下;搜实现类的 import 来源
启动直接失败,抛注册中心相关异常 registry.address 写错;Nacos 没启动 浏览器打开 127.0.0.1:8848/nacos 能不能进
端口占用异常 dubbo.protocol.port 已被别的进程占;同机跑多个 provider 没改端口 lsof -i :20881 / `netstat -ano
启动时长时间卡住 Dubbo 的启动检查机制在作怪:默认会在启动时校验依赖的服务是否可用 见下

启动检查(check)是 Dubbo 的一个默认行为:提供者和消费者启动时会去检查所依赖的服务在注册中心是否存在,不存在就报错而不是安静地起来。单机开发时这能帮你早发现配置错误,但在服务未全部就绪的编排场景下会拖慢甚至阻塞启动。它有几个可关的开关,细节留给后面的高级特性部分,这里只需要知道有这个机制、它是启动卡住的常见嫌疑。

本步骤产出小结

到这步为止,工程状态是:user-provider 已经能对外提供 UserService 服务,注册中心里有了它的记录,Netty 在 20881 上听着,Nacos 里能查到实例。也就是说"服务有了、地址可查"。

但还没有任何东西在调用它。接口定义在 user-api 里,契约已经抽出来了,接下来要做的就是让另一个应用拿着这份契约去把服务调起来------下一篇写消费者,重点是 @DubboReference 的用法,以及 version 怎么对上提供者那边的 1.0.0。

官方文档

总结

  • 提供者的搭建收敛成五步:引依赖、开扫描、标 @DubboService、写四项配置、启动验证。少任何一步服务都注册不上。
  • @DubboService 是 2.7+ 的注解,老版本是 @Service。用混了不会报错,只会静默失效。
  • version 写在暴露端,是后续多版本共存的入口,工程里已经放了 1.0.0 和 2.0.0 两份实现。
  • 两个端口的真相:Tomcat 是容器,Dubbo 自己起了 Netty,消费者连的是 Netty 的 20881。
  • 注册的是 IP 而不是服务名。多网卡、Docker、虚拟机环境下 Dubbo 会探错网卡,用 dubbo.protocol.host 显式指定能直接绕开。
相关推荐
hudd6681 小时前
JavaEE 初阶 - 多线程(进阶)
java·开发语言·java-ee
Pniubi1 小时前
区分Java创建线程池、多线程
java·线程·线程池·多线程·并发
程序员梅雨1 小时前
ZooKeeper面经总结
java·大数据·linux·分布式·后端·zookeeper·中间件
Yyyyyy~2 小时前
【Java】多线程初阶
java
蜗牛互联网2 小时前
Java 17 调用视觉 Responses API:JSON 约束与人工复核路由
java·人工智能·后端
SL_staff3 小时前
物模型三要素误用避坑指南:属性、事件、服务的正确建模逻辑
java
一条泥憨鱼3 小时前
【Redis | 黑马点评】短信登录功能 源码深度分析
java·redis·后端·session·黑马点评
威哥爱编程3 小时前
【AI全栈12-12】Spring Boot 3.x → 4.x 迁移实操:Jakarta 11、Jackson 3 与 AI 2.0
java·spring boot·后端
威哥爱编程3 小时前
【AI全栈12-11】Spring Boot 把 AI 接口真正上线扛量:限流 / 降级 / 可观测
java·spring boot·后端