概述
订单服务只知道自己表里的 userId,用户信息在另一个服务的库里。这一篇不急着写代码,先把"要做什么、为什么这么拆模块"讲清楚:需求跨了哪两个服务、跟之前 Feign 那版差在哪、四个模块分别装什么、依赖往哪个方向走。
纲要
- 业务需求:两个查询能力,一个"顺便带上用户信息"的附加要求
- 为什么这个需求天然需要跨服务调用(时序 + 流程图,标明消费者与提供者)
- 与 Feign 方案的对照:同一套需求,两种实现手段
- 模块划分:
dubbo-api/dubbo-domains/user-service/order-service各管什么 - 依赖方向:为什么接口和实体要单独成模块,以及怎么做到单向无环
- 契约落点:接口签名、实体字段、两个 HTTP 端点
- 技术要点清单:注册、协议、端口、序列化、版本统一
- 数据库准备:
tb_order/tb_user建表 SQL,以及为什么不建物理外键 - 工程实读:
cloud-dubbo-demo的真实模块树 - 验证目标:做完该看到什么现象
- 常见误解澄清
需求从哪来
需求本身很小,小到可以用一句话说完:
- 用户服务(user-service)提供"根据用户 id 查询用户"的能力;
- 订单服务(order-service)提供"根据订单 id 查询订单"的能力;
- 订单服务在返回订单详情时,要求把这条订单所属用户的信息一并带上。
前两条各自独立,谁也不用理谁。第三条把两边拴在了一起------订单表里只有 userId 这个数字,username、address 这些字段在用户库里。订单服务想拼出一个完整的返回体,就必须去问一趟用户服务。
这就是整个案例的动机。它跟前置课那个"最小 Dubbo 案例"的本质区别,不在于调用方式,而在于这一次是把它放进 SpringCloud 的治理体系里做:注册仍然走 Nacos,SpringCloud 的其余能力(配置、网关这些)都还在,只是把服务之间的调用从 HTTP 换成了 Dubbo 的 RPC。
一次查询要跨两个服务
先把数据流画出来。客户端打的是订单服务的接口,但请求会在服务端内部再"拐"一次。
#mermaid-svg-8WZSqMS3pxep8PbM{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-8WZSqMS3pxep8PbM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8WZSqMS3pxep8PbM .error-icon{fill:#552222;}#mermaid-svg-8WZSqMS3pxep8PbM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8WZSqMS3pxep8PbM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8WZSqMS3pxep8PbM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8WZSqMS3pxep8PbM .marker.cross{stroke:#333333;}#mermaid-svg-8WZSqMS3pxep8PbM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8WZSqMS3pxep8PbM p{margin:0;}#mermaid-svg-8WZSqMS3pxep8PbM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8WZSqMS3pxep8PbM .cluster-label text{fill:#333;}#mermaid-svg-8WZSqMS3pxep8PbM .cluster-label span{color:#333;}#mermaid-svg-8WZSqMS3pxep8PbM .cluster-label span p{background-color:transparent;}#mermaid-svg-8WZSqMS3pxep8PbM .label text,#mermaid-svg-8WZSqMS3pxep8PbM span{fill:#333;color:#333;}#mermaid-svg-8WZSqMS3pxep8PbM .node rect,#mermaid-svg-8WZSqMS3pxep8PbM .node circle,#mermaid-svg-8WZSqMS3pxep8PbM .node ellipse,#mermaid-svg-8WZSqMS3pxep8PbM .node polygon,#mermaid-svg-8WZSqMS3pxep8PbM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8WZSqMS3pxep8PbM .rough-node .label text,#mermaid-svg-8WZSqMS3pxep8PbM .node .label text,#mermaid-svg-8WZSqMS3pxep8PbM .image-shape .label,#mermaid-svg-8WZSqMS3pxep8PbM .icon-shape .label{text-anchor:middle;}#mermaid-svg-8WZSqMS3pxep8PbM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8WZSqMS3pxep8PbM .rough-node .label,#mermaid-svg-8WZSqMS3pxep8PbM .node .label,#mermaid-svg-8WZSqMS3pxep8PbM .image-shape .label,#mermaid-svg-8WZSqMS3pxep8PbM .icon-shape .label{text-align:center;}#mermaid-svg-8WZSqMS3pxep8PbM .node.clickable{cursor:pointer;}#mermaid-svg-8WZSqMS3pxep8PbM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8WZSqMS3pxep8PbM .arrowheadPath{fill:#333333;}#mermaid-svg-8WZSqMS3pxep8PbM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8WZSqMS3pxep8PbM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8WZSqMS3pxep8PbM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8WZSqMS3pxep8PbM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8WZSqMS3pxep8PbM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8WZSqMS3pxep8PbM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8WZSqMS3pxep8PbM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8WZSqMS3pxep8PbM .cluster text{fill:#333;}#mermaid-svg-8WZSqMS3pxep8PbM .cluster span{color:#333;}#mermaid-svg-8WZSqMS3pxep8PbM 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-8WZSqMS3pxep8PbM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8WZSqMS3pxep8PbM rect.text{fill:none;stroke-width:0;}#mermaid-svg-8WZSqMS3pxep8PbM .icon-shape,#mermaid-svg-8WZSqMS3pxep8PbM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8WZSqMS3pxep8PbM .icon-shape p,#mermaid-svg-8WZSqMS3pxep8PbM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8WZSqMS3pxep8PbM .icon-shape .label rect,#mermaid-svg-8WZSqMS3pxep8PbM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8WZSqMS3pxep8PbM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8WZSqMS3pxep8PbM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8WZSqMS3pxep8PbM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /order/101
查订单
Dubbo RPC
queryById userId=1
查用户
返回 User
Order 里带上 user 字段
前端 / 浏览器
order-service :8082
消费者
dubbo-demo
tb_order
user-service :8081
提供者
dubbo-demo
tb_user
从"谁调谁"看,角色是固定的:
| 角色 | 服务 | 端口 | 职责 |
|---|---|---|---|
| 消费者(Consumer) | order-service | 8082 | 接收前端请求、查自己的订单、发起 Dubbo 调用拿用户 |
| 提供者(Provider) | user-service | 8081 | 查询用户、暴露 Dubbo 服务等候被调 |
注意"消费者"这个身份不是给前端安上的------前端调订单走的是普通 HTTP。真正的消费者是订单服务:它既是 HTTP 服务端,又是 Dubbo 消费端,两个身份叠在一个应用里。这一点想不清楚,后面很容易把 @DubboReference 写到 Controller 之外的地方去找。
时序上更直观:
tb_user user-service 8081 tb_order order-service 8082 前端 tb_user user-service 8081 tb_order order-service 8082 前端 #mermaid-svg-K9ae7jwmYjIaUDsY{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-K9ae7jwmYjIaUDsY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K9ae7jwmYjIaUDsY .error-icon{fill:#552222;}#mermaid-svg-K9ae7jwmYjIaUDsY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K9ae7jwmYjIaUDsY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K9ae7jwmYjIaUDsY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K9ae7jwmYjIaUDsY .marker.cross{stroke:#333333;}#mermaid-svg-K9ae7jwmYjIaUDsY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K9ae7jwmYjIaUDsY p{margin:0;}#mermaid-svg-K9ae7jwmYjIaUDsY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K9ae7jwmYjIaUDsY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-K9ae7jwmYjIaUDsY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-K9ae7jwmYjIaUDsY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-K9ae7jwmYjIaUDsY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-K9ae7jwmYjIaUDsY .sequenceNumber{fill:white;}#mermaid-svg-K9ae7jwmYjIaUDsY #sequencenumber{fill:#333;}#mermaid-svg-K9ae7jwmYjIaUDsY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-K9ae7jwmYjIaUDsY .messageText{fill:#333;stroke:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K9ae7jwmYjIaUDsY .labelText,#mermaid-svg-K9ae7jwmYjIaUDsY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .loopText,#mermaid-svg-K9ae7jwmYjIaUDsY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .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-K9ae7jwmYjIaUDsY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-K9ae7jwmYjIaUDsY .noteText,#mermaid-svg-K9ae7jwmYjIaUDsY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-K9ae7jwmYjIaUDsY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K9ae7jwmYjIaUDsY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K9ae7jwmYjIaUDsY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K9ae7jwmYjIaUDsY .actorPopupMenu{position:absolute;}#mermaid-svg-K9ae7jwmYjIaUDsY .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-K9ae7jwmYjIaUDsY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K9ae7jwmYjIaUDsY .actor-man circle,#mermaid-svg-K9ae7jwmYjIaUDsY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-K9ae7jwmYjIaUDsY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /order/101 select * from tb_order where id = 101 Order(id=101, userId=1) Dubbo 调用 queryById(1) select * from tb_user where id = 1 User(id=1, username=柳岩) User Order{ id:101, userId:1, user:{id:1,username:"柳岩"} }
关键在中间那一步:OS->>US 走的是 Dubbo 协议,不是 HTTP。前端完全不知道内部拐了这一下。
需求没变,实现手段变了
这个案例最值得琢磨的地方,是它和前面 SpringCloud 篇的 cloud-demo 是同一套业务需求。那边已经有"根据订单查订单、顺带查用户"的做法,用的是 Feign;这边只是把 Feign 换成了 Dubbo。
需求一个字没改,变的全是实现层面的东西。对照着看差别在哪:
| 维度 | Feign 方案 | Dubbo 方案 |
|---|---|---|
| 模块划分 | feign-api(Feign 接口 + 实体混在一起) |
dubbo-api(接口)与 dubbo-domains(实体)分开 |
| 调用方式 | 声明式 HTTP,@FeignClient 拼 URL |
二进制 RPC,@DubboReference 引用远程对象 |
| 调用对外表现 | 消费者写 @GetMapping 路径,本质是发 HTTP 请求 |
消费者拿到的像本地 Bean,直接方法调用 |
| 依赖的 starter | spring-cloud-starter-openfeign |
spring-cloud-starter-dubbo |
| 契约所在位置 | 消费者自己声明的接口(路径 + 参数靠人工对齐) | 双方共享的 dubbo-api 模块(提供者实现它) |
| 注册方式 | 注册到 Nacos,spring.cloud.nacos.discovery |
注册到 Nacos,但 Dubbo 侧写 spring-cloud://localhost 委托 |
| 序列化 | JSON(文本) | Hessian2(二进制,默认) |
| 协议端口 | 复用服务自己的 web 端口 | 独占一个 Dubbo 端口(本例 20881) |
一句话概括:这需求用 HTTP 也能做,用 RPC 也能做,这里选的是 RPC 这条路,顺带把模块划分按 Dubbo 的规矩重排了一遍。 至于 spring-cloud://localhost 这行配置为什么能成立、它和 nacos://127.0.0.1:8848 差在哪,那是机制层的活,见同章的方案概述篇,这篇只负责把需求与模块边界定死。
模块规划:四个模块各管什么
Dubbo 用 maven 的标准做法是"公共接口抽到一个模块里,双方依赖它"。这个案例在标准的思路上又走细了一步------接口和实体拆成两个模块,合起来四个:
| 模块 | 角色 | packaging | 装什么 |
|---|---|---|---|
dubbo-api |
契约模块 | jar | 只有服务接口,如 UserService。写清楚"能调什么" |
dubbo-domains |
实体模块 | jar | 跨服务传输的公共实体,如 User、Order。写清楚"传什么" |
user-service |
提供者 | jar(SpringBoot) | 接口实现 UserServiceImpl、UserController、UserMapper、配置文件 |
order-service |
消费者 | jar(SpringBoot) | OrderController(含 @DubboReference)、OrderServiceImpl、OrderMapper、配置文件 |
dubbo-api 与 dubbo-domains 都不是可启动的应用,没有 main 方法,只有一个空 resources 目录------它们是纯粹的"零件仓库",谁需要谁来依赖。
依赖方向:单向,且没有环
依赖关系必须画出来看一遍,因为模块拆分最容易翻车的地方就是绕成环:
#mermaid-svg-wDT5K3l8pLf8F5ks{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-wDT5K3l8pLf8F5ks .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wDT5K3l8pLf8F5ks .error-icon{fill:#552222;}#mermaid-svg-wDT5K3l8pLf8F5ks .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wDT5K3l8pLf8F5ks .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wDT5K3l8pLf8F5ks .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wDT5K3l8pLf8F5ks .marker.cross{stroke:#333333;}#mermaid-svg-wDT5K3l8pLf8F5ks svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wDT5K3l8pLf8F5ks p{margin:0;}#mermaid-svg-wDT5K3l8pLf8F5ks .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster-label text{fill:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster-label span{color:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster-label span p{background-color:transparent;}#mermaid-svg-wDT5K3l8pLf8F5ks .label text,#mermaid-svg-wDT5K3l8pLf8F5ks span{fill:#333;color:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks .node rect,#mermaid-svg-wDT5K3l8pLf8F5ks .node circle,#mermaid-svg-wDT5K3l8pLf8F5ks .node ellipse,#mermaid-svg-wDT5K3l8pLf8F5ks .node polygon,#mermaid-svg-wDT5K3l8pLf8F5ks .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wDT5K3l8pLf8F5ks .rough-node .label text,#mermaid-svg-wDT5K3l8pLf8F5ks .node .label text,#mermaid-svg-wDT5K3l8pLf8F5ks .image-shape .label,#mermaid-svg-wDT5K3l8pLf8F5ks .icon-shape .label{text-anchor:middle;}#mermaid-svg-wDT5K3l8pLf8F5ks .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wDT5K3l8pLf8F5ks .rough-node .label,#mermaid-svg-wDT5K3l8pLf8F5ks .node .label,#mermaid-svg-wDT5K3l8pLf8F5ks .image-shape .label,#mermaid-svg-wDT5K3l8pLf8F5ks .icon-shape .label{text-align:center;}#mermaid-svg-wDT5K3l8pLf8F5ks .node.clickable{cursor:pointer;}#mermaid-svg-wDT5K3l8pLf8F5ks .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wDT5K3l8pLf8F5ks .arrowheadPath{fill:#333333;}#mermaid-svg-wDT5K3l8pLf8F5ks .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wDT5K3l8pLf8F5ks .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wDT5K3l8pLf8F5ks .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDT5K3l8pLf8F5ks .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wDT5K3l8pLf8F5ks .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDT5K3l8pLf8F5ks .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster text{fill:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks .cluster span{color:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks 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-wDT5K3l8pLf8F5ks .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wDT5K3l8pLf8F5ks rect.text{fill:none;stroke-width:0;}#mermaid-svg-wDT5K3l8pLf8F5ks .icon-shape,#mermaid-svg-wDT5K3l8pLf8F5ks .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDT5K3l8pLf8F5ks .icon-shape p,#mermaid-svg-wDT5K3l8pLf8F5ks .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wDT5K3l8pLf8F5ks .icon-shape .label rect,#mermaid-svg-wDT5K3l8pLf8F5ks .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDT5K3l8pLf8F5ks .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wDT5K3l8pLf8F5ks .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wDT5K3l8pLf8F5ks :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 显式声明
显式声明
order-service 消费者
dubbo-api 契约
user-service 提供者
dubbo-domains 实体
方向很干净:
- 两个 service 都指向
dubbo-api,dubbo-api指向dubbo-domains; dubbo-domains谁也不依赖,是最底层;- 没有任何一条边反向指回 service------服务之间没有编译期依赖,只有运行期的 Dubbo 调用。
dubbo-api 的 pom 里有一句值得单独拎出来看:
xml
<dependency>
<groupId>cn.mf</groupId>
<artifactId>dubbo-domains</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
为什么接口模块要依赖实体模块?因为接口签名里出现了 User:
java
package cn.mf.dubbo.api;
import cn.mf.dubbo.domain.User;
public interface UserService {
User queryById(Long id);
}
编译这个接口需要 User 这个类。所以 dubbo-api → dubbo-domains 这条边是被语言层面逼出来的,躲不掉。两个 service 的 pom 里又各自显式 声明了一遍 dubbo-domains,虽然通过 dubbo-api 能传递过来,但传递依赖在 maven 里会随依赖收敛策略变化,写全更稳。
为什么接口和实体非得独立成模块
前置课讲过"抽取公共的接口模块",那是把接口和实体塞在一个模块里。这个案例把两者拆开,理由是实打实的:
- 接口是"能力约定",实体是"数据结构",两者的变更节奏不一样。加一个接口方法影响的是调用方;加一个实体字段影响的是所有传输方。拆开之后,改动的影响面一眼可见。
- 避免传递依赖失控 。实体模块里往往要引 Lombok 之类的工具依赖;如果和接口混在一个模块,任何依赖
dubbo-api的人都会被塞进这些无关依赖。 - 序列化边界清楚 。跨服务二进制传输的对象必须能被双方正确反序列化,也就是双方的类定义要完全一致。把实体集中到一个模块,是保证"两侧看到的类是同一个类"的最简单办法。
顺带说一句:如果消费者和提供者各自写一份 User 类,哪怕字段一模一样、包名一模一样,Dubbo 反序列化也可能出问题------因为类加载器不同。这就是实体模块存在的意义,不是"可有可无的整理",是功能性的必需。
契约与端点
接口和实体定了,两个 HTTP 端点也就定了:
| 归属 | 类型 | 签名 / 路径 | 说明 |
|---|---|---|---|
| dubbo-api | Dubbo 接口 | UserService#queryById(Long id) : User |
提供者暴露、消费者引用 |
| user-service | HTTP | GET /user/{id} |
便捷调试用,直接查用户 |
| order-service | HTTP | GET /order/{orderId} |
前端入口,返回值里带 user |
类结构本身不复杂:
#mermaid-svg-S8JJBxucRf02rcOZ{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-S8JJBxucRf02rcOZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S8JJBxucRf02rcOZ .error-icon{fill:#552222;}#mermaid-svg-S8JJBxucRf02rcOZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S8JJBxucRf02rcOZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S8JJBxucRf02rcOZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S8JJBxucRf02rcOZ .marker.cross{stroke:#333333;}#mermaid-svg-S8JJBxucRf02rcOZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S8JJBxucRf02rcOZ p{margin:0;}#mermaid-svg-S8JJBxucRf02rcOZ g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-S8JJBxucRf02rcOZ g.classGroup text .title{font-weight:bolder;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster-label text{fill:#333;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster-label span{color:#333;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster-label span p{background-color:transparent;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster text{fill:#333;}#mermaid-svg-S8JJBxucRf02rcOZ .cluster span{color:#333;}#mermaid-svg-S8JJBxucRf02rcOZ .nodeLabel,#mermaid-svg-S8JJBxucRf02rcOZ .edgeLabel{color:#131300;}#mermaid-svg-S8JJBxucRf02rcOZ .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-S8JJBxucRf02rcOZ .label text{fill:#131300;}#mermaid-svg-S8JJBxucRf02rcOZ .labelBkg{background:#ECECFF;}#mermaid-svg-S8JJBxucRf02rcOZ .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-S8JJBxucRf02rcOZ .classTitle{font-weight:bolder;}#mermaid-svg-S8JJBxucRf02rcOZ .node rect,#mermaid-svg-S8JJBxucRf02rcOZ .node circle,#mermaid-svg-S8JJBxucRf02rcOZ .node ellipse,#mermaid-svg-S8JJBxucRf02rcOZ .node polygon,#mermaid-svg-S8JJBxucRf02rcOZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S8JJBxucRf02rcOZ .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ g.clickable{cursor:pointer;}#mermaid-svg-S8JJBxucRf02rcOZ g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-S8JJBxucRf02rcOZ g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-S8JJBxucRf02rcOZ .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-S8JJBxucRf02rcOZ .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-S8JJBxucRf02rcOZ .dashed-line{stroke-dasharray:3;}#mermaid-svg-S8JJBxucRf02rcOZ .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-S8JJBxucRf02rcOZ #compositionStart,#mermaid-svg-S8JJBxucRf02rcOZ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #compositionEnd,#mermaid-svg-S8JJBxucRf02rcOZ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #dependencyStart,#mermaid-svg-S8JJBxucRf02rcOZ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #dependencyStart,#mermaid-svg-S8JJBxucRf02rcOZ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #extensionStart,#mermaid-svg-S8JJBxucRf02rcOZ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #extensionEnd,#mermaid-svg-S8JJBxucRf02rcOZ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #aggregationStart,#mermaid-svg-S8JJBxucRf02rcOZ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #aggregationEnd,#mermaid-svg-S8JJBxucRf02rcOZ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #lollipopStart,#mermaid-svg-S8JJBxucRf02rcOZ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ #lollipopEnd,#mermaid-svg-S8JJBxucRf02rcOZ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-S8JJBxucRf02rcOZ .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-S8JJBxucRf02rcOZ .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S8JJBxucRf02rcOZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S8JJBxucRf02rcOZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S8JJBxucRf02rcOZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 返回
组合
<<interface>>
UserService
+queryById(Long id) : User
User
+Long id
+String username
+String address
Order
+Long id
+Long price
+String name
+Integer num
+Long userId
+User user
Order 里那个 User user 字段是特意留的:数据库里没有这一列,它是订单服务在返回前运行时塞进去 的。查完订单,从 order.getUserId() 拿到用户 id,Dubbo 调一下用户服务,拿到 User 再 setUser 上去。这个字段什么时机被赋值,是整个案例的验收标准。
两个实体的字段定义如下,注意它们都实现了 Serializable:
java
package cn.mf.dubbo.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
private String address;
}
java
package cn.mf.dubbo.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class Order implements Serializable {
private Long id;
private Long price;
private String name;
private Integer num;
private Long userId;
private User user;
}
关于 Serializable:Dubbo 默认用 Hessian2 序列化,严格来说并不强制要求对象实现这个接口。但课程里两个实体都加上了,这个习惯值得保留------一旦换成 JDK 原生序列化,或者对象要进缓存、写文件、过某些网关,没有它就会直接抛 NotSerializableException。廉价保险,没必要省。
技术要点清单
把散落的要点收成一张表,按归属模块对应着看:
| 要点 | 说明 | 归属模块 |
|---|---|---|
| SpringCloud 注册 | 两侧都引 spring-cloud-starter-alibaba-nacos-discovery,server-addr: localhost:8848 |
user-service / order-service |
| Dubbo 调用 | 两侧都引 spring-cloud-starter-dubbo,把注册发现委托给 SpringCloud |
user-service / order-service |
| 注册委托 | dubbo.registry.address: spring-cloud://localhost,不是 nacos:// |
两侧 |
| 暴露服务 | 提供者实现类打 @DubboService,并配 dubbo.scan.base-packages |
user-service |
| 引用服务 | 消费者字段打 @DubboReference |
order-service |
| 协议端口 | 只有提供者需要 dubbo.protocol,dubbo / 20881 |
user-service |
| 启动检查 | 消费者加 dubbo.consumer.check: false,避免启动顺序耦合 |
order-service |
| 重试次数 | dubbo.consumer.retries: 0,查询类调用关掉自动重试 |
order-service |
| 序列化 | 传输对象集中放公共模块,两侧类定义一致 | dubbo-domains |
| 版本统一 | 全工程共用父 pom 的三件套版本,禁止各模块各写一套 | 父 pom |
| Lombok | @Data 生成 getter/setter,父 pom 统一引 |
全工程 |
版本统一这条单独强调一下。父 pom 里锁死的是这么一组:
yaml
spring-boot: 2.3.9.RELEASE
spring-cloud: Hoxton.SR10
spring-cloud-alibaba: 2.2.5.RELEASE
mysql-connector-java: 5.1.47
这三者是一套互相认账的组合。任何一个模块单独升版本(尤其 SpringBoot 往 2.6+ 走),Nacos 与 Dubbo 的 starter 会立刻因为配置加载方式变化而报错。整合类案例的怪问题,九成出在版本不配套,而不是代码写错了。
数据库准备
两个服务连的是同一个库 dubbo-demo,但各自只碰自己那张表。建库建表脚本如下:
sql
CREATE DATABASE IF NOT EXISTS `dubbo-demo` DEFAULT CHARACTER SET utf8;
USE `dubbo-demo`;
-- 订单表:只存 userId,不存用户姓名、地址
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`userId` bigint(20) NOT NULL COMMENT '用户id',
`name` varchar(100) DEFAULT NULL COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '商品价格',
`num` int(10) DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 DEFAULT CHARSET = utf8 COMMENT '订单表';
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
-- 用户表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL COMMENT '收件人',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 DEFAULT CHARSET = utf8 COMMENT '用户表';
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
有两个细节容易被忽略。
第一,tb_order 里的外键列实际写成了 userId 这个小驼峰名字 ,而不是 user_id。这跟"数据库字段用下划线、Java 字段用小驼峰"的一般约定是反的。之所以能跑通,是因为 OrderMapper 直接 select *,列名和 Java 字段名一字不差地对上了,压根没用到下划线转驼峰。如果你按习惯把列改成 user_id,而没开 map-underscore-to-camel-case,查出来的 userId 就恒为 null------后面 queryById(null) 必然空指针。
第二,tb_order 没有任何指向 tb_user 的外键约束 。userId 只是个普通数字,语义上指代用户,物理上什么也不关联。
不建外键不是偷懒,是微服务里必须这么干。订单服务和用户服务是两个独立部署的应用,各自拥有自己的数据;如果数据库层面存在外键,就等于把两个服务的表绑成了一体------用户库想清理一条历史数据,订单库不点头就删不掉;用户服务想换库、想分库,订单库立刻被拖住。跨服务的关联只能靠应用层做,也就是这次要实现的 Dubbo 调用。外键能带来的一点一致性保障,换不来这个代价。
工程结构实读
放进 IDE 的真实形态是这样(已剔除 target 与 .iml):
tree
cloud-dubbo-demo
├── pom.xml # 父工程:统一 SpringBoot / SpringCloud / Alibaba 版本
├── dubbo-domains # 【实体模块】谁都能依赖,自己不依赖谁
│ └── src/main/java/cn/mf/dubbo/domain/
│ ├── User.java # id / username / address
│ └── Order.java # id / price / name / num / userId / user
├── dubbo-api # 【契约模块】只放服务接口
│ └── src/main/java/cn/mf/dubbo/api/
│ └── UserService.java # User queryById(Long id)
├── user-service # 【提供者】8081,Dubbo 协议 20881
│ └── src/main/
│ ├── java/cn/mf/user/
│ │ ├── UserApplication.java # @SpringBootApplication + @MapperScan
│ │ ├── controller/UserController.java
│ │ ├── service/impl/UserServiceImpl.java # @DubboService 暴露服务
│ │ └── mapper/UserMapper.java # select * from tb_user where id = #{id}
│ └── resources/application.yml
└── order-service # 【消费者】8082
└── src/main/
├── java/cn/mf/order/
│ ├── OrderApplication.java # @SpringBootApplication + @MapperScan
│ ├── controller/OrderController.java # @DubboReference 引用远程服务
│ ├── service/OrderService.java
│ ├── service/impl/OrderServiceImpl.java
│ └── mapper/OrderMapper.java # select * from tb_order where id = #{id}
└── resources/application.yml
order-service 的 Controller 是整个案例的落点,需求里那句"带着用户信息返回"就靠这几行实现:
java
package cn.mf.order.controller;
import cn.mf.dubbo.api.UserService;
import cn.mf.dubbo.domain.Order;
import cn.mf.dubbo.domain.User;
import cn.mf.order.service.OrderService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@DubboReference
private UserService userService;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 1、根据 id 查询订单(走本地 mybatis)
Order order = orderService.queryOrderById(orderId);
// 2、拿到订单里的用户 id
Long userId = order.getUserId();
// 3、Dubbo 远程调用,查用户
User user = userService.queryById(userId);
// 4、把用户塞进订单,一并返回
order.setUser(user);
return order;
}
}
userService 这个字段的类型是 cn.mf.dubbo.api.UserService------接口来自公共模块,实现来自另一个进程 。本地没有任何实现类,全靠 @DubboReference 生成代理,调用时把参数序列化发出去。
提供者这边,实现类换个注解就完成暴露:
java
package cn.mf.user.service.impl;
import cn.mf.dubbo.api.UserService;
import cn.mf.dubbo.domain.User;
import cn.mf.user.mapper.UserMapper;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
@DubboService
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.findById(id);
}
}
一个容易看走眼的地方:UserController 里注入的也是这个 UserService 接口,但它匹配的是本地的 UserServiceImpl,不是远程代理 。因为 @DubboService 在暴露 Dubbo 服务的同时,也把这个实现注册成了 Spring Bean,同进程内自然优先拿到本地实例。
做完应该看到什么
需求分析写完,验收标准也要提前定死,不然写完代码自己都说不清算不算成功。
| 观察点 | 预期现象 |
|---|---|
| Nacos 控制台 | 服务列表里同时出现 user-service 与 order-service |
| 端口监听 | 提供者的 Dubbo 端口 20881 处于监听状态 |
| 用户接口 | GET http://localhost:8081/user/1 返回柳岩那条用户数据 |
| 订单接口 | GET http://localhost:8082/order/101 返回的 JSON 里 user 字段不为 null |
启动与验证命令:
bash
# 前置:Nacos 是一切的前提,先起它(默认 8848)
# Windows: startup.cmd -m standalone ;macOS/Linux: sh startup.sh -m standalone
# 起提供者 user-service(8081,Dubbo 监听 20881)
# IDEA 里运行 UserApplication
# 起消费者 order-service(8082)
# IDEA 里运行 OrderApplication
# 查订单------这一步才是整合是否成功的判据
curl http://localhost:8082/order/101
# 看 Dubbo 协议端口有没有起来
lsof -i:20881 # macOS / Linux
netstat -ano | findstr 20881 # Windows
第 4 步的正确返回长这样,注意 user 对象是完整的:
text
{
"id": 101,
"price": 699900,
"name": "Apple 苹果 iPhone 12 ",
"num": 1,
"userId": 1,
"user": { "id": 1, "username": "柳岩", "address": "湖南省衡阳市" }
}
如果跑出来是 "user": null,先别怀疑 Dubbo 配置,按这个顺序排:订单查询本身有没有查到数据(userId 是否为 null)→ 提供者有没有起来并注册到 Nacos → 20881 端口在不在 → spring-cloud://localhost 有没有被写成 nacos://。前两步错占绝大多数。
常见误解澄清
"整合之后 SpringCloud 的注册中心就可以不要了。" 不行。Dubbo 那行 registry.address: spring-cloud://localhost 恰恰是"向 SpringCloud 注册中心登记"的意思,Nacos 不但不能停,还是整个链路的起点。真要写成 dubbo:// 或 nacos:// 直连,也绕不开 Nacos 那台机器。
"实体模块可有可无,双方各自写一份 User 类就行了。" 跨服务传对象时不行。Dubbo 序列化后由对端反序列化,两侧的类定义必须一致;各写一份,包名、字段数量、字段类型哪怕差一点,轻则字段丢失,重则反序列化异常。公共实体模块是功能组件,不是代码整洁的装饰。
"订单表可以建个外键关联用户表,省得跨服务查。" 这一条在单体里成立,在微服务里是禁忌。物理外键把两张表绑成一体,等于让两个独立服务共享了数据库层的强耦合------谁都没法单独换库、单独迁移数据。本例里 userId 就是个普通 bigint,关联关系由应用层的 Dubbo 调用来表达。
"这个需求本来就该用 RPC,用 HTTP 做不出来。" 恰恰相反,前面的 Feign 方案就是这么做的,而且完全满足需求。这不是"能不能"的问题,是"选哪种"的问题:HTTP + JSON 生态好、跨语言、调试直观,代价是文本序列化和 HTTP 栈的开销;Dubbo 用二进制 RPC 换性能,代价是绑定 Dubbo 生态、契约要靠公共模块维持。选型没有绝对的对错。
下一篇
需求与模块边界定完了,接着就是按这套规划把工程搭起来:父 pom 统一版本、两个公共模块、提供者暴露服务、消费者引用服务。细节见同章的 SpringCloud整合dubbo:代码实现。
官方文档
总结
- 需求的切口很小:订单详情要带上用户信息,而用户数据在另一个服务里,于是产生了跨服务调用;订单服务是消费者,用户服务是提供者。
- 与 Feign 方案对照,需求没变、实现手段变了------这正是"整合"这件事的立论角度:注册、配置、网关继续用 SpringCloud,调用这一段交给 Dubbo。
- 模块划分四件套:
dubbo-api管能力约定、dubbo-domains管数据结构、两个 service 各自实现与调用;接口和实体拆开,是因为它们的变更节奏不同、依赖诉求不同、序列化边界要清楚。 - 依赖方向单向无环:service →
dubbo-api→dubbo-domains,服务与服务之间只有运行期调用,没有编译期依赖。 - 数据库层面
tb_order.userId只是普通字段,不建外键------跨服务不能有数据库级耦合,关联关系留给应用层。 - 验收只认一件事:
GET /order/101回来的 JSON 里user字段有值。