1. 认识微服务

本章从单体架构的优缺点切入,分析大型项目采用单体架构的问题,以及微服务架构的解决思路。

1.1 单体架构

单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在同一个工程中开发;部署时需对所有模块一起编译、打包;架构设计和开发模式非常简单。

当项目规模较小时,单体架构上手快、部署运维方便,因此早期小型项目多采用这种模式。

但随着业务规模扩大、团队人员增加,单体架构的问题逐渐凸显:

  • 团队协作成本高:数十人协作同一项目时,模块间代码边界模糊,分支合并易陷入 “冲突泥潭”。
  • 系统发布效率低:任何模块变更都需发布整个系统,模块间制约多,一次发布可能耗时数十分钟甚至数小时。
  • 系统可用性差:所有功能作为一个服务部署,相互影响大。热点功能耗尽资源时,会导致其他服务不可用。

单体架构可用性问题演示(以黑马商城为例)

为直观展示单体架构的可用性问题,对黑马商城的 hm-service 模块做如下改造与测试:

  1. 修改代码模拟耗时:修改 com.hmall.controller.HelloController 中的 hello 方法,模拟接口执行耗时。

  2. 启动项目与接口测试:启动项目后,有两个无需登录即可访问的接口:

  3. 模拟热点接口高并发:假设 /hi热点接口,使用 Jemeter 模拟 500 个用户并发访问。课前资料提供了 Jemeter 测试脚本。

  4. 导入 Jemeter 并执行测试:

    由于 /hi 接口执行耗时(500 毫秒),服务端处理能力有限,请求会逐渐积压,最终耗尽 Tomcat 资源。此时,原本正常的 /search/list 接口也会被拖慢。

  5. 验证影响:启动 Jemeter 测试后,在浏览器访问 http://localhost:8080/search/list,会发现响应速度极慢。

    若进一步提高 /hi 的并发,/search/list 的响应会更慢,甚至超时。

可见,单体架构的可用性较差,功能间相互影响大。即使做水平扩展(增加机器),热点接口仍会占用资源,无法从根本上解决扩展性问题。

1.2 微服务

微服务架构的核心是服务化:将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时需满足以下特点:

  • 单一职责:一个微服务负责一部分业务功能,且核心数据不依赖于其他模块。
  • 团队自治:每个微服务有独立的开发、测试、发布、运维团队,人员规模不超过 10 人(“2 张披萨能喂饱” 原则)。
  • 服务自治:每个微服务独立打包部署,访问自己的数据库,并做好服务隔离,避免影响其他服务。

微服务拆分示例(黑马商城)

黑马商城可将商品、用户、购物车、交易等模块拆分,由不同团队独立开发和部署:

微服务对单体问题的解决

单体架构的问题在微服务架构下得到改善:

  • 团队协作成本高:服务拆分后,每个服务代码量减少,参与开发的人员通常 1~3 名,协作成本大幅降低。
  • 系统发布效率低:每个服务独立部署,模块变更时仅需打包部署对应服务。
  • 系统可用性差:服务独立部署且隔离,使用专属服务器资源,不会相互影响。

微服务架构是分布式架构的最佳实践,适合大型互联网项目开发。但拆分后也会面临新问题(如跨服务业务处理、页面请求路由、服务隔离等),后续会逐步讲解。

1.3 SpringCloud

微服务拆分后,跨服务协作、路由、隔离等问题需要专门的组件解决。SpringCloud是 Java 领域最全面的微服务组件集合,依托 SpringBoot 的自动装配能力,大幅降低了项目搭建和组件使用的成本,是中小型企业实现微服务开发的优选方案。

SpringCloud 官网:https://spring.io/projects/spring-cloud#overview

SpringCloud 与 SpringBoot 版本对应

目前 Spring Cloud 最新版本为 2025.0.x(代号 Northfields),依赖 JDK 17,对应 Spring Boot 3.5.x 版本。Spring Cloud 各版本与 Spring Boot 的适配遵循严格的兼容性规范,且部分早期版本已正式停止支持(End of Life, EOL)。企业中常根据稳定性需求选择次新版本(如 2024.0.x、2023.0.x)进行落地。

Spring Cloud 版本(Release Train) Spring Boot 版本 备注
2025.0.x(Northfields) 3.5.x 最新版本,依赖 JDK 17
2024.0.x(Moorgate) 3.4.x 次新版本,稳定性已过验证
2023.0.x(Leyton) 3.3.x、3.2.x 适配 Spring Boot 3.x 主流版本
2022.0.x(Kilburn) 3.0.x、3.1.x(从 2022.0.3 版本开始适配) 早期 Spring Boot 3.x 适配版本
2021.0.x(Jubilee) 2.6.x、2.7.x(从 2021.0.3 版本开始适配) 已停止支持(EOL)
2020.0.x(Ilford) 2.4.x、2.5.x(从 2020.0.3 版本开始适配) 已停止支持(EOL)
Hoxton 2.2.x、2.3.x(从 SR5 版本开始适配) 已停止支持(EOL)
Greenwich 2.1.x 已停止支持(EOL)
Finchley 2.0.x 已停止支持(EOL)
Edgware 1.5.x 已停止支持(EOL)
Dalston 1.5.x 已停止支持(EOL)

注:根据 Spring Cloud 官方说明,Dalston、Edgware、Finchley、Greenwich、2020.0.x(Ilford)、2021.0.x(Jubilee)、2022.0.x(Kilburn)均已停止支持,不再提供 bug 修复及安全更新,建议避免在新系统中使用,并逐步将旧系统迁移至 2023.0.x 及以上版本。

本文使用的是 Spring Cloud 2021.0.x + Spring Boot 2.7.x

此外,SpringCloudAlibaba 也已成为 SpringCloud 生态的一部分,课程中会使用其部分组件。

父工程依赖配置(黑马商城)

在黑马商城的父工程 hmall 中,已配置 SpringCloud 和 SpringCloudAlibaba 的依赖:

这样,后续使用 SpringCloud 或 SpringCloudAlibaba 组件时,无需单独指定版本。

2. 微服务拆分

接下来,我们将黑马商城单体项目拆分为微服务项目,并解决过程中出现的问题。

2.1 熟悉黑马商城

首先需熟悉黑马商城项目的基本结构:

可直接启动项目测试效果,但需修改数据库连接参数(在 application-local.yaml 中):

1
2
3
4
hm:
db:
host: 192.168.150.101 # 修改为你Ubuntu系统所在虚拟机的IP地址
pw: 123 # 修改为Docker中MySQL的密码

同时需配置启动项激活 local 环境:

2.1.1 登录业务

登录业务流程:

服务端入口为 com.hmall.controller.UserController 中的 login 方法:

2.1.2 搜索商品

在首页搜索框输入关键字并点击搜索,进入搜索列表页面:

该页面调用接口 /search/list,服务端入口为 com.hmall.controller.SearchController 中的 search 方法:

image

2.1.3 购物车业务

  • 加入购物车:在商品列表点击 “加入购物车” 按钮,商品加入购物车:

    image

  • 购物车列表页:加入成功后进入购物车列表页,可查看、修改、删除购物车商品:

    image

相关功能入口为 com.hmall.controller.CartController

image

查询购物车列表时,需判断商品最新价格和状态,因此要查询商品信息,业务流程:

image

2.1.4 下单业务

  • 订单结算页:在购物车页面点击 “结算” 按钮,进入订单结算页面:

    image

  • 提交订单:点击 “提交订单”,服务端执行 3 件事:

    • 创建新订单
    • 扣减商品库存
    • 清理购物车商品

业务入口为 com.hmall.controller.OrderController 中的 createOrder 方法:

image

2.1.5 支付业务

下单后跳转到支付页面(目前仅支持余额支付):

image

选择 “余额支付” 后,服务端会创建支付流水单并返回流水单号;用户输入密码点击 “确认支付” 时,服务端执行:

  • 校验用户密码
  • 扣减余额
  • 修改支付流水状态
  • 修改交易订单状态

请求入口为 com.hmall.controller.PayController

image

2.2 服务拆分原则

服务拆分需重点考虑两个问题:

  • 什么时候拆?
  • 如何拆?

2.2.1 什么时候拆

  • 初创项目:优先验证可行性,建议采用单体架构。原因是:
    • 开发成本低,可快速产出产品投入市场验证
    • 若产品不符合市场需求,损失较小
    • 缺点:后期拆分可能面临代码耦合问题(前易后难)
  • 大型项目:立项时目标明确,建议直接采用微服务架构。原因是:
    • 长远考虑,避免后期拆分的麻烦
    • 缺点:前期投入人力和时间成本高(前难后易)

2.2.2 怎么拆

微服务拆分的核心目标是高内聚、低耦合

  • 高内聚:每个微服务职责单一,内部业务关联度高、完整性强。修改业务时应尽量仅涉及当前服务,降低变更成本。
  • 低耦合:服务功能相对独立,减少对其他服务的依赖;若有依赖,需保证接口稳定性(外观不变)。例如:订单服务需商品数据时,应调用商品服务的接口,而非直接访问商品数据库,避免数据耦合。
拆分方式
  1. 纵向拆分:按功能模块拆分(如黑马商城的用户、订单、购物车等模块),提高服务内聚性。
  2. 横向拆分:抽取各模块的公共业务为通用服务(如消息发送、风控管理),提高复用性,降低耦合。

黑马商城按纵向拆分可分为:

  • 用户服务
  • 商品服务
  • 订单服务
  • 购物车服务
  • 支付服务

2.3 拆分购物车、商品服务

微服务项目的两种工程结构:

  • 完全解耦:每个服务独立工程(可跨语言),耦合度低但管理麻烦。
  • Maven 聚合:父工程下的多个 Module,集中管理(适合授课),但编译时间较长。

课程采用Maven 聚合工程,基于黑马商城父工程 hmall 拆分服务(父工程已预设 SpringBoot、SpringCloud 依赖版本)。

2.3.1 商品服务(item-service)

  1. 创建 Module:在 hmall 父工程中创建 Maven 模块,命名为 item-service,JDK 版本为 11。

    image

    imageimage

  2. 配置依赖(pom.xml)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    <?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">
    <parent>
    <artifactId>hmall</artifactId>
    <groupId>com.heima</groupId>
    <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>item-service</artifactId>

    <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
    <!-- 公共模块 -->
    <dependency>
    <groupId>com.heima</groupId>
    <artifactId>hm-common</artifactId>
    <version>1.0.0</version>
    </dependency>
    <!-- Web -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 数据库 -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- MyBatis-Plus -->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <!-- 单元测试 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    </dependencies>
    <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>
  3. 编写启动类

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.hmall.item;

    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @MapperScan("com.hmall.item.mapper") // 扫描Mapper接口
    @SpringBootApplication
    public class ItemApplication {
    public static void main(String[] args) {
    SpringApplication.run(ItemApplication.class, args);
    }
    }
  4. 配置文件:从 hm-service 拷贝配置文件,修改 application.yaml

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    server:
    port: 8081 # 商品服务端口
    spring:
    application:
    name: item-service # 服务名称
    profiles:
    active: dev
    datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${hm.db.pw}
    mybatis-plus:
    configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
    global-config:
    db-config:
    update-strategy: not_null
    id-type: auto
    logging:
    level:
    com.hmall: debug
    pattern:
    dateformat: HH:mm:ss:SSS
    file:
    path: "logs/${spring.application.name}"
    knife4j: # Swagger文档配置
    enable: true
    openapi:
    title: 商品服务接口文档
    description: "商品服务接口文档信息"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
    default:
    group-name: default
    api-rule: package
    api-rule-resources:
    - com.hmall.item.controller

    application-dev.yamlapplication-local.yaml 直接拷贝,修改数据库连接参数为 Ubuntu 虚拟机的 IP 和密码)

  5. 拷贝商品相关代码:将 hm-service 中与商品管理相关的代码(实体类、Controller、Service、Mapper 等)拷贝到 item-service

    image

  6. 修改代码依赖:调整 ItemServiceImpldeductStock 方法的 ItemMapper 包路径(因包结构变化)。

    image

  7. 导入数据库表:在 Docker 的 MySQL 中执行课前资料提供的 SQL 文件,创建 hm-item 数据库(每个微服务独立数据库)。

    image

    image

  8. 启动与测试

    • 配置启动项,激活local环境:

      image

      image

    • 启动 ItemApplication,访问 Swagger 文档:http://localhost:8081/doc.html

    • 测试 “根据 id 批量查询商品” 接口,参数:

      1
      100002672302,100002624500,100002533430

      结果如下:

      image

2.3.2 购物车服务(cart-service)

  1. 创建 Module:在 hmall 父工程中创建 Maven 模块,命名为 cart-service

    image

  2. 配置依赖(pom.xml):与 item-service 类似,仅模块名为 cart-service

  3. 编写启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.hmall.cart;

    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @MapperScan("com.hmall.cart.mapper")
    @SpringBootApplication
    public class CartApplication {
    public static void main(String[] args) {
    SpringApplication.run(CartApplication.class, args);
    }
    }
  4. 配置文件:拷贝 item-service 的配置文件,修改 application.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 8082 # 购物车服务端口
    spring:
    application:
    name: cart-service # 服务名称
    datasource:
    url: jdbc:mysql://${hm.db.host}:3306/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    # 其他配置基本与item-service一致,只需要把item改成cart即可,略...
  5. 拷贝购物车相关代码:将 hm-service 中与购物车相关的代码拷贝到 cart-service,最终结构:

    image

  6. 修改代码:在 CartServiceImpl 中处理依赖问题:

    • 暂时写死用户 ID(因登录功能未拆分,可以写成1L)
    • 注释查询商品信息的代码(需调用商品服务,待后续解决,暂时先把handleCartItems函数内容注释掉)

    image

  7. 导入数据库表:执行课前资料的 SQL 文件,创建 hm-cart 数据库及 cart 表。

    image

    image

  8. 启动与测试

    • 配置启动项激活local环境:

      image

    • 启动 CartApplication,访问 Swagger 文档:http://localhost:8082/doc.html

    • 测试 “查询我的购物车列表” 接口,结果中商品相关字段为空(因代码注释)。

      image

问题:如何在 cart-service 中查询 item-service 的商品信息?

当前购物车服务查询商品信息的代码被注释,需解决服务间通信问题,后续将介绍微服务远程调用方案。

2.4 服务调用

在拆分购物车服务时,发现购物车查询需依赖商品服务的商品信息,但商品逻辑已迁移至 item-service,导致购物车数据不完整。因此需将本地方法调用改造为跨微服务的远程调用(RPC,Remote Procedure Call)

2.4.1 远程调用流程

购物车查询商品的流程变为:

image

代码中需改造的核心步骤:

image

2.4.2 如何实现跨服务 HTTP 调用?

前端通过浏览器发送 HTTP 请求可实现 “远程查询服务端数据”(如 Swagger 测试商品接口 http://localhost:8081/items)。同理,若在 cart-service 中模拟 “浏览器发送 HTTP 请求”,即可调用 item-service 的接口。

image

2.4.2.1 RestTemplate(Spring HTTP 请求工具)

Spring 提供 RestTemplate 简化 HTTP 请求发送,支持 GET、POST、PUT、DELETE 等多种请求类型。

RestTemplate 说明

同步 HTTP 客户端,基于底层 HTTP 库(如 JDK HttpURLConnection、Apache HttpComponents 等)封装模板方法。支持常见 HTTP 场景,且可配置为共享组件(启动时初始化,避免并发修改)。

image

2.4.2.2 注册 RestTemplate 为 Spring Bean

cart-service 中创建配置类,将 RestTemplate 注册到 Spring 容器:

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.hmall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RemoteCallConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

2.4.3 远程调用实战(购物车调用商品服务)

修改 cart-serviceCartServiceImplhandleCartItems 方法,通过 RestTemplate 发送 HTTP 请求到 item-service

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void handleCartItems(List<CartVO> vos) {
// 1. 获取商品ID集合
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());

// 2. 发送HTTP GET请求到item-service的商品查询接口
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}", // 请求路径(带路径参数)
HttpMethod.GET, // 请求方法
null, // 请求体(GET无请求体)
new ParameterizedTypeReference<List<ItemDTO>>() {}, // 响应类型(泛型列表)
Map.of("ids", CollUtil.join(itemIds, ",")) // 路径参数(商品ID拼接为字符串)
);

// 3. 解析响应
if (!response.getStatusCode().is2xxSuccessful()) {
return; // 请求失败,直接返回
}
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
return; // 商品列表为空,直接返回
}

// 4. 转换为“商品ID -> 商品DTO”的Map,方便后续填充购物车VO
Map<Long, ItemDTO> itemMap = items.stream()
.collect(Collectors.toMap(ItemDTO::getId, Function.identity()));

// 5. 填充购物车VO的商品信息
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item != null) {
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
}

2.4.4 测试远程调用

重启 cart-service,再次测试 “查询我的购物车列表” 接口,商品相关数据(价格、库存、状态等)可正常返回。

image

2.5 总结

微服务拆分时机

  • 创业型公司:先以单体架构快速迭代,验证市场模型;业务跑通后,再随规模扩大拆分微服务(前易后难)。
  • 大型企业:项目初期直接搭建微服务架构(资源充足,前难后易)。

微服务拆分原则

  • 核心目标:高内聚、低耦合
  • 拆分方式:
    • 纵向拆分:按业务功能模块拆分(如用户、订单、商品)。
    • 横向拆分:抽取通用业务为独立服务(如消息、风控),提高复用性。

微服务远程调用(RPC)

微服务间远程调用称为 RPC,常见实现方式:

  • 基于 HTTP 协议:不关心服务提供者技术,仅依赖 HTTP 接口,符合微服务解耦需求。
  • 基于 Dubbo 协议:高性能二进制协议,需服务提供者和消费者遵循 Dubbo 规范。

课程采用 HTTP 方式,通过 Spring RestTemplate 实现:

  1. 注册 RestTemplate 到 Spring 容器。
  2. 调用 RestTemplate 方法发送请求(如 getForObjectpostForObjectexchange 等)。

3. 服务注册和发现

上一章实现了微服务拆分及跨服务远程调用,但手动发送 HTTP 请求的方式存在问题。当商品服务(item-service)多实例部署时,会面临地址管理、调用选择、故障处理、动态扩展等挑战。

试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:

image

此时,每个item-service的实例其IP或端口不同,问题来了:

  • item-service这么多实例,cart-service如何知道每一个实例的地址?
  • http请求要写url地址,cart-service服务到底该调用哪个实例呢?
  • 如果在运行过程中,某一个item-service实例宕机,cart-service依然在调用该怎么办?
  • 如果并发太高,item-service临时多部署了N台实例,cart-service如何知道新实例的地址?

为解决这些问题,需引入注册中心

3.1 注册中心原理

微服务远程调用涉及两个角色:

  • 服务提供者:提供接口供其他服务访问(如 item-service)。
  • 服务消费者:调用其他服务的接口(如 cart-service)。

注册中心是管理服务地址的核心组件,三者关系如下:

image

核心流程

  1. 服务注册:服务启动时,将自身信息(服务名、IP、端口)注册到注册中心。
  2. 服务订阅:消费者从注册中心获取目标服务的实例列表。
  3. 负载均衡:消费者从实例列表中选择一个实例发起调用。
  4. 动态更新:
    • 服务提供者定期发送心跳到注册中心,报告健康状态。
    • 注册中心若长时间未收到心跳,将剔除该实例。
    • 新实例启动时自动注册,注册中心会将变更通知消费者,更新其本地实例列表。

3.2 Nacos 注册中心

国内常用的注册中心框架有 Eureka、Nacos、Consul 等。课程选择Nacos(Alibaba 出品),原因是中文文档丰富,且兼具配置管理功能。

Nacos 官网:https://nacos.io/

3.2.1 部署 Nacos(基于 Docker)

  1. 准备 MySQL 数据库

    • 将课前资料中的 Nacos 初始化 SQL 文件导入 Docker 的 MySQL 容器。

    image

    • 最终生成的表结构:

    image

  2. 配置 Nacos 环境变量

    • 找到课前资料的nacos文件夹,修改nacos/custom.env中的 MySQL 地址为虚拟机 IP:

    image

    image

  3. 上传并部署 Nacos

    • nacos 目录上传至 Ubuntu 虚拟机的 /root 目录。

    • 执行 Docker 命令启动 Nacos:

      1
      2
      3
      4
      5
      6
      7
      8
      sudo docker run -d \
      --name nacos \
      --env-file ./nacos/custom.env \
      -p 8848:8848 \
      -p 9848:9848 \
      -p 9849:9849 \
      --restart=always \
      nacos/nacos-server:v2.1.0-slim
  4. 访问 Nacos 控制台

    • 地址:http://虚拟机IP:8848/nacos/(替换为实际 IP)。
    • 登录账号密码均为nacos

    image

3.3 服务注册(以item-service为例)

将服务注册到 Nacos 的步骤:

  • 引入依赖
  • 配置Nacos地址
  • 重启

3.3.1 引入依赖

item-servicepom.xml 中添加 Nacos 注册依赖:

1
2
3
4
5
<!-- Nacos服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

3.3.2 配置 Nacos 地址

item-serviceapplication.yaml 中添加配置:

1
2
3
4
5
6
spring:
application:
name: item-service # 服务名称(必须唯一,用于服务发现)
cloud:
nacos:
server-addr: 192.168.150.101:8848 # Nacos地址(替换为实际虚拟机IP)

3.3.3 启动多实例并验证

  1. 配置多实例

    • item-service新增启动项,修改端口(如 8083)避免冲突:

      image

      image

  2. 启动实例

    • 启动两个 item-service 实例(端口 8081 和 8083)。

    image

  3. 验证注册结果

    • 登录 Nacos 控制台,在 “服务列表” 中查看item-service

    image

    • 点击 “详情” 可查看两个实例的 IP 和端口:

    image

3.4 服务发现(以cart-service为例)

服务消费者通过 Nacos 获取服务实例列表并调用的步骤:

  • 引入依赖
  • 配置Nacos地址
  • 发现并调用服务

3.4.1 引入依赖

cart-servicepom.xml 中添加依赖(包含服务注册和发现功能):

1
2
3
4
5
6
7
8
9
10
<!-- Nacos服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

(注:负载均衡依赖用于从多实例中选择一个进行调用)

3.4.2 配置 Nacos 地址

cart-serviceapplication.yaml 中添加配置:

1
2
3
4
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848 # Nacos地址(替换为实际虚拟机IP)

3.4.3 发现并调用服务

  1. 注入DiscoveryClient:SpringCloud 自动装配的 DiscoveryClient 可用于获取服务实例列表:

    1
    private final DiscoveryClient discoveryClient;

    image

  2. 修改远程调用逻辑:从 Nacos 获取 item-service 的实例列表,通过负载均衡(如随机算法)选择一个实例调用:

    image

  3. 测试验证:启动 cart-service,通过 Swagger 测试购物车查询接口,可正常获取商品数据,且调用会随机分配到 item-service 的两个实例。

总结

  • 注册中心作用:解决服务地址管理、动态扩缩容、故障自动剔除等问题。
  • Nacos 优势:中文支持好,兼具服务注册发现和配置管理功能。
  • 核心流程:服务注册(启动时上报地址)→ 服务发现(消费者订阅实例列表)→ 负载均衡(选择实例调用)→ 动态更新(基于心跳和通知)。

4. OpenFeign

上一章通过 Nacos 实现服务治理,用RestTemplate实现远程调用,但存在代码复杂、调用体验不统一的问题(需手动处理服务发现、负载均衡、HTTP 请求构建)。

image

OpenFeign 可通过声明式注解简化远程调用,让其像本地方法调用一样便捷。

4.1 快速入门

cart-service调用item-service的商品查询接口为例,演示 OpenFeign 的使用。

4.1.1 引入依赖

cart-servicepom.xml中添加 OpenFeign 及负载均衡依赖:

1
2
3
4
5
6
7
8
9
10
<!-- OpenFeign 核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡依赖(OpenFeign调用需配合负载均衡) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

4.1.2 启用 OpenFeign

cart-service的启动类CartApplication上添加@EnableFeignClients注解,开启 OpenFeign 功能:

image

4.1.3 编写 Feign 客户端

定义接口,通过 SpringMVC 注解声明远程调用的核心参数(请求方式、路径、参数、返回值),OpenFeign 会自动生成实现类。

cart-service中创建ItemClient接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collection;
import java.util.List;

@FeignClient("item-service") // 声明服务名称(对应Nacos中的服务名)
public interface ItemClient {

// 注解与item-service的Controller接口保持一致
@GetMapping("/items") // 请求方式+路径
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); // 请求参数+返回值类型
}

4.1.4 使用 Feign 客户端

CartServiceImpl中注入ItemClient,直接调用接口方法(无需手动处理服务发现、HTTP 请求):

image

优势:OpenFeign 自动完成「服务发现→负载均衡→HTTP 请求发送→响应解析」全流程,代码简洁且与本地调用体验一致。

4.2 连接池优化

Feign 底层依赖 HTTP 客户端实现,默认使用HttpURLConnection(无连接池,性能差)。推荐替换为支持连接池OKHttpApache HttpClient,提升并发性能。

4.2.1 引入 OKHttp 依赖

cart-servicepom.xml中添加依赖:

1
2
3
4
5
<!-- Feign-OKHttp 依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

4.2.2 开启连接池

cart-serviceapplication.yaml中配置启用 OKHttp:

1
2
3
feign:
okhttp:
enabled: true # 开启OKHttp连接池

4.2.3 验证连接池生效

通过断点调试验证底层客户端是否切换为 OKHttp:

  1. org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClientexecute方法处打断点。

image

  1. 以 Debug 模式启动cart-service,调用购物车查询接口,进入断点。
  2. 观察client对象类型,确认是OkHttpClient

image

4.3 最佳实践(Feign 客户端抽取)

多个服务(如cart-servicetrade-service)可能需要调用同一服务(如item-service)的接口,若每个服务都重复定义 Feign 客户端,会导致代码冗余。解决方式是将 Feign 客户端抽取到公共模块。

4.3.1 思路分析

两种抽取方案对比:

方案 实现方式 优点 缺点
方案 1:公共模块抽取 抽取到独立的hm-api模块 结构清晰,抽取简单 服务间耦合度较高
方案 2:服务内抽取 每个服务单独抽取 API 模块 耦合度低 结构复杂,维护成本高

image

课程采用方案 1(适合现有项目结构)。

4.3.2 创建公共 API 模块(hm-api)

  1. 创建 Module:在hmall父工程下创建hm-api模块。

    image

  2. 配置依赖(pom.xml)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    <?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">
    <parent>
    <artifactId>hmall</artifactId>
    <groupId>com.heima</groupId>
    <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hm-api</artifactId>

    <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
    <!-- OpenFeign 核心依赖 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- 负载均衡依赖 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!-- Swagger注解依赖(接口文档相关) -->
    <dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>1.6.6</version>
    <scope>compile</scope>
    </dependency>
    </dependencies>
    </project>
  3. 拷贝 Feign 客户端及 DTO:将cart-service中的ItemClientItemDTO拷贝到hm-api模块,最终结构:

    image

4.3.3 服务引入公共模块

  1. 引入依赖:在cart-servicepom.xml中添加hm-api依赖:

    1
    2
    3
    4
    5
    6
    <!-- 引入公共API模块 -->
    <dependency>
    <groupId>com.heima</groupId>
    <artifactId>hm-api</artifactId>
    <version>1.0.0</version>
    </dependency>
  2. 删除冗余代码:删除cart-service中原有ItemClientItemDTO

  3. 解决扫描问题:启动cart-service会报错 ——ItemClient位于com.hmall.api.client包,而启动类扫描范围是com.hmall.cart,无法识别 Feign 客户端。

    两种解决方式:

    • 方式 1:指定扫描包(推荐,适用于多个 Feign 客户端):

    image

    • 方式 2:指定 Feign 客户端类(适用于少量客户端):

    image

4.4 日志配置

OpenFeign 的日志输出需满足两个条件:

  1. Feign 客户端所在包的日志级别为DEBUG
  2. 配置 Feign 的日志级别(默认NONE,不输出日志)。

4.4.1 Feign 日志级别

级别 说明
NONE 不记录任何日志(默认)
BASIC 记录请求方法、URL、响应状态码、执行时间
HEADERS 在 BASIC 基础上,增加请求 / 响应头信息
FULL 记录完整请求 / 响应(头、体、元数据)

4.4.2 配置日志级别

  1. 定义日志配置类:在hm-api模块中创建配置类:

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.hmall.api.config;

    import feign.Logger;
    import org.springframework.context.annotation.Bean;

    public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
    return Logger.Level.FULL; // 配置为FULL级别
    }
    }
  2. 生效配置

    • 局部生效(仅对指定 FeignClient 生效):

      1
      2
      3
      4
      5
      @FeignClient(
      value = "item-service",
      configuration = DefaultFeignConfig.class // 绑定配置类
      )
      public interface ItemClient { ... }
    • 全局生效(对所有 FeignClient 生效):

      1
      2
      3
      4
      5
      6
      @EnableFeignClients(
      basePackages = "com.hmall.api.client",
      defaultConfiguration = DefaultFeignConfig.class // 全局配置
      )
      @SpringBootApplication
      public class CartApplication { ... }
  3. 配置包日志级别:在cart-serviceapplication.yaml中添加:

    1
    2
    3
    logging:
    level:
    com.hmall.api.client: debug # Feign客户端所在包的日志级别为DEBUG

4.4.3 日志示例(FULL 级别)

1
2
3
4
5
6
7
8
9
10
11
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient          : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body)
17:35:32:278 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms)
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 26 May 2023 09:35:32 GMT
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds]
17:35:32:280 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":100000006163,"name":"巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉裤","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}]
17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (369-byte body)

第四章完成微服务拆分后,实际联调中暴露了多入口维护、用户信息传递、配置冗余等问题。所以后续将会通过微服务网关解决请求入口与鉴权问题,通过Nacos 统一配置解决配置管理问题。

5. 网关路由

5.1 认识网关

5.1.1 网关的定义与作用

网关是网络数据传输的 “关口”,负责在不同网络间进行路由转发、安全校验、数据过滤。通俗来讲,网关类似园区传达室的 “大爷”,承担两大核心职责:

  • 安全拦截:验证访问者身份,拦截非法请求;
  • 路由转发:接收外部请求,传递给目标接收者。

image

在微服务架构中,前端请求不直接访问微服务,必须经过网关层,流程如下:

  1. 网关对请求进行登录身份校验,校验通过才允许放行;
  2. 校验通过后,网关根据请求特征判断目标微服务,将请求转发至对应服务。

image

5.1.2 SpringCloud 中的网关方案

SpringCloud 提供两种网关实现,目前主流为 SpringCloudGateway:

网关方案 技术基础 性能特点 现状
Netflix Zuul Servlet 3.x 同步阻塞模型 已淘汰
SpringCloudGateway Spring WebFlux 响应式编程,高吞吐 主流推荐

SpringCloudGateway 官方网站:https://spring.io/projects/spring-cloud-gateway#learn

5.2 快速入门:实现网关路由

网关本身是独立的微服务,需通过创建模块、配置依赖与路由规则实现功能。核心步骤为:创建项目→引入依赖→编写启动类→配置路由→测试验证

5.2.1 步骤 1:创建网关模块(hm-gateway)

hmall父工程下新建 Maven 模块,命名为hm-gateway,作为网关微服务:

image

5.2.2 步骤 2:引入核心依赖

hm-gatewaypom.xml中添加网关、Nacos 服务发现、负载均衡依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?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">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>hm-gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- 公共模块(工具类、通用DTO) -->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- SpringCloudGateway 核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos服务发现(网关从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>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

5.2.3 步骤 3:编写启动类

com.hmall.gateway包下创建启动类,无需额外注解(@SpringBootApplication包含自动配置):

image

1
2
3
4
5
6
7
8
9
10
11
package com.hmall.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

5.2.4 步骤 4:配置路由规则

hm-gatewayresources目录创建application.yaml,配置端口、Nacos 地址及路由规则(需替换 Nacos 地址为实际虚拟机 IP):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
server:
port: 8080 # 网关端口(前端统一访问入口)
spring:
application:
name: gateway # 网关服务名(注册到Nacos)
cloud:
nacos:
server-addr: 192.168.150.101:8848 # Nacos服务地址
gateway:
routes:
# 1. 商品服务路由
- id: item # 路由唯一ID(自定义,不可重复)
uri: lb://item-service # 目标服务(lb=负载均衡,从Nacos拉取实例)
predicates: # 路由断言(匹配请求路径)
- Path=/items/**,/search/** # 匹配/items/、/search/下所有路径
# 2. 购物车服务路由
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
# 3. 用户服务路由
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
# 4. 交易服务路由
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
# 5. 支付服务路由
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

5.2.5 步骤 5:测试路由功能

  1. 启动服务:依次启动 Nacos、GatewayApplicationitem-service

  2. 访问测试:通过网关访问商品接口,URL 格式为http://网关IP:网关端口/微服务接口路径

    ,例如:http://localhost:8080/items/page?pageNo=1&pageSize=1

  3. 验证结果:接口返回商品数据,说明路由转发成功。

image

启动user-servicecart-service后,前端可通过http://localhost:8080统一入口访问所有微服务功能,无需维护多个服务地址。

5.3 路由规则与断言详解

5.3.1 路由规则核心结构

路由规则的基本语法如下,routesRouteDefinition的集合,支持多规则配置:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**

routes对应的类型及RouteDefinition核心属性:

image

image

RouteDefinition核心属性说明:

属性名 含义
id 路由唯一标识(自定义,不可重复)
predicates 路由断言:判断请求是否符合当前规则
filters 路由过滤:请求 / 响应的加工逻辑(后续讲解)
uri 目标服务地址:lb://服务名代表负载均衡,从 Nacos 获取实例列表

5.3.2 常用路由断言类型

路由断言是网关匹配请求的 “规则”,SpringCloudGateway 支持多种断言类型,覆盖路径、参数、时间等场景:

断言名称 说明 示例
After 匹配某个时间点之后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 匹配某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 匹配两个时间点之间的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 匹配包含指定 Cookie的请求 - Cookie=chocolate, ch.p(Cookie 键为 chocolate,值匹配 ch.p 正则)
Header 匹配包含指定请求头的请求 - Header=X-Request-Id, \d+(请求头键为 X-Request-Id,值为数字)
Host 匹配访问指定域名的请求 - Host=**.somehost.org,**.anotherhost.org(匹配二级域名)
Method 匹配指定 HTTP 方法的请求 - Method=GET,POST(仅允许 GET、POST 请求)
Path 匹配指定路径的请求(最常用) - Path=/red/{segment},/blue/**(/red/xxx 或 /blue/ 下所有路径)
Query 匹配包含指定参数的请求 - Query=name(必须包含 name 参数);- Query=name, Jack(name 参数值为 Jack)
RemoteAddr 匹配指定 IP 来源的请求 - RemoteAddr=192.168.1.1/24(IP 在 192.168.1.0-255 范围)
Weight 基于权重的路由(负载均衡扩展) - Weight=group1, 80(group1 分组中占 80% 权重)

5.3.3 断言组合使用

多个断言之间为 **“与” 关系 **,需同时满足才会触发路由。例如:仅允许 GET 请求访问/items路径:

1
2
3
4
5
- id: item
uri: lb://item-service
predicates:
- Path=/items/**
- Method=GET # 同时满足路径和方法断言

6. 网关登录校验

6.1 鉴权思路分析

单体架构中,一次登录校验即可在所有业务中共享用户信息;但微服务拆分后,服务独立部署、数据不共享,若每个微服务单独实现登录校验,会存在秘钥泄露风险代码冗余问题

6.1.1 核心痛点

  • 安全性差:每个微服务需存储 JWT 秘钥,易泄露;
  • 开发效率低:重复编写登录校验、权限判断代码。

6.1.2 网关统一鉴权方案

网关作为所有请求的入口,可集中处理登录校验,解决上述问题:

  • 秘钥集中管理:仅网关和用户服务保存 JWT 秘钥;
  • 代码统一开发:登录校验逻辑仅在网关实现一次。

鉴权流程

image

6.1.3 待解决问题

  • 如何在网关转发请求前执行登录校验?
  • 网关校验通过后,如何将用户信息传递给微服务?
  • 微服务间调用(不经过网关)如何传递用户信息?

6.2 网关过滤器:鉴权的技术基础

网关的请求处理依赖过滤器链,登录校验需通过过滤器在请求转发前执行。

6.2.1 Gateway 工作原理

Gateway 处理请求的核心流程:

  1. 客户端请求进入网关,HandlerMapping匹配对应的路由规则(Route);
  2. WebHandler加载该路由的过滤器链(Filter Chain),按顺序执行过滤器;
  3. 过滤器逻辑分为pre(转发前)和post(响应后)两部分,仅pre逻辑全部通过后,请求才会转发到微服务;
  4. 微服务返回响应后,倒序执行过滤器的post逻辑,最终返回给客户端。

image

关键结论:登录校验逻辑需实现为pre阶段的过滤器,且执行顺序需在请求转发过滤器(NettyRoutingFilter)之前。

6.2.2 网关过滤器类型

Gateway 提供两种核心过滤器,均支持自定义:

过滤器类型 作用范围 配置方式
GatewayFilter 指定路由(灵活) 需在 yaml 中配置
GlobalFilter 所有路由(全局) 无需配置,自动生效

两种过滤器的方法签名一致,最终会被统一封装到过滤器链中,按Order排序执行(值越小,优先级越高):

1
2
3
4
5
6
/**
* @param exchange 请求上下文(含request、response)
* @param chain 过滤器链(用于传递请求)
* @return 标记请求是否放行:chain.filter(exchange) 为放行
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

6.2.3 内置 GatewayFilter 使用

Gateway 内置多种GatewayFilter(如添加请求头、路径重写),无需编码,直接在 yaml 中配置即可。

示例:添加请求头过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
# 作用于所有路由的默认过滤器
default-filters:
- AddRequestHeader=X-Gateway, hmall-gateway # 给所有请求添加头信息
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**
# 仅作用于当前路由的过滤器
filters:
- AddRequestHeader=X-Service, item-service

6.3 自定义过滤器

实际业务中,内置过滤器无法满足复杂需求(如登录校验),需自定义过滤器。

6.3.1 自定义 GatewayFilter

需继承AbstractGatewayFilterFactory,支持动态配置参数,作用于指定路由。

示例 1:基础无参过滤器

1
2
3
4
5
6
7
8
9
10
11
@Component
// 类名必须以GatewayFilterFactory为后缀
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
System.out.println("GatewayFilter执行了");
return chain.filter(exchange); // 放行
};
}
}

配置使用

1
2
3
4
5
spring:
cloud:
gateway:
default-filters:
- PrintAny # 直接使用类名前缀

示例 2:带参数的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
public class PrintParamGatewayFilterFactory
extends AbstractGatewayFilterFactory<PrintParamGatewayFilterFactory.Config> {

// 过滤器逻辑(支持排序)
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
// 获取配置参数
System.out.println("a=" + config.getA() + ", b=" + config.getB());
return chain.filter(exchange);
}, 100); // 执行顺序:100
}

// 自定义配置类(存储参数)
@Data
static class Config {
private String a;
private String b;
}

// 指定参数顺序(用于yaml简洁配置)
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b");
}

// 指定配置类类型
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}

配置使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 方式1:按顺序传参(简洁)
spring:
cloud:
gateway:
default-filters:
- PrintParam=123, 456

# 方式2:指定参数名(灵活)
spring:
cloud:
gateway:
default-filters:
- name: PrintParam
args:
a: 123
b: 456

6.3.2 自定义 GlobalFilter

直接实现GlobalFilterOrdered接口,作用于所有路由,无需配置,适合全局逻辑(如登录校验)。

示例:简单拦截过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 拦截未登录请求
System.out.println("未登录,拦截请求");
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401); // 401未授权
return response.setComplete(); // 终止请求
}

@Override
public int getOrder() {
return 0; // 执行顺序:0(优先级高)
}
}

6.4 实战:网关登录校验(基于 JWT)

利用GlobalFilter实现 JWT 校验,核心流程:排除白名单→获取 token→校验 token→传递用户信息→放行

6.4.1 准备 JWT 工具

hm-service拷贝 JWT 相关工具到网关模块:

image

  • JwtTool:JWT 生成、解析工具;
  • JwtProperties:JWT 配置(秘钥、过期时间);
  • AuthProperties:白名单配置(无需校验的路径);
  • 秘钥文件hmall.jks

配置 yaml 参数

1
2
3
4
5
6
7
8
9
10
11
hm:
jwt:
location: classpath:hmall.jks # 秘钥文件路径
alias: hmall # 秘钥别名
password: hmall123 # 秘钥密码
tokenTTL: 30m # token有效期
auth:
excludePaths: # 白名单路径(无需登录)
- /search/**
- /users/login
- /items/**

6.4.2 实现登录校验过滤器

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); // 路径匹配工具

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 排除白名单路径
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
if (isExclude(path)) {
return chain.filter(exchange); // 放行
}

// 2. 获取请求头中的token
String token = null;
if (!CollUtils.isEmpty(request.getHeaders().get("authorization"))) {
token = request.getHeaders().get("authorization").get(0);
}

// 3. 校验token
Long userId;
try {
userId = jwtTool.parseToken(token); // 解析token获取用户ID
} catch (UnauthorizedException e) {
// 校验失败,返回401
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// 4. 传递用户信息(后续补充)
System.out.println("登录用户ID:" + userId);

// 5. 放行
return chain.filter(exchange);
}

// 判断路径是否在白名单中
private boolean isExclude(String path) {
for (String pattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pattern, path)) {
return true;
}
}
return false;
}

@Override
public int getOrder() {
return 0; // 优先执行
}
}

6.4.3 测试鉴权效果

  1. 启动服务:网关、用户服务、商品服务;

  2. 测试白名单路径:访问 http://localhost:8080/items/1 ,未登录可正常返回数据;

    image

  3. 测试受保护路径:访问 http://localhost:8080/carts ,未登录返回 401;

    image

6.5 微服务获取用户信息

网关校验通过后,需将用户信息传递给微服务。通过请求头 + 拦截器 + ThreadLocal实现:

image

  • 网关:将用户 ID 存入请求头;
  • 微服务:通过拦截器读取请求头,存入 ThreadLocal 供业务使用。

6.5.1 网关传递用户信息

修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:

image6.5.2 微服务拦截器获取用户

hm-common模块实现拦截器,统一处理用户信息(所有微服务复用)。

步骤 1:ThreadLocal 工具类

hm-common中已提供UserContext,用于存储当前线程的用户信息:

image

image

步骤 2:实现拦截器

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 读取请求头中的用户ID
String userId = request.getHeader("user-info");
if (StrUtil.isNotBlank(userId)) {
UserContext.setUser(Long.valueOf(userId)); // 存入ThreadLocal
}
return true; // 放行
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.removeUser(); // 清除ThreadLocal
}
}

步骤 3:自动装配拦截器

hm-common中配置拦截器,并通过 SpringBoot 自动装配生效:

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.hmall.common.config;

import com.hmall.common.interceptor.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ConditionalOnClass(DispatcherServlet.class) // 仅当存在SpringMVC时生效
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor()); // 注册拦截器
}
}

步骤 4:配置自动装配

hm-commonresources/META-INF/spring.factories中添加配置,确保微服务能扫描到:

image

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MvcConfig

6.5.3 恢复业务代码

以购物车服务为例,恢复CartServiceImpl中读取当前用户的逻辑:

image

6.6 OpenFeign 传递用户信息

微服务间通过 OpenFeign 调用时,不经过网关,需手动传递用户信息。利用 Feign 的RequestInterceptor实现自动携带请求头。

6.6.1 实现 Feign 拦截器

hm-api模块的DefaultFeignConfig中添加拦截器,自动将用户 ID 存入请求头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.hmall.api.config;

import com.hmall.common.utils.UserContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
// 其他配置(如日志级别)...

// Feign请求拦截器:自动传递用户信息
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 从ThreadLocal获取用户ID
Long userId = UserContext.getUser();
if (userId != null) {
// 添加请求头
template.header("user-info", userId.toString());
}
}
};
}
}

6.6.2 测试微服务间调用

以 “下单业务” 为例:

  1. 前端请求订单服务(trade-service)创建订单;
  2. 订单服务通过 OpenFeign 调用购物车服务(cart-service)清理购物车;
  3. 购物车服务通过拦截器读取user-info请求头,获取当前用户 ID,完成清理。

image

此时,微服务间调用可正常获取用户信息,无需额外传递参数。

7. 配置管理

7.1 配置管理概述

7.1.1 微服务配置的核心痛点

前文解决了远程调用、服务注册发现、网关路由等问题,但仍存在三大配置相关痛点:

  • 配置重复:每个微服务都包含数据库、日志、Swagger 等重复配置,维护成本高;
  • 修改繁琐:配置写死在本地文件,修改后需重启服务才能生效;
  • 路由静态:网关路由配置固化,变更需重启网关。

7.1.2 Nacos 配置中心解决方案

Nacos 兼具服务注册发现配置管理双重能力,可实现配置的集中存储、动态更新,且无需重启服务。其核心价值:

  • 集中管理共享配置,消除重复;
  • 配置变更实时推送,实现热更新;
  • 支持网关动态路由,无需重启网关。

image

7.2 配置共享:消除重复配置

配置共享是将多个微服务的通用配置(如数据库、日志)抽取到 Nacos 集中管理,微服务按需拉取合并。以cart-service为例,分两步实现:

7.2.1 步骤 1:在 Nacos 添加共享配置

识别cart-service中的重复配置,在 Nacos 控制台创建对应共享配置文件(配置管理→配置列表→+)。

1. 共享数据库配置(dataId:shared-jdbc.yaml)

image

image

配置内容(支持动态参数,默认值 + 覆盖机制):

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto

2. 共享日志配置(dataId:shared-log.yaml)

1
2
3
4
5
6
7
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"

3. 共享 Swagger 配置(dataId:shared-swagger.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城接口文档}
description: ${hm.swagger.description:黑马商城接口文档}
email: ${hm.swagger.email:zhanghuyi@itcast.cn}
concat: ${hm.swagger.concat:虎哥}
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- ${hm.swagger.package}

7.2.2 步骤 2:微服务拉取共享配置

微服务需通过bootstrap.yaml(引导阶段加载)指定 Nacos 地址及共享配置,再合并本地application.yaml

1. 引入依赖

cart-servicepom.xml中添加 Nacos 配置及 bootstrap 依赖:

1
2
3
4
5
6
7
8
9
10
<!-- Nacos配置管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 加载bootstrap文件(优先级高于application.yaml) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2. 创建 bootstrap.yaml

bootstrap.yaml在 SpringCloud 引导阶段加载,用于配置 Nacos 地址及共享配置(解决 “先有鸡还是先有蛋” 的 Nacos 地址获取问题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: cart-service # 服务名
profiles:
active: dev # 激活的环境
cloud:
nacos:
server-addr: 192.168.150.101 # Nacos地址(Ubuntu虚拟机IP)
config:
file-extension: yaml # 配置文件格式
shared-configs: # 拉取的共享配置列表
- dataId: shared-jdbc.yaml # 数据库+MyBatis配置
- dataId: shared-log.yaml # 日志配置
- dataId: shared-swagger.yaml # Swagger配置

3. 简化本地 application.yaml

删除重复配置,仅保留服务特有配置:

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8082 # 服务端口
feign:
okhttp:
enabled: true # 开启Feign的OKHttp连接池
hm:
swagger:
title: 购物车服务接口文档 # 覆盖共享配置的title
package: com.hmall.cart.controller # 接口扫描包
db:
database: hm-cart # 数据库名(对应shared-jdbc中的${hm.db.database})

4. 验证效果

重启cart-service,验证数据库连接、日志输出、Swagger 文档均正常生效,说明共享配置拉取成功。

7.2.3 关键原理:配置加载顺序

SpringCloud 配置加载优先级(从高到低):

  1. Nacos 共享配置(shared-configs);
  2. Nacos 服务特有配置([服务名]-[环境].yaml);
  3. 本地application-[环境].yaml
  4. 本地application.yaml

image

7.3 配置热更新:无需重启生效

配置热更新指 Nacos 配置修改后,微服务实时感知并应用新配置,无需重启。以 “购物车上限” 为例实现。

7.3.1 步骤 1:在 Nacos 添加热更新配置

创建服务特有配置(dataId:cart-service.yaml,所有环境共享):

image

1
2
3
hm:
cart:
maxAmount: 1 # 购物车商品数量上限(初始值1)

7.3.2 步骤 2:微服务读取热更新配置

通过@ConfigurationProperties注解绑定配置,自动支持热更新(无需@RefreshScope)。

1. 创建配置绑定类

image

1
2
3
4
5
6
7
8
9
10
11
12
package com.hmall.cart.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart") // 绑定配置前缀
public class CartProperties {
private Integer maxAmount; // 对应nacos中的hm.cart.maxAmount
}

2. 业务中使用配置

修改CartServiceImpl,替换硬编码的购物车上限:

image

7.3.3 步骤 3:测试热更新效果

  1. 初始测试:添加 2 个商品到购物车,触发 “超过上限 1” 的异常;

    image

  2. 修改 Nacos 配置:将maxAmount改为 5;

    image

  3. 无需重启测试:再次添加商品,可成功添加(上限变为 5),热更新生效。

    image

7.4 动态路由:网关路由热更新

网关默认将路由配置缓存到内存,无法通过普通热更新修改。需手动监听 Nacos 配置变更,更新网关路由表。

7.4.1 核心难点与解决方案

难点 解决方案
监听 Nacos 配置变更 利用NacosConfigManager获取ConfigService,添加监听器
更新网关路由表 利用RouteDefinitionWriter接口保存 / 删除路由

7.4.2 步骤 1:网关整合 Nacos 配置

1. 引入依赖

hm-gatewaypom.xml中添加依赖(同微服务配置共享):

1
2
3
4
5
6
7
8
9
10
<!-- Nacos配置管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 加载bootstrap文件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2. 配置 bootstrap.yaml

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway # 网关服务名
cloud:
nacos:
server-addr: 192.168.150.101 # Nacos地址
config:
file-extension: yaml
shared-configs:
- dataId: shared-log.yaml # 共享日志配置

3. 简化 application.yaml

删除本地路由配置,仅保留鉴权相关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8080 # 网关端口
hm:
jwt:
location: classpath:hmall.jks # 秘钥地址
alias: hmall # 秘钥别名
password: hmall123 # 秘钥密码
tokenTTL: 30m # 登录有效期
auth:
excludePaths: # 无需登录校验的路径
- /search/**
- /users/login
- /items/**

7.4.3 步骤 2:实现动态路由加载器

编写DynamicRouteLoader类,监听 Nacos 路由配置并更新网关路由表。

1. 动态路由加载器代码

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.hmall.gateway.route;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

// 路由更新工具(SpringCloud Gateway提供)
private final RouteDefinitionWriter writer;
// Nacos配置管理(自动装配,用于获取ConfigService)
private final NacosConfigManager nacosConfigManager;

// Nacos中路由配置的dataId和group
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
// 记录已加载的路由ID,用于更新前删除旧路由
private final Set<String> routeIds = new HashSet<>();

// 项目启动时执行初始化
@PostConstruct
public void initRouteConfigListener() throws NacosException {
// 1. 注册监听器并首次拉取配置(getConfigAndSignListener=拉取+监听)
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null; // 使用默认线程池
}

// 配置变更时触发
@Override
public void receiveConfigInfo(String configInfo) {
updateConfigInfo(configInfo);
}
});
// 2. 首次启动加载路由
updateConfigInfo(configInfo);
}

// 解析配置并更新路由表
private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更:{}", configInfo);
// 1. JSON反序列化为RouteDefinition列表
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2. 清空旧路由
routeIds.forEach(routeId -> writer.delete(Mono.just(routeId)).subscribe());
routeIds.clear();
// 3. 加载新路由
if (CollUtils.isNotEmpty(routeDefinitions)) {
routeDefinitions.forEach(route -> {
writer.save(Mono.just(route)).subscribe(); // 保存新路由
routeIds.add(route.getId()); // 记录路由ID
});
}
}
}

7.4.4 步骤 3:在 Nacos 添加路由配置

创建 JSON 格式的路由配置(dataId:gateway-routes.json,类型:JSON):

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
[
{
"id": "item",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}
],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/carts/**"}
}
],
"filters": [],
"uri": "lb://cart-service"
},
{
"id": "user",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
}
],
"filters": [],
"uri": "lb://user-service"
},
{
"id": "trade",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/orders/**"}
}
],
"filters": [],
"uri": "lb://trade-service"
},
{
"id": "pay",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0":"/pay-orders/**"}
}
],
"filters": [],
"uri": "lb://pay-service"
}
]

7.4.5 步骤 4:测试动态路由

  1. 初始测试:启动网关后,访问 http://localhost:8080/search/list,返回 404(无路由配置);

    image

  2. 添加 Nacos 配置:提交gateway-routes.json后,无需重启网关;

  3. 验证路由:再次访问同一地址,返回商品列表(路由生效)。

    image

7.5 总结

功能 实现方式 核心组件 / 注解
配置共享 Nacos 集中存储,微服务通过 bootstrap 拉取 bootstrap.yamlshared-configs
配置热更新 @ConfigurationProperties绑定配置 @ConfigurationProperties
动态路由 监听 Nacos 配置,更新网关路由表 NacosConfigManagerRouteDefinitionWriter

Nacos 配置中心彻底解决了微服务配置的重复、静态、难维护问题,是微服务架构的核心基础设施之一。