Java 项目实战: 交友项目 - SpringCloud 整合 Dubbo 的需求分析与模块规划

概述

订单服务只知道自己表里的 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 字段有值。
相关推荐
哦虎!1 小时前
【JavaEE】文件操作IO
java
十年Java程序媛1 小时前
Redis 分布式锁 + GZIP 压缩缓存踩坑|高并发缓存真实线上复盘
java·开发语言·spring boot·redis·分布式·缓存·面试
顽疲2 小时前
Java vue 养老系统源码详解:入住核算单 live-check 缴费入账与预交金收据实践
java·vue.js·python·java 养老·springboot 养老·vue养老
共饮一杯无2 小时前
LiveKit本地录制落盘路径排查与修复实践
java·go·livekit
流烟默2 小时前
Spring Cloud 微服务 Sentinel 热点参数与授权规则集成指南(附架构建议)
spring cloud·微服务·架构·sentinel
StevenLdh2 小时前
第八期 · 情绪闯关挑战盒:一个「无惩罚」成就系统的数据建模
java·微信小程序
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 入门案例:编写 Dubbo 服务消费者
java·dubbo·交友
IT研究室3 小时前
最新计算机毕业设计选题推荐-基于spring boot的教育学习平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
亚林瓜子3 小时前
Spring Boot 集成 ip2region 离线 IP 库实战指南
java·spring boot·后端·ip·ip2region