Spring Cloud Alibaba 微服务实战:IDEA 手把手从零搭建到部署
2026-06-24 09:39:00 · 标签:Spring Cloud、微服务、教程、IDEA、Docker、CI/CD
引言
网上 Spring Cloud 教程很多,但大多数都是"贴一段代码 -> 贴下一段代码",读者根本不知道这段 XML 写在哪、那个注解导什么包、为什么这里要这样配。
本文的目标:假设你刚装好 IntelliJ IDEA,我们从创建项目的第一秒开始,一步一步搭出一套完整的微服务系统。 每一步都会说清楚三个问题:
- 在哪写 -- 这个文件在 IDEA 里怎么创建出来的
- 写什么 -- 完整代码,包括 import 和 package
- 为什么 -- 这行配置是干什么的,不写行不行
我们的业务场景是:一个电商平台,包含用户、订单、商品三个核心服务。
一、项目初始化:父子工程搭建(IDEA 版)
1.1 版本选型
关于版本号:以下版本为写作时的推荐组合。Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者有严格的版本兼容关系。另外注意:Spring Cloud 2025.0 起,Gateway 的 Maven artifact 从spring-cloud-starter-gateway改为spring-cloud-starter-gateway-server-webflux,配置文件前缀也变成spring.cloud.gateway.server.webflux.*。本文已使用新名称,如果你参考其他旧教程,注意这个差异。
截至 2026 年 6 月,推荐的版本组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 4.0.6 | 微服务单个服务的基础框架(2025年11月发布) |
| Spring Cloud | 2025.1.0 | 微服务治理套件(网关、远程调用等) |
| Spring Cloud Alibaba | 2025.1.0.0 | 阿里开源的微服务组件(Nacos、Sentinel、Seata) |
| Java | 25 | 当前最新 LTS,Spring Boot 4.x 推荐的 Java 版本 |
| Maven | 3.6+ | IDEA 自带,不用单独装 |
| IntelliJ IDEA | 2026.x | Ultimate 或 Community 版均可 |
这三个"版本号"之间的关系,新手很容易搞混:
- Spring Boot 是"地基":每个微服务都是一个 Spring Boot 应用
- Spring Cloud 是"上层建筑":在 Spring Boot 之上提供微服务能力
- Spring Cloud Alibaba 是"建材供应商":用 Nacos 代替 Eureka,用 Sentinel 代替 Hystrix
它们之间有严格的版本兼容关系。简单记:Spring Cloud 2025.1.x 必须配 Spring Boot 4.0.x,Spring Cloud Alibaba 2025.1.0.x 必须配 Spring Cloud 2025.1.x。 不要自己乱搭版本,否则会出现各种奇怪的兼容问题。
Spring Boot 4 的重要变化:从 4.0 开始,Spring Boot 升级到 Jakarta EE 11(javax.* 全面移除)、Jackson 3(包名变为tools.jackson)、Tomcat 11。同时 bootstrap.yml 被正式废弃,Nacos 等配置中心的连接信息改为通过spring.config.import写在application.yml中,后面会具体说明。
1.2 创建父工程
在 IDEA 中创建一个 Maven 项目(用 IDEA 自带的 Maven 项目模板即可,不需要 Spring Initializr -- 父工程本身不运行,只管理子模块)。
项目基本信息:
| 字段 | 值 | 说明 |
|---|---|---|
| Name | shop-cloud | 项目名,也是最终文件夹名 |
| Location | D:\projects\shop-cloud | 你喜欢的路径即可 |
| GroupId | com.shop | 组织标识,倒置域名格式 |
| ArtifactId | shop-cloud | 项目标识,一般跟 Name 一致 |
| Version | 1.0.0 | 初始版本 |
JDK 选择 25(Spring Boot 4.x 要求 Java 17+,25 是当前 LTS)。
创建完成后,删掉 src 目录。父工程不需要 src -- 为什么?因为父工程只做两件事:
- 声明有哪些子模块(
) - 统一管理所有 jar 包的版本(
)
它自己不写代码,所以不需要 src。
现在,打开 pom.xml,你会看到 IDEA 自动生成的最简配置。把 packaging 改成 pom,然后加上以下内容。不要怕长,我一行一行解释。
父 POM 完整内容(shop-cloud/pom.xml):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ======================================== -->
<!-- 父工程自身坐标 -->
<!-- ======================================== -->
<groupId>com.shop</groupId>
<artifactId>shop-cloud</artifactId>
<version>1.0.0</version>
<!--
packaging 设为 pom 是关键:
只有 <packaging>pom</packaging>,Maven 才允许你:
1. 通过 <modules> 声明子模块
2. 通过 <dependencyManagement> 统一管理版本
这个值默认为 jar,不改成 pom 后面全报错
-->
<packaging>pom</packaging>
<name>shop-cloud</name>
<description>电商微服务父工程</description>
<!-- ======================================== -->
<!-- 子模块列表(下一步逐个创建) -->
<!-- ======================================== -->
<modules>
<module>shop-common</module>
<module>shop-gateway</module>
<module>shop-user</module>
<module>shop-order</module>
<module>shop-goods</module>
<module>shop-sync</module>
</modules>
<!-- ======================================== -->
<!-- 统一版本号:properties 集中定义 -->
<!-- ======================================== -->
<!--
properties 的作用:把所有版本号集中在一处定义。
好处:升级时只改这一处,不用在全文件里一个个翻。
注意:这里只是声明了"这个变量是多少",并没有让任何 jar 生效。
-->
<properties>
<java.version>25</java.version>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 核心框架版本 -->
<spring-boot.version>4.0.6</spring-boot.version>
<spring-cloud.version>2025.1.0</spring-cloud.version>
<spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
<!-- 常用工具库版本 -->
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<hutool.version>5.8.34</hutool.version>
<lombok.version>1.18.36</lombok.version>
<mysql-connector.version>9.1.0</mysql-connector.version>
</properties>
<!-- ======================================== -->
<!-- dependencyManagement:只声明版本,不实际引入 -->
<!-- ======================================== -->
<!--
重点!!!这是新手最容易糊涂的地方。
<dependencyManagement> 的作用是"版本仲裁",不是"依赖引入"。
写在 <dependencyManagement> 里的依赖,不会实际下载到项目中。
它的用途是:当某个子模块用到了这个 jar 包时,
Maven 会自动使用这里声明的版本,子模块就不需要再写 <version> 了。
举个例子:
- 父 POM 的 <dependencyManagement> 里声明了 hutool 5.8.28
- shop-user 的 <dependencies> 里直接写 hutool,不写版本号
- Maven 编译时自动用 5.8.28
这样做的意义:避免 6 个子模块各自声明不同版本的 hutool,
导致最终打包时不知道用哪个版本(依赖冲突)。
那依赖真正写在哪?写在各子模块自己的 <dependencies> 块里。
父 POM 只"声明可选范围",子模块自己"决定用不用"。
-->
<dependencyManagement>
<dependencies>
<!--
Spring Boot 依赖管理 BOM。
"type=pom + scope=import" 组合 = 把这个 POM 里定义的所有
依赖版本"导入"到当前 POM 的 dependencyManagement 中。
BOM = Bill of Materials,可以理解为"物料清单"。
导入后,所有 Spring Boot 官方组件(web、jdbc、redis 等)
的版本都由 spring-boot-dependencies 帮你控制好了。
你在子模块引入 spring-boot-starter-web 时,不需要写版本号。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--
Spring Cloud 依赖管理 BOM。
导入后,gateway、openfeign、loadbalancer 等 Spring Cloud
组件的版本都由这里统一管理。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--
Spring Cloud Alibaba 依赖管理 BOM。
导入后,Nacos、Sentinel、Seata 等阿里组件的版本由这里管理。
-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--
下面这三个不是 BOM 导入,而是直接声明版本。
因为 MyBatis-Plus、Hutool、MySQL 驱动不是 Spring 生态的,
它们没有自己的 BOM POM 可以 import,所以要显式写出来。
再次强调:这里只是"声明版本供子模块参考",
不会实际下载这些 jar 包。只有子模块在 <dependencies> 里
引用了它们,Maven 才会去下载。
-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- ======================================== -->
<!-- 编译插件:指定 Java 版本 -->
<!-- ======================================== -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<source>25</source>
<target>25</target>
<!--
下面这一行是为了让 Lombok 的注解处理器
在编译时正常工作。没有它,Lombok 可能不会生效。
-->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
写完父 POM 后,IDE 会提示 Maven 配置有变化,点击 Load Maven Changes(重新加载 Maven 项目)让配置生效。
1.3 公共模块 shop-common:手把手创建
这是新手最容易卡住的地方。我们先搞清楚三个问题:
问题一:shop-common 是什么?
想象你要写 6 个微服务。每个服务都要用到一个"统一响应格式"——成功时返回 {code: 200, message: "success", data: ...},失败时返回 {code: 500, message: "库存不足", data: null}。
如果没有 shop-common,你需要把这套代码在 6 个服务里各复制一份。哪天要改响应格式(比如加个 timestamp 字段),你得改 6 处。
有了 shop-common,把这套代码写一次,打成 jar 包。6 个服务各引入这个 jar,一处修改全局生效。这就是"公共模块"的意义。
问题二:shop-common 要依赖什么?
shop-common 只需要依赖两个东西:
- Lombok -- 用来通过注解自动生成 getter/setter/构造方法,省得手写
- Spring Web -- 因为里面有
@RestControllerAdvice这类注解(用于全局异常处理)
注意:shop-common 不需要数据库(MyBatis-Plus)、不需要 Nacos 注册——那些是业务模块自己的事。公共模块越轻越好。
重要警告:Gateway 不能引入 shop-common! 因为 shop-common 依赖了 spring-boot-starter-web(Servlet 栈),而 Gateway 基于 WebFlux(响应式栈),两者互斥。把 Servlet 和 WebFlux 放在同一个 classpath 下会导致启动失败,报 "Spring MVC found on classpath" 之类的错误。网关模块如果需要统一响应格式,直接在网关内部定义一个轻量 Result 类即可,不要图省事引入 shop-common。
问题三:依赖写在父 POM 还是 shop-common 自己的 POM?
这是最核心的问题。答案:
| POM 位置 | 标签 | 作用 |
|---|---|---|
| 父 POM | | 说"Lombok 的版本是 1.18.34",但不下载 |
| shop-common POM | | 说"我要用 Lombok",Maven 就去下载 1.18.34 |
打个比方:父 POM 是"菜单(列出有什么菜、什么价)",子模块 POM 是"点菜(我要这个、那个)"。你光有菜单不行,得点菜才给你上菜。
现在开始创建:
第 1 步:创建 shop-common 子模块
在父工程下新建一个 Maven 模块(IDE 中右键父工程 -> 新建模块,选 Maven 类型即可,不用 Spring Initializr)。
第 2 步:填写模块信息:
| 字段 | 值 |
|---|---|
| Name | shop-common |
| ArtifactId | shop-common(一般自动同步 Name) |
| GroupId | com.shop |
创建完成后:
- 父 POM 的
中会自动添加(如果没有,手动加上)shop-common shop-common的 POM 中标签已经自动指向父工程
第 4 步:写 shop-common 的 pom.xml
新建模块后,IDEA 给你生成了一个初始的 shop-common/pom.xml。把它改成这样:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ======================================== -->
<!-- 指向父工程 -->
<!-- ======================================== -->
<!--
这三行是 Maven 父子关系的核心:
- parent:声明我的父 POM 是谁
- relativePath:父 POM 的文件位置。
默认值是 ../pom.xml,刚好就是上一级目录
IDEA 创建模块时会自动生成这三行,一般不需要你手动写
-->
<parent>
<groupId>com.shop</groupId>
<artifactId>shop-cloud</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<!--
子模块自身的坐标。
groupId 和 version 可以从 parent 继承,所以只需要写 artifactId。
实际上你写了 groupId 和 version 也没事,Maven 会拿 parent 的覆盖掉。
-->
<artifactId>shop-common</artifactId>
<!-- 不写 groupId,从 parent 继承 com.shop -->
<!-- 不写 version,从 parent 继承 1.0.0 -->
<name>shop-common</name>
<description>公共模块:统一响应体、工具类、异常</description>
<!-- ======================================== -->
<!-- 这里才是真正"引入依赖"的地方 -->
<!-- ======================================== -->
<dependencies>
<!--
注意:这里没有写 <version>!!!
因为父 POM 的 <dependencyManagement> 已经声明了版本。
子模块直接引用,Maven 自动找父 POM 里声明的版本。
如果你在这里也写了 <version>,会覆盖父 POM 的声明。
但这不推荐——版本应该统一在父 POM 管理。
-->
<!-- Lombok:帮你自动生成 getter/setter/toString/构造方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- 不写 version,由父 POM 控制 -->
</dependency>
<!--
Spring Web 相关。
为什么公共模块需要 Spring Web?
因为 GlobalExceptionHandler 用到了 @RestControllerAdvice,
这个注解来自 spring-web 包。
如果不引入,IDEA 编译会报"找不到符号"。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 不写 version,由 spring-boot-dependencies BOM 控制 -->
</dependency>
</dependencies>
</project>
Lombok 额外一步(重要!):写完 POM 后,还需要安装 IDE 的 Lombok 插件,否则 IDE 会一直标红 @Data 说找不到 getter/setter。在插件市场搜索 "Lombok" 安装并重启 IDE。同时确保 IDE 设置的 Annotation Processing(注解处理)已开启,否则编译时 Lombok 不会生成代码。
第 3 步:创建 Java 源文件
在 shop-common/src/main/java 下创建包 com.shop.common,然后在这个包下创建三个类。
文件一:统一响应体 Result.java
路径:shop-common/src/main/java/com/shop/common/Result.java
package com.shop.common;
// 下面这些 import 缺一不可
import lombok.AllArgsConstructor; // 生成全参构造方法
import lombok.Data; // 生成 getter/setter/toString/equals/hashCode
import lombok.NoArgsConstructor; // 生成无参构造方法
/**
* 统一响应体。
* 整个项目的所有接口都返回这个格式,
* 前端只需要处理一种结构。
*
* 示例:
* 成功 -> {"code":200, "message":"success", "data":{...}}
* 失败 -> {"code":500, "message":"库存不足", "data":null}
*
* @param <T> 实际返回的数据类型,比如 UserVO、OrderVO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
/** 状态码:200 成功,其他为错误码 */
private int code;
/** 提示信息:成功时为 "success",失败时描述原因 */
private String message;
/** 实际返回的数据,失败时为 null */
private T data;
// ---- 静态工厂方法,方便快捷构造 Result 对象 ----
/**
* 成功时调用,比如 return Result.ok(user);
*/
public static <T> Result<T> ok(T data) {
return new Result<>(200, "success", data);
}
/**
* 失败时调用(默认错误码 500),比如 return Result.fail("库存不足");
*/
public static <T> Result<T> fail(String message) {
return new Result<>(500, message, null);
}
/**
* 失败时调用(自定义错误码),比如 return Result.fail(401, "请先登录");
*/
public static <T> Result<T> fail(int code, String message) {
return new Result<>(code, message, null);
}
}
关键理解:
@Data是 Lombok 的组合注解,等于同时加了@Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor。编译时 Lombok 会为你生成所有这些方法的字节码——所以源码里看不到 getter/setter,但编译后的 .class 文件里有。@NoArgsConstructor生成无参构造new Result()(JSON 反序列化时需要)@AllArgsConstructor生成全参构造new Result(200, "success", data)是泛型,这样返回用户数据时用Result,返回订单数据时用Result,类型安全- 三个
static工厂方法是语法糖:Result.ok(user)比new Result<>(200, "success", user)更可读
文件二:业务异常 BizException.java
路径:shop-common/src/main/java/com/shop/common/BizException.java
package com.shop.common;
/**
* 业务异常。
* 当业务规则被违反时(如库存不足、用户不存在),
* 抛出这个异常而不是直接返回 Result.fail()。
* 这样 Service 层专注于业务逻辑,
* 异常由 GlobalExceptionHandler 统一处理并转成 Result 格式。
*
* 使用示例:
* throw new BizException("库存不足");
* throw new BizException(401, "请先登录");
*/
public class BizException extends RuntimeException {
/** 错误码,默认 500 */
private final int code;
/**
* 默认错误码 500
*/
public BizException(String message) {
super(message);
this.code = 500;
}
/**
* 自定义错误码,比如 401 未登录、403 无权限
*/
public BizException(int code, String message) {
super(message);
this.code = code;
}
/** 获取错误码(供 GlobalExceptionHandler 使用) */
public int getCode() {
return code;
}
}
为什么不用 Lombok? BizException 继承了 RuntimeException,字段只有 code 一个——手写 getter 和两个构造方法就几行代码,不值得引入 Lombok(虽然用了也没问题)。
文件三:全局异常处理器 GlobalExceptionHandler.java
路径:shop-common/src/main/java/com/shop/common/GlobalExceptionHandler.java
package com.shop.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器。
* 任何微服务引入 shop-common 后,这个类自动生效。
* 不需要在每个 Controller 里写 try-catch。
*
* 工作原理:
* @RestControllerAdvice 会拦截所有 @RestController 中抛出的异常,
* 按 @ExceptionHandler 声明的方法逐一匹配,匹配到就处理并返回。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 捕获业务异常。
* 返回格式:{"code":xxx, "message":"xxx", "data":null}
*/
@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 兜底:捕获所有未被上面处理的异常。
* 防止直接抛 500 到前端,暴露堆栈信息。
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.fail("服务器内部错误");
}
}
现在 shop-common 模块就完成了。总结一下你做了什么:
- 创建了
shop-commonMaven 模块(父工程下新建模块) - 在
shop-common/pom.xml中引入了 Lombok 和 Spring Web 两个依赖(没有写版本号,版本由父 POM 管理) - 创建了
com.shop.common包,里面有 3 个类:Result(响应体)、BizException(业务异常)、GlobalExceptionHandler(异常处理器)
其他模块(shop-user、shop-order 等)只需要在 pom.xml 中引入 shop-common,就能直接使用这三个类。但注意:Gateway 模块不能引入 shop-common,因为 Spring Web(Servlet)与 Gateway(WebFlux)互斥。
二、Nacos:注册中心 + 配置中心
Nacos 是微服务的"通讯录"和"配置中心"二合一。
- 注册中心:shop-user 启动后向 Nacos 报到,shop-order 问 Nacos"shop-user 在哪",Nacos 告诉它地址
- 配置中心:不用在每个服务里写数据库密码,统一配在 Nacos 里,改一处全生效
2.1 部署 Nacos
Nacos 本身是一个 Java 应用,我们需要先把它跑起来,后面的微服务才能注册上去。
方式一:直接运行(推荐,不需要额外安装任何工具)
你已经装了 JDK 25,Nacos 开箱即用。
第 1 步:打开 Nacos Release 页面,找到 3.1.1 版本,下载 nacos-server-3.1.1.zip(约 130MB)。
第 2 步:解压到任意目录,比如 D:\nacos。解压后目录结构大概是这样:
D:\nacos\
bin\ <- 启动脚本在这里
conf\ <- 配置文件
target\ <- Nacos 的 jar 包
第 3 步:打开终端(Win+R 输入 cmd),进入 D:\nacos\bin,运行:
# Windows(双击 startup.cmd 也行)
startup.cmd -m standalone
# Mac / Linux
sh startup.sh -m standalone
-m standalone 表示单机模式。Nacos 默认是集群模式(需要多台机器),开发学习用单机模式足够。
第 4 步:等待约 10 秒,浏览器访问 http://localhost:8848/nacos,看到登录页就说明 Nacos 启动成功了。默认用户名和密码都是 nacos。
登录后你会看到 Nacos 的控制台界面——左边菜单有"服务管理"和"配置管理",我们很快就要用到它们。
方式二:Docker 部署(如果你已经装了 Docker)
如果你熟悉 Docker,也可以用一条命令启动:
docker run -d --name nacos \
-e MODE=standalone \
-p 8848:8848 -p 9848:9848 \
nacos/nacos-server:v3.1.1
同样访问 http://localhost:8848/nacos 即可。
没有 Docker?完全没关系。 直接用方式一,后面部署 Sentinel 也是用 java -jar 直接启动。Docker 会在第十章详细介绍,到时候你会发现它的真正价值——一键启动全套环境(MySQL + Redis + Nacos + 微服务集群),而不是一个个手动启。
2.2 创建第一个微服务 shop-user
现在我们要创建第一个真正的微服务模块。和创建 shop-common 的步骤一样:
第 1 步:在父工程下新建 Maven 模块,也可以选 Spring Initializr(有 Spring Boot 启动类,IDE 可以帮我们初始化依赖)。
第 2 步:填写信息:
| 字段 | 值 |
|---|---|
| Name | shop-user |
| Group | com.shop |
| Artifact | shop-user |
| Package | com.shop.user |
| Spring Boot | 4.0.6 |
第 3 步:依赖先勾选 Spring Web(后面的依赖手动加,更清楚每个依赖的作用)。
第 4 步:打开 shop-user/pom.xml,如果是用 Spring Initializr 生成的,需要把 从 spring-boot-starter-parent 改为我们的父工程 shop-cloud。
完整的 shop-user/pom.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.shop</groupId>
<artifactId>shop-cloud</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>shop-user</artifactId>
<dependencies>
<!-- 引入 shop-common,就可以用 Result、BizException 了 -->
<dependency>
<groupId>com.shop</groupId>
<artifactId>shop-common</artifactId>
<version>${project.version}</version>
<!--
${project.version} 是 Maven 内置变量,等于当前项目的版本号。
因为父工程是 1.0.0,所以这里就是 1.0.0。
这样写的好处:父工程版本升级时不用改每一个子模块。
-->
</dependency>
<!-- Nacos 服务注册:让 shop-user 启动时向 Nacos 报到 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos 配置中心:让 shop-user 从 Nacos 拉取配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--
Spring Cloud LoadBalancer:服务间负载均衡。
Spring Cloud 2020 之后不再内置 Ribbon,
Nacos 2.x 必须加这个,否则 Feign 调用会报
"No instances available for shop-user"。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
第 5 步:创建 application.yml
在 shop-user/src/main/resources 目录下,新建文件 application.yml:
在 shop-user/src/main/resources 目录下新建 application.yml 文件。
重要变化:Spring Cloud 2025.x 正式废弃了bootstrap.yml。以前 Nacos 的连接信息必须写在 bootstrap 里(因为要先连 Nacos 才能拉配置),现在统一改用spring.config.import写在application.yml中。Spring Boot 4 会在加载本地配置之前先解析spring.config.import,自然解决了"鸡生蛋"问题。
server:
port: 8081 # 固定端口,避免每次重启随机换端口
spring:
application:
name: shop-user # 服务名,注册到 Nacos 时的标识
# ========================================
# 关键!!!Spring Boot 4.x 废弃了 bootstrap.yml,
# 必须通过 spring.config.import 显式声明从 Nacos 导入配置。
# 不写这一行,启动时直接报错:
# "No spring.config.import property has been defined"
# optional: 前缀表示 Nacos 连不上时不会阻止应用启动,
# 方便本地开发时不想启动 Nacos 的情况。
# ========================================
config:
import:
- optional:nacos:${spring.application.name}.yaml
- optional:nacos:common-datasource.yaml
cloud:
nacos:
# ---- 注册中心配置 ----
discovery:
server-addr: localhost:8848
username: nacos # Nacos 认证用户名(默认 nacos)
password: nacos # Nacos 认证密码(默认 nacos)
namespace: dev # 命名空间:隔离开发/测试/生产环境
group: SHOP_GROUP # 分组:同一环境内可按业务分组
# ---- 配置中心配置 ----
config:
server-addr: localhost:8848
username: nacos # Nacos 认证用户名
password: nacos # Nacos 认证密码
namespace: dev
group: SHOP_GROUP
file-extension: yaml # 配置文件的格式,默认 properties
# 共享配置:多个微服务共用的配置提取到这里
# 比如数据源配置,shop-user/shop-order/shop-goods 都要用
shared-configs:
- data-id: common-datasource.yaml
group: SHOP_GROUP
refresh: true # Nacos 上改了之后自动刷新本地配置
profiles:
active: dev
第 6 步:创建启动类
shop-user/src/main/java/com/shop/user/ShopUserApplication.java:
package com.shop.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* shop-user 微服务启动类。
*
* @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
* @EnableDiscoveryClient 让服务启动后向 Nacos 注册
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ShopUserApplication {
public static void main(String[] args) {
SpringApplication.run(ShopUserApplication.class, args);
}
}
第 7 步:在 Nacos 控制台创建共享配置
浏览器打开 http://localhost:8848/nacos -> 左侧"配置管理" -> "配置列表"。
右上角选择命名空间 dev,点击 + 新建配置:
- Data ID:
common-datasource.yaml - Group:
SHOP_GROUP - 配置格式:
YAML - 配置内容:
spring:
datasource:
url: jdbc:mysql://localhost:3306/shop_user?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD:root123}
driver-class-name: com.mysql.cj.jdbc.Driver
这里 ${DB_PASSWORD:root123} 的意思是:先从环境变量 DB_PASSWORD 取,取不到就用默认值 root123。生产环境通过 Docker 容器的环境变量注入真实密码,避免写在配置文件里。
第 8 步:验证
运行 ShopUserApplication 的 main 方法。启动成功后,打开 Nacos 控制台 -> 服务管理 -> 服务列表,在 dev 命名空间下能看到 shop-user,就说明注册成功。
三、Gateway:统一网关
网关是所有外部请求的入口。前端调用 http://localhost:8080/api/user/9527,网关把这个请求转发给 shop-user 服务的 /user/9527。
3.1 创建网关模块
和之前一样:在父工程下新建模块。依赖选择页面勾选 Gateway(而不是 Spring Web)。因为 Gateway 基于 WebFlux(异步非阻塞),跟 Spring Web(同步阻塞)互斥。
依赖选好后创建。然后修改 shop-gateway/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.shop</groupId>
<artifactId>shop-cloud</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>shop-gateway</artifactId>
<dependencies>
<!--
重要:这里不引入 shop-common!
因为 shop-common 依赖了 spring-boot-starter-web(Servlet 栈),
而 Gateway 基于 WebFlux(响应式栈),两者互斥,放一起会导致启动失败。
如果网关需要统一响应格式,直接在网关模块内定义一个轻量 Result 类即可。
-->
<!--
Spring Cloud Gateway 核心(WebFlux 响应式版)。
重要:Spring Cloud 2025.0 起废弃了 spring-cloud-starter-gateway,
改名为 spring-cloud-starter-gateway-server-webflux。
用旧名字在 2025.x BOM 中找不到!
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<!-- 网关也要连 Nacos,这样才能通过服务名找到下游 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--
关键警告:千万不要引入 spring-boot-starter-web!
Gateway 基于 Spring WebFlux(响应式),
跟传统的 Spring Web(Servlet)不兼容。
两个同时存在会导致项目启动失败,报
"Spring MVC found on classpath" 之类的错误。
-->
</dependencies>
</project>
Spring Cloud 2025 Gateway 改名:Spring Cloud 2025.0 废弃了spring-cloud-starter-gateway,拆分为两个独立 starter: -spring-cloud-starter-gateway-server-webflux(WebFlux 版,即原来的 gateway) -spring-cloud-starter-gateway-server-webmvc(新增的 Servlet 版) 用旧名字会直接报"找不到依赖"。同理,配置文件前缀也变了:spring.cloud.gateway.->spring.cloud.gateway.server.webflux.,详见下方路由配置。
3.2 路由配置
在 shop-gateway/src/main/resources 下创建 application.yml:
spring:
application:
name: shop-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: dev
group: SHOP_GROUP
gateway:
# Spring Cloud 2025 起,gateway 配置前缀改为 server.webflux
server:
webflux:
routes:
# 路由规则:访问 /api/user/** 的请求转发到 shop-user 服务
- id: shop-user
uri: lb://shop-user # lb:// 表示从 Nacos 获取地址并负载均衡
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1 # 去掉路径中的 /api(/api/user/123 -> /user/123)
- id: shop-goods
uri: lb://shop-goods
predicates:
- Path=/api/goods/**
filters:
- StripPrefix=1
- id: shop-order
uri: lb://shop-order
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
# 全局默认过滤器
default-filters:
# 解决 CORS 重复 header 问题
- DedupeResponseHeader=Access-Control-Allow-Origin
3.3 启动类
package com.shop.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ShopGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ShopGatewayApplication.class, args);
}
}
四、OpenFeign:服务间远程调用
当一个服务需要调用另一个服务的接口时,用 OpenFeign。它让你像调用本地方法一样调用远程接口——底层自动帮你发 HTTP 请求、解析 JSON 响应。
哪些模块需要加 OpenFeign? 原则很简单:谁调别人,谁加。
| 模块 | 是否需要 OpenFeign | 原因 |
|---|---|---|
| shop-order | 需要 | 下单要调 shop-user 查用户、调 shop-goods 查库存/扣库存 |
| shop-user | 不需要 | 纯被调方,只对外提供接口 |
| shop-goods | 不需要 | 纯被调方,只对外提供接口 |
| shop-gateway | 不需要 | 网关用自己的路由机制转发,不走 Feign |
| shop-common | 不需要 | 公共 jar 包,不涉及远程调用 |
| shop-sync | 需要(如果调别的服务) | 数据同步完成后可能要通知缓存刷新等 |
下面以 shop-order 为例,它需要调用 shop-user 和 shop-goods 两个服务。
4.1 创建 shop-order 模块
和创建 shop-user 一样的步骤。shop-order/pom.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.shop</groupId>
<artifactId>shop-cloud</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>shop-order</artifactId>
<dependencies>
<dependency>
<groupId>com.shop</groupId>
<artifactId>shop-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- OpenFeign:声明式 HTTP 客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
4.2 Feign 接口及使用
完整的下单流程代码已在下方给出。注意文件路径:
文件一:shop-order/src/main/java/com/shop/order/feign/UserClient.java(新建 feign 包)
package com.shop.order.feign;
import com.shop.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 调用 shop-user 服务的 Feign 接口。
*
* @FeignClient name = "shop-user" 对应 Nacos 中注册的服务名。
* path = "/user" 是 shop-user 服务中 Controller 的 @RequestMapping 前缀。
*/
@FeignClient(name = "shop-user", path = "/user")
public interface UserClient {
@GetMapping("/{userId}")
Result<UserVO> getUserById(@PathVariable Long userId);
}
本地 VO 类 shop-order/src/main/java/com/shop/order/feign/UserVO.java:
package com.shop.order.feign;
import lombok.Data;
/**
* 用户信息 VO。
* 注意:这个类定义在调用方(shop-order),不是提供方(shop-user)。
* 只定义自己需要的字段,不需要把用户表的全部字段都列出来。
*/
@Data
public class UserVO {
private Long id;
private String username;
private String phone;
}
文件二:shop-order/src/main/java/com/shop/order/feign/GoodsClient.java
package com.shop.order.feign;
import com.shop.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "shop-goods", path = "/goods")
public interface GoodsClient {
@GetMapping("/{goodsId}")
Result<GoodsVO> getGoodsById(@PathVariable Long goodsId);
@PutMapping("/stock/deduct")
Result<Void> deductStock(@RequestBody StockDeductDTO dto);
}
GoodsVO(shop-order/src/main/java/com/shop/order/feign/GoodsVO.java)——同样只定义调用方需要的字段:
package com.shop.order.feign;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class GoodsVO {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
}
StockDeductDTO(shop-order/src/main/java/com/shop/order/feign/StockDeductDTO.java)——扣库存的请求体:
package com.shop.order.feign;
import lombok.Data;
@Data
public class StockDeductDTO {
/** 商品 ID */
private Long goodsId;
/** 扣减数量 */
private Integer count;
}
注意:DTO(Data Transfer Object)和 VO 都定义在调用方(shop-order),不是提供方(shop-goods)。调用方只定义自己需要的字段,避免引入不必要的依赖。shop-goods 服务内部有自己的 Goods 实体类,字段更全——但调用方不关心那些。
shop-order 的 application.yml(shop-order/src/main/resources/application.yml):
server:
port: 8082 # shop-user 用 8081,这里用 8082,避免端口冲突
spring:
application:
name: shop-order
config:
import:
- optional:nacos:${spring.application.name}.yaml
- optional:nacos:common-datasource.yaml
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: dev
group: SHOP_GROUP
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: dev
group: SHOP_GROUP
file-extension: yaml
shared-configs:
- data-id: common-datasource.yaml
group: SHOP_GROUP
refresh: true
profiles:
active: dev
端口分配建议:shop-user:8081、shop-order:8082、shop-goods:8083、shop-gateway:8080。统一规划避免冲突。
启动类 shop-order/src/main/java/com/shop/order/ShopOrderApplication.java:
package com.shop.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 必须加这个,否则 @FeignClient 不生效
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class, args);
}
}
五、Sentinel:流量控制与熔断降级
Sentinel 是微服务的"保险丝"。当流量暴增时(秒杀、大促),自动限流;当下游服务挂了时,自动熔断,防止故障扩散。
5.1 部署 Sentinel 控制台
从 Sentinel Release 页面 下载 sentinel-dashboard-1.8.9.jar,放在项目根目录。
在 IDEA Terminal 中运行:
java -Dserver.port=8858 \
-Dcsp.sentinel.dashboard.server=localhost:8858 \
-Dsentinel.dashboard.auth.username=admin \
-Dsentinel.dashboard.auth.password=admin123 \
-jar sentinel-dashboard-1.8.9.jar
访问 http://localhost:8858,用户名 admin,密码 admin123。
5.2 各模块接入 Sentinel
Sentinel 有两个不同的 starter,不能混用:
| Sentinel 模块 | 适用模块 | 作用 |
|---|---|---|
spring-cloud-starter-alibaba-sentinel | shop-user、shop-order、shop-goods | 保护 REST 接口(限流/熔断/降级),同时也内置了 Feign 熔断 |
spring-cloud-alibaba-sentinel-gateway | shop-gateway(只此一家) | 网关专属:按路由 ID 限流,基于 WebFlux,跟上面的互斥 |
为什么 Gateway 不能用普通版? spring-cloud-starter-alibaba-sentinel 间接依赖了 Spring Web(Servlet),而 Gateway 是 WebFlux 栈。两者放一起启动会报 "Spring MVC found on classpath"。所以网关必须用 spring-cloud-alibaba-sentinel-gateway。
Feign 熔断需要额外加依赖吗?不需要。 spring-cloud-starter-alibaba-sentinel 已经内置了 Feign 的 Sentinel 集成。只需在 application.yml 中开一个开关即可(见下方配置),然后在 @FeignClient 注解上指定 fallback 类。
shop-order 的 pom.xml 追加(只需一个依赖):
<!-- 同时保护 REST 接口 + Feign 熔断,一个就够了 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
shop-gateway 的 pom.xml 追加(注意:不是普通版!):
<!-- 网关专属 Sentinel,基于 WebFlux,不会跟 Gateway 冲突 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
Sentinel 依赖找不到? 跟 Gateway 不同,Sentinel 的 artifact 名没改。90% 的情况是 Spring Cloud Alibaba 的 BOM 没拉下来(父 POM 里spring-cloud-alibaba-dependencies:2025.1.0.0没解析成功),导致所有阿里系依赖都没有版本号。排查三步: 1. 先在 IDEA 的 Maven 面板看父 POM 的spring-cloud-alibaba-dependencies是否报红——如果报红,说明 BOM 没拉到,先解决 BOM 的问题 2. 终端执行mvn dependency:resolve -U强制更新,看哪个 jar 卡住了 3. 如果 Maven 中央仓库同步慢,在settings.xml加阿里云镜像:https://maven.aliyun.com/repository/public
在 application.yml 中加 Sentinel 配置(以 shop-order 为例,shop-user/goods 类似):
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8858 # Sentinel 控制台地址
port: 8719 # 心跳端口,每台实例要不一样
datasource:
flow:
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
data-id: ${spring.application.name}-flow-rules
group-id: SENTINEL_GROUP
data-type: json
rule-type: flow
# Feign 集成 Sentinel:开启后 @FeignClient(fallback=...) 才生效
feign:
sentinel:
enabled: true
feign.sentinel.enabled=true是关键:不开启的话,Feign 接口不会走 Sentinel 代理,@FeignClient(fallback=...)写了也不会触发。这个能力内置在spring-cloud-starter-alibaba-sentinel里,不需要额外的 circuitbreaker 依赖。
六、分布式事务:Seata
下单操作跨越三个服务:订单(写订单)、商品(扣库存)、用户(扣积分)。任何一个失败,前面的都要回滚。
Seata AT 模式原理:
- 第一阶段:各服务执行本地事务,Seata 自动记录 undo_log
- 第二阶段:全成功则删 undo_log;有失败则用 undo_log 回滚
每个业务服务只需加一个依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在事务方法上加 @GlobalTransactional 即可。业务代码基本不用改。
七、RocketMQ:异步消息解耦
下单后需要发短信、扣优惠券、写行为日志、同步 ES。这些都跟"下单"这个核心动作无关,应该异步处理。
7.1 快速部署
RocketMQ 也是 Java 应用,可以直接运行。
方式一:直接运行(推荐)
第 1 步:从 RocketMQ 下载页面 下载 rocketmq-all-5.3.1-bin-release.zip,解压到 D:\rocketmq。
第 2 步:先启动 NameServer(RocketMQ 的"注册中心",类似 Nacos 的角色):
# Windows
D:\rocketmq\bin\mqnamesrv.cmd
# Mac / Linux
sh bin/mqnamesrv
第 3 步:再开一个终端,启动 Broker(RocketMQ 的"消息存储和转发"服务):
# Windows
D:\rocketmq\bin\mqbroker.cmd -n localhost:9876
# Mac / Linux
sh bin/mqbroker -n localhost:9876
NameServer 默认端口 9876,Broker 默认端口 10911。两个都启动成功后,RocketMQ 就可以用了。
方式二:Docker 部署(如果你已经装了 Docker)
# NameServer
docker run -d --name rmq-namesrv \
-p 9876:9876 \
apache/rocketmq:5.3.1 sh mqnamesrv
# Broker(注意 --link 连到 NameServer)
docker run -d --name rmq-broker \
-p 10911:10911 \
-e "JAVA_OPT_EXT=-Xms512m -Xmx512m" \
--link rmq-namesrv \
apache/rocketmq:5.3.1 sh mqbroker -n rmq-namesrv:9876
7.2 依赖与配置
在需要使用 RocketMQ 的模块(如 shop-order)的 pom.xml 中加一个依赖:
<!-- RocketMQ:Spring Cloud Stream Binder -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
注意:Spring Cloud Alibaba 2025.x 将原来的spring-cloud-starter-alibaba-rocketmq(RocketMQ-Spring 方式,提供RocketMQTemplate和@RocketMQMessageListener)拆分为两个独立模块。新的spring-cloud-starter-stream-rocketmq直接实现 Spring Cloud Stream Binder SPI,不再内置RocketMQTemplate。如果项目中确实需要RocketMQTemplate,需单独引入rocketmq-spring-boot-starter。
然后在 application.yml 中配置 RocketMQ 连接和消息通道:
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
producer:
group: order-producer-group # 生产者组名
# 声明消息通道:通道名 -> Topic 的映射
bindings:
orderCreated-out-0: # 生产者通道(StreamBridge 用这个名发消息)
destination: order-created-topic # RocketMQ Topic
content-type: application/json
smsNotify-in-0: # 消费者通道(函数名-in-0)
destination: order-created-topic
group: sms-notify-group # 消费者组
content-type: application/json
function:
definition: smsNotify # 声明消费者函数 Bean 的名称
命名规则:Spring Cloud Stream 4.x 的函数式绑定遵循格式。- - smsNotify消费者对应smsNotify-in-0,orderCreated生产者对应orderCreated-out-0。Spring Cloud 2025.x 已正式移除@EnableBinding、@Input、@Output、@StreamListener等注解,统一改用函数式编程模型。
7.3 生产者示例
在 shop-order 中发消息,不再用 RocketMQTemplate,改用 StreamBridge:
import org.springframework.cloud.stream.function.StreamBridge;
@Service
public class OrderService {
private final StreamBridge streamBridge;
public OrderService(StreamBridge streamBridge) {
this.streamBridge = streamBridge;
}
public void createOrder(CreateOrderDTO dto) {
// ... 下单逻辑 ...
// 发送异步消息(StreamBridge.send 直接投递到 Binder)
Map<String, Object> event = new HashMap<>();
event.put("orderId", order.getId());
event.put("userId", dto.getUserId());
event.put("totalAmount", order.getTotalAmount());
boolean sent = streamBridge.send("orderCreated-out-0", event);
if (sent) {
log.info("消息发送成功 orderId={}", order.getId());
} else {
log.error("消息发送失败 orderId={},写入本地消息表补偿", order.getId());
}
}
}
关键变化:StreamBridge是 Spring Cloud Stream 4.x 的标准 API,不绑定任何具体 MQ 实现。send()的第一个参数是通道名(配置文件bindings下声明的名字),不是 Topic 名。Topic 在配置文件的destination中指定。将来切换 MQ(比如从 RocketMQ 换成 Kafka)只需改配置,业务代码不用动。
7.4 消费者示例
消费者也不再需要 @RocketMQMessageListener 注解,改为声明一个 Consumer Bean:
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.function.Consumer;
@Component
public class SmsNotifyConsumer {
private static final Logger log = LoggerFactory.getLogger(SmsNotifyConsumer.class);
@Bean
public Consumer<String> smsNotify() {
return message -> {
log.info("收到下单消息: {}", message);
// 解析消息,发送短信
};
}
}
函数名必须与配置一致:@Bean的方法名smsNotify必须与配置文件spring.cloud.stream.function.definition中声明的名称一致,同时与 bindings 下的通道名前缀(smsNotify-in-0)匹配。Spring Cloud Stream 会自动将三者关联起来。
7.5 RocketMQ Dashboard 管理控制台
Dashboard 提供 Web 界面,可以查看 Topic 列表、Consumer Group 消费进度、消息堆积量、消息详情等。
方式一:Docker Compose 追加服务(推荐)。在 docker-compose.yml 中新增:
rocketmq-dashboard:
image: apacherocketmq/rocketmq-dashboard:latest
ports:
- "8088:8080"
environment:
JAVA_OPTS: "-Drocketmq.namesrv.addr=rocketmq-namesrv:9876"
depends_on:
- rocketmq-namesrv
方式二:Docker 单独启动(不想改 compose 文件时用):
docker run -d --name rocketmq-dashboard \
-p 8088:8080 \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=localhost:9876" \
apacherocketmq/rocketmq-dashboard:latest
方式三:源码编译运行(本地已有 JDK 17+ 和 Maven,不需要 Docker):
git clone https://github.com/apache/rocketmq-dashboard.git
cd rocketmq-dashboard
mvn clean package -DskipTests
java -jar target/rocketmq-dashboard-*.jar \
--rocketmq.namesrv.addr=localhost:9876 \
--server.port=8088
启动后浏览器打开 http://localhost:8088,主要功能页面:
| 页面 | 用途 |
|---|---|
| Topic | 查看所有 Topic、队列数、最小/最大偏移量 |
| Consumer | 查看 Consumer Group 消费进度、延迟量(堆积) |
| Message | 按 Topic + Message Key 精确查询消息内容 |
| Broker | 查看 Broker 运行时状态和配置信息 |
八、数据同步服务:对接外部系统
shop-sync 模块负责对接外部供应商的数据(商品、库存)。核心设计模式是适配器模式:
Supplier A (REST API) --> SupplierAAdapter -->
--> SyncEngine --> MySQL --> RocketMQ
Supplier B (FTP/CSV) --> SupplierBAdapter -->
关键代码已在原文给出,这里不再重复。重点是理解架构:每个供应商一个适配器,同步引擎统一调度,变化的数据通过消息队列通知缓存刷新。
九、定时对账:保证数据最终一致性
即使同步和消息都正常,网络抖动、系统宕机也会导致 Redis 和 MySQL 的数据不一致。定时对账是最后的兜底。
@Component
@Slf4j
public class StockReconciliationTask {
@Autowired
private StringRedisTemplate redisTemplate;
/** 每 10 分钟:Redis vs MySQL 库存对账 */
@Scheduled(cron = "0 */10 * * * ?")
public void reconcileStock() {
// 对比 Redis 和 MySQL 的库存,不一致时以 DB 为准修复
// 因为回滚操作也记录在 DB,DB 是最终真相来源
}
/** 每天凌晨 2 点:与供应商全量对账 */
@Scheduled(cron = "0 0 2 * * ?")
public void fullReconciliation() {
// 拉取供应商全量数据,逐条对比
}
}
十、Docker 容器化部署
关键技巧:分层构建。将不常变的依赖层(第三方 jar)和常变的业务层(自己写的代码)分开,每次只更新业务层(几 MB)。
FROM maven:3.9-eclipse-temurin-25 AS builder
WORKDIR /build
COPY pom.xml .
COPY src/ src/
RUN mvn clean package -DskipTests -U -q
# layertools 把 jar 包拆成四层
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
# 按从稳定到多变的顺序复制,充分利用 Docker 构建缓存
COPY --from=builder /build/target/extracted/dependencies/ ./
COPY --from=builder /build/target/extracted/spring-boot-loader/ ./
COPY --from=builder /build/target/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/target/extracted/application/ ./
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
Docker Compose 一键启动全套环境(MySQL + Redis + Nacos + RocketMQ + 所有微服务),配置同原文。
十一、Kubernetes 生产部署
核心要点:
- Deployment:声明期望副本数,K8s 自动维持
- HPA:CPU > 70% 自动扩缩(2-10 个 Pod)
- 健康检查:
livenessProbe(活了没)和readinessProbe(能接请求不),缺一不可 - 滚动更新:
maxSurge=1, maxUnavailable=0保证更新过程中始终有足够的 Pod 在运行
完整 YAML 见原文。
十二、ELK 日志体系
微服务日志散落在各个容器中,排查问题时手动翻日志会疯掉。ELK 解决了这个问题。
链路:应用输出 JSON 日志 -> Filebeat 采集 -> Logstash 清洗 -> Elasticsearch 存储 -> Kibana 展示
要点:
- 日志输出成 JSON 格式(用 logstash-logback-encoder),每行一个 JSON,方便解析
- 引入 Micrometer Tracing,自动在请求头中传递 traceId,跨服务日志搜索同一 traceId 就能看到完整请求链路
十三、CI/CD 自动化流水线
代码推送 -> GitHub Actions 编译/测试/打包 -> 推送 Docker 镜像 -> ArgoCD 自动同步到 K8s 集群。
这就是 GitOps:Git 仓库里存的 K8s 配置就是系统状态,ArgoCD 发现镜像 tag 变了就自动更新部署。
十四、若依集成方案(简要)
若依微服务版(RuoYi-Cloud)已经集成了 Nacos、Gateway、Sentinel、Seata、认证中心、系统管理、代码生成等。如果项目需要一个后台管理系统(运营人员使用),可以从若依起步,在它的基础上增加业务模块(shop-order、shop-goods 等),省掉大量基础功能的开发。
十五、总结
学习路径建议
第 1 周: Nacos + Gateway + 第一个微服务(跑通整个链路)
第 2 周: OpenFeign + Sentinel(服务间调用 + 流量保护)
第 3 周: Seata + RocketMQ(分布式事务 + 异步消息)
第 4 周: 数据同步服务(适配器模式 + 定时对账)
第 5 周: Docker(多阶段构建 + Compose 编排)
第 6 周: Kubernetes(Deployment + HPA + Ingress)
第 7 周: ELK(日志采集 + 链路追踪)
第 8 周: CI/CD(GitHub Actions + ArgoCD)
最重要的建议:不要试图一把梭全学会。本文每个章节对应一周的学习量。每学完一章,动手把代码在 IDEA 里跑起来,确认理解了再去下一章。微服务不是学出来的,是踩出来的。
写于 2026 年 6 月 26 日。希望这篇教程能让每一个刚入门的开发者,在 IDEA 里一行一行地把微服务搭起来。