← 返回博客
2026-06-24 09:39:00

Spring Cloud Alibaba 微服务实战:IDEA 手把手从零搭建到部署

Spring Cloud Alibaba 微服务实战:IDEA 手把手从零搭建到部署

2026-06-24 09:39:00 · 标签:Spring Cloud、微服务、教程、IDEA、Docker、CI/CD

引言

网上 Spring Cloud 教程很多,但大多数都是"贴一段代码 -> 贴下一段代码",读者根本不知道这段 XML 写在哪、那个注解导什么包、为什么这里要这样配。

本文的目标:假设你刚装好 IntelliJ IDEA,我们从创建项目的第一秒开始,一步一步搭出一套完整的微服务系统。 每一步都会说清楚三个问题:

  1. 在哪写 -- 这个文件在 IDEA 里怎么创建出来的
  2. 写什么 -- 完整代码,包括 import 和 package
  3. 为什么 -- 这行配置是干什么的,不写行不行

我们的业务场景是:一个电商平台,包含用户、订单、商品三个核心服务。


一、项目初始化:父子工程搭建(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 Boot4.0.6微服务单个服务的基础框架(2025年11月发布)
Spring Cloud2025.1.0微服务治理套件(网关、远程调用等)
Spring Cloud Alibaba2025.1.0.0阿里开源的微服务组件(Nacos、Sentinel、Seata)
Java25当前最新 LTS,Spring Boot 4.x 推荐的 Java 版本
Maven3.6+IDEA 自带,不用单独装
IntelliJ IDEA2026.xUltimate 或 Community 版均可

这三个"版本号"之间的关系,新手很容易搞混:

它们之间有严格的版本兼容关系。简单记: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 -- 父工程本身不运行,只管理子模块)。

项目基本信息:

字段说明
Nameshop-cloud项目名,也是最终文件夹名
LocationD:\projects\shop-cloud你喜欢的路径即可
GroupIdcom.shop组织标识,倒置域名格式
ArtifactIdshop-cloud项目标识,一般跟 Name 一致
Version1.0.0初始版本

JDK 选择 25(Spring Boot 4.x 要求 Java 17+,25 是当前 LTS)。

创建完成后,删掉 src 目录。父工程不需要 src -- 为什么?因为父工程只做两件事:

它自己不写代码,所以不需要 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 只需要依赖两个东西:

  1. Lombok -- 用来通过注解自动生成 getter/setter/构造方法,省得手写
  2. 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 步:填写模块信息:

字段
Nameshop-common
ArtifactIdshop-common(一般自动同步 Name)
GroupIdcom.shop

创建完成后:

第 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);
    }
}

关键理解


文件二:业务异常 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 模块就完成了。总结一下你做了什么:

  1. 创建了 shop-common Maven 模块(父工程下新建模块)
  2. shop-common/pom.xml 中引入了 Lombok 和 Spring Web 两个依赖(没有写版本号,版本由父 POM 管理
  3. 创建了 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 是微服务的"通讯录"和"配置中心"二合一。

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 步:填写信息:

字段
Nameshop-user
Groupcom.shop
Artifactshop-user
Packagecom.shop.user
Spring Boot4.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,点击 + 新建配置:

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);
}

GoodsVOshop-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;
}

StockDeductDTOshop-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.ymlshop-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-sentinelshop-user、shop-order、shop-goods保护 REST 接口(限流/熔断/降级),同时也内置了 Feign 熔断
spring-cloud-alibaba-sentinel-gatewayshop-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 模式原理:

  1. 第一阶段:各服务执行本地事务,Seata 自动记录 undo_log
  2. 第二阶段:全成功则删 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-0orderCreated 生产者对应 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 生产部署

核心要点:

完整 YAML 见原文。


十二、ELK 日志体系

微服务日志散落在各个容器中,排查问题时手动翻日志会疯掉。ELK 解决了这个问题。

链路:应用输出 JSON 日志 -> Filebeat 采集 -> Logstash 清洗 -> Elasticsearch 存储 -> Kibana 展示

要点:


十三、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 里一行一行地把微服务搭起来。