概述
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、实际连不通"的地址。
排查思路(按顺序):
- 去 Nacos 实例详情看注册上去的 IP 到底是多少,跟实际能通的地址比。
- 在消费者所在机器上
ping这个 IP、telnet IP 20881测端口,确认是不是网络不通而不是 Dubbo 的问题。 - 确认是本机
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显式指定能直接绕开。