SpringCloud
1. 认识微服务
本章从单体架构的优缺点切入,分析大型项目采用单体架构的问题,以及微服务架构的解决思路。
1.1 单体架构
单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在同一个工程中开发;部署时需对所有模块一起编译、打包;架构设计和开发模式非常简单。
当项目规模较小时,单体架构上手快、部署运维方便,因此早期小型项目多采用这种模式。
但随着业务规模扩大、团队人员增加,单体架构的问题逐渐凸显:
- 团队协作成本高:数十人协作同一项目时,模块间代码边界模糊,分支合并易陷入 “冲突泥潭”。
- 系统发布效率低:任何模块变更都需发布整个系统,模块间制约多,一次发布可能耗时数十分钟甚至数小时。
- 系统可用性差:所有功能作为一个服务部署,相互影响大。热点功能耗尽资源时,会导致其他服务不可用。
单体架构可用性问题演示(以黑马商城为例)
为直观展示单体架构的可用性问题,对黑马商城的 hm-service 模块做如下改造与测试:
-
修改代码模拟耗时:修改
com.hmall.controller.HelloController中的hello方法,模拟接口执行耗时。 -
启动项目与接口测试:启动项目后,有两个无需登录即可访问的接口:
-
http://localhost:8080/search/list
测试发现
/search/list接口响应正常(耗时约 30 毫秒)。
-
模拟热点接口高并发:假设
/hi是热点接口,使用 Jemeter 模拟 500 个用户并发访问。课前资料提供了 Jemeter 测试脚本。 -
导入 Jemeter 并执行测试:
由于
/hi接口执行耗时(500 毫秒),服务端处理能力有限,请求会逐渐积压,最终耗尽 Tomcat 资源。此时,原本正常的/search/list接口也会被拖慢。 -
验证影响:启动 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 | |
同时需配置启动项激活 local 环境:
2.1.1 登录业务
登录业务流程:
服务端入口为 com.hmall.controller.UserController 中的 login 方法:
2.1.2 搜索商品
在首页搜索框输入关键字并点击搜索,进入搜索列表页面:
该页面调用接口 /search/list,服务端入口为 com.hmall.controller.SearchController 中的 search 方法:
2.1.3 购物车业务
-
加入购物车:在商品列表点击 “加入购物车” 按钮,商品加入购物车:
-
购物车列表页:加入成功后进入购物车列表页,可查看、修改、删除购物车商品:
相关功能入口为 com.hmall.controller.CartController:
查询购物车列表时,需判断商品最新价格和状态,因此要查询商品信息,业务流程:
2.1.4 下单业务
-
订单结算页:在购物车页面点击 “结算” 按钮,进入订单结算页面:
-
提交订单:点击 “提交订单”,服务端执行 3 件事:
- 创建新订单
- 扣减商品库存
- 清理购物车商品
业务入口为 com.hmall.controller.OrderController 中的 createOrder 方法:
2.1.5 支付业务
下单后跳转到支付页面(目前仅支持余额支付):
选择 “余额支付” 后,服务端会创建支付流水单并返回流水单号;用户输入密码点击 “确认支付” 时,服务端执行:
- 校验用户密码
- 扣减余额
- 修改支付流水状态
- 修改交易订单状态
请求入口为 com.hmall.controller.PayController:
2.2 服务拆分原则
服务拆分需重点考虑两个问题:
- 什么时候拆?
- 如何拆?
2.2.1 什么时候拆
- 初创项目:优先验证可行性,建议采用单体架构。原因是:
- 开发成本低,可快速产出产品投入市场验证
- 若产品不符合市场需求,损失较小
- 缺点:后期拆分可能面临代码耦合问题(前易后难)
- 大型项目:立项时目标明确,建议直接采用微服务架构。原因是:
- 长远考虑,避免后期拆分的麻烦
- 缺点:前期投入人力和时间成本高(前难后易)
2.2.2 怎么拆
微服务拆分的核心目标是高内聚、低耦合:
- 高内聚:每个微服务职责单一,内部业务关联度高、完整性强。修改业务时应尽量仅涉及当前服务,降低变更成本。
- 低耦合:服务功能相对独立,减少对其他服务的依赖;若有依赖,需保证接口稳定性(外观不变)。例如:订单服务需商品数据时,应调用商品服务的接口,而非直接访问商品数据库,避免数据耦合。
拆分方式
- 纵向拆分:按功能模块拆分(如黑马商城的用户、订单、购物车等模块),提高服务内聚性。
- 横向拆分:抽取各模块的公共业务为通用服务(如消息发送、风控管理),提高复用性,降低耦合。
黑马商城按纵向拆分可分为:
- 用户服务
- 商品服务
- 订单服务
- 购物车服务
- 支付服务
2.3 拆分购物车、商品服务
微服务项目的两种工程结构:
- 完全解耦:每个服务独立工程(可跨语言),耦合度低但管理麻烦。
- Maven 聚合:父工程下的多个 Module,集中管理(适合授课),但编译时间较长。
课程采用Maven 聚合工程,基于黑马商城父工程 hmall 拆分服务(父工程已预设 SpringBoot、SpringCloud 依赖版本)。
2.3.1 商品服务(item-service)
-
创建 Module:在
hmall父工程中创建 Maven 模块,命名为item-service,JDK 版本为 11。 -
配置依赖(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> -
编写启动类:
1
2
3
4
5
6
7
8
9
10
11
12
13package 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);
}
} -
配置文件:从
hm-service拷贝配置文件,修改application.yaml: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
41server:
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.yaml和application-local.yaml直接拷贝,修改数据库连接参数为 Ubuntu 虚拟机的 IP 和密码) -
拷贝商品相关代码:将
hm-service中与商品管理相关的代码(实体类、Controller、Service、Mapper 等)拷贝到item-service。 -
修改代码依赖:调整
ItemServiceImpl中deductStock方法的ItemMapper包路径(因包结构变化)。 -
导入数据库表:在 Docker 的 MySQL 中执行课前资料提供的 SQL 文件,创建
hm-item数据库(每个微服务独立数据库)。 -
启动与测试:
-
配置启动项,激活
local环境: -
启动
ItemApplication,访问 Swagger 文档:http://localhost:8081/doc.html -
测试 “根据 id 批量查询商品” 接口,参数:
1
100002672302,100002624500,100002533430结果如下:
-
2.3.2 购物车服务(cart-service)
-
创建 Module:在
hmall父工程中创建 Maven 模块,命名为cart-service。 -
配置依赖(pom.xml):与
item-service类似,仅模块名为cart-service。 -
编写启动类:
1
2
3
4
5
6
7
8
9
10
11
12
13package 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);
}
} -
配置文件:拷贝
item-service的配置文件,修改application.yaml:1
2
3
4
5
6
7
8server:
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即可,略... -
拷贝购物车相关代码:将
hm-service中与购物车相关的代码拷贝到cart-service,最终结构: -
修改代码:在
CartServiceImpl中处理依赖问题:- 暂时写死用户 ID(因登录功能未拆分,可以写成1L)
- 注释查询商品信息的代码(需调用商品服务,待后续解决,暂时先把
handleCartItems函数内容注释掉)
-
导入数据库表:执行课前资料的 SQL 文件,创建
hm-cart数据库及cart表。 -
启动与测试:
-
配置启动项激活
local环境: -
启动
CartApplication,访问 Swagger 文档:http://localhost:8082/doc.html -
测试 “查询我的购物车列表” 接口,结果中商品相关字段为空(因代码注释)。
-
问题:如何在 cart-service 中查询 item-service 的商品信息?
当前购物车服务查询商品信息的代码被注释,需解决服务间通信问题,后续将介绍微服务远程调用方案。
2.4 服务调用
在拆分购物车服务时,发现购物车查询需依赖商品服务的商品信息,但商品逻辑已迁移至 item-service,导致购物车数据不完整。因此需将本地方法调用改造为跨微服务的远程调用(RPC,Remote Procedure Call)。
2.4.1 远程调用流程
购物车查询商品的流程变为:
代码中需改造的核心步骤:
2.4.2 如何实现跨服务 HTTP 调用?
前端通过浏览器发送 HTTP 请求可实现 “远程查询服务端数据”(如 Swagger 测试商品接口 http://localhost:8081/items)。同理,若在 cart-service 中模拟 “浏览器发送 HTTP 请求”,即可调用 item-service 的接口。
2.4.2.1 RestTemplate(Spring HTTP 请求工具)
Spring 提供 RestTemplate 简化 HTTP 请求发送,支持 GET、POST、PUT、DELETE 等多种请求类型。
RestTemplate 说明:
同步 HTTP 客户端,基于底层 HTTP 库(如 JDK
HttpURLConnection、Apache HttpComponents 等)封装模板方法。支持常见 HTTP 场景,且可配置为共享组件(启动时初始化,避免并发修改)。
2.4.2.2 注册 RestTemplate 为 Spring Bean
在 cart-service 中创建配置类,将 RestTemplate 注册到 Spring 容器:
1 | |
2.4.3 远程调用实战(购物车调用商品服务)
修改 cart-service 中 CartServiceImpl 的 handleCartItems 方法,通过 RestTemplate 发送 HTTP 请求到 item-service:
1 | |
2.4.4 测试远程调用
重启 cart-service,再次测试 “查询我的购物车列表” 接口,商品相关数据(价格、库存、状态等)可正常返回。
2.5 总结
微服务拆分时机
- 创业型公司:先以单体架构快速迭代,验证市场模型;业务跑通后,再随规模扩大拆分微服务(前易后难)。
- 大型企业:项目初期直接搭建微服务架构(资源充足,前难后易)。
微服务拆分原则
- 核心目标:高内聚、低耦合。
- 拆分方式:
- 纵向拆分:按业务功能模块拆分(如用户、订单、商品)。
- 横向拆分:抽取通用业务为独立服务(如消息、风控),提高复用性。
微服务远程调用(RPC)
微服务间远程调用称为 RPC,常见实现方式:
- 基于 HTTP 协议:不关心服务提供者技术,仅依赖 HTTP 接口,符合微服务解耦需求。
- 基于 Dubbo 协议:高性能二进制协议,需服务提供者和消费者遵循 Dubbo 规范。
课程采用 HTTP 方式,通过 Spring RestTemplate 实现:
- 注册
RestTemplate到 Spring 容器。 - 调用
RestTemplate方法发送请求(如getForObject、postForObject、exchange等)。
3. 服务注册和发现
上一章实现了微服务拆分及跨服务远程调用,但手动发送 HTTP 请求的方式存在问题。当商品服务(item-service)多实例部署时,会面临地址管理、调用选择、故障处理、动态扩展等挑战。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个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)。
注册中心是管理服务地址的核心组件,三者关系如下:
核心流程:
- 服务注册:服务启动时,将自身信息(服务名、IP、端口)注册到注册中心。
- 服务订阅:消费者从注册中心获取目标服务的实例列表。
- 负载均衡:消费者从实例列表中选择一个实例发起调用。
- 动态更新:
- 服务提供者定期发送心跳到注册中心,报告健康状态。
- 注册中心若长时间未收到心跳,将剔除该实例。
- 新实例启动时自动注册,注册中心会将变更通知消费者,更新其本地实例列表。
3.2 Nacos 注册中心
国内常用的注册中心框架有 Eureka、Nacos、Consul 等。课程选择Nacos(Alibaba 出品),原因是中文文档丰富,且兼具配置管理功能。
Nacos 官网:https://nacos.io/
3.2.1 部署 Nacos(基于 Docker)
-
准备 MySQL 数据库:
- 将课前资料中的 Nacos 初始化 SQL 文件导入 Docker 的 MySQL 容器。
- 最终生成的表结构:
-
配置 Nacos 环境变量:
- 找到课前资料的
nacos文件夹,修改nacos/custom.env中的 MySQL 地址为虚拟机 IP:
- 找到课前资料的
-
上传并部署 Nacos:
-
将
nacos目录上传至 Ubuntu 虚拟机的/root目录。 -
执行 Docker 命令启动 Nacos:
1
2
3
4
5
6
7
8sudo 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
-
-
访问 Nacos 控制台:
- 地址:
http://虚拟机IP:8848/nacos/(替换为实际 IP)。 - 登录账号密码均为
nacos:
- 地址:
3.3 服务注册(以item-service为例)
将服务注册到 Nacos 的步骤:
- 引入依赖
- 配置Nacos地址
- 重启
3.3.1 引入依赖
在 item-service 的 pom.xml 中添加 Nacos 注册依赖:
1 | |
3.3.2 配置 Nacos 地址
在 item-service 的 application.yaml 中添加配置:
1 | |
3.3.3 启动多实例并验证
-
配置多实例:
-
为
item-service新增启动项,修改端口(如 8083)避免冲突:
-
-
启动实例:
- 启动两个
item-service实例(端口 8081 和 8083)。
- 启动两个
-
验证注册结果:
- 登录 Nacos 控制台,在 “服务列表” 中查看
item-service:
- 点击 “详情” 可查看两个实例的 IP 和端口:
- 登录 Nacos 控制台,在 “服务列表” 中查看
3.4 服务发现(以cart-service为例)
服务消费者通过 Nacos 获取服务实例列表并调用的步骤:
- 引入依赖
- 配置Nacos地址
- 发现并调用服务
3.4.1 引入依赖
在 cart-service 的 pom.xml 中添加依赖(包含服务注册和发现功能):
1 | |
(注:负载均衡依赖用于从多实例中选择一个进行调用)
3.4.2 配置 Nacos 地址
在 cart-service 的 application.yaml 中添加配置:
1 | |
3.4.3 发现并调用服务
-
注入
DiscoveryClient:SpringCloud 自动装配的DiscoveryClient可用于获取服务实例列表:1
private final DiscoveryClient discoveryClient; -
修改远程调用逻辑:从 Nacos 获取
item-service的实例列表,通过负载均衡(如随机算法)选择一个实例调用: -
测试验证:启动
cart-service,通过 Swagger 测试购物车查询接口,可正常获取商品数据,且调用会随机分配到item-service的两个实例。
总结
- 注册中心作用:解决服务地址管理、动态扩缩容、故障自动剔除等问题。
- Nacos 优势:中文支持好,兼具服务注册发现和配置管理功能。
- 核心流程:服务注册(启动时上报地址)→ 服务发现(消费者订阅实例列表)→ 负载均衡(选择实例调用)→ 动态更新(基于心跳和通知)。
4. OpenFeign
上一章通过 Nacos 实现服务治理,用RestTemplate实现远程调用,但存在代码复杂、调用体验不统一的问题(需手动处理服务发现、负载均衡、HTTP 请求构建)。
OpenFeign 可通过声明式注解简化远程调用,让其像本地方法调用一样便捷。
4.1 快速入门
以cart-service调用item-service的商品查询接口为例,演示 OpenFeign 的使用。
4.1.1 引入依赖
在cart-service的pom.xml中添加 OpenFeign 及负载均衡依赖:
1 | |
4.1.2 启用 OpenFeign
在cart-service的启动类CartApplication上添加@EnableFeignClients注解,开启 OpenFeign 功能:
4.1.3 编写 Feign 客户端
定义接口,通过 SpringMVC 注解声明远程调用的核心参数(请求方式、路径、参数、返回值),OpenFeign 会自动生成实现类。
在cart-service中创建ItemClient接口:
1 | |
4.1.4 使用 Feign 客户端
在CartServiceImpl中注入ItemClient,直接调用接口方法(无需手动处理服务发现、HTTP 请求):
优势:OpenFeign 自动完成「服务发现→负载均衡→HTTP 请求发送→响应解析」全流程,代码简洁且与本地调用体验一致。
4.2 连接池优化
Feign 底层依赖 HTTP 客户端实现,默认使用HttpURLConnection(无连接池,性能差)。推荐替换为支持连接池的OKHttp或Apache HttpClient,提升并发性能。
4.2.1 引入 OKHttp 依赖
在cart-service的pom.xml中添加依赖:
1 | |
4.2.2 开启连接池
在cart-service的application.yaml中配置启用 OKHttp:
1 | |
4.2.3 验证连接池生效
通过断点调试验证底层客户端是否切换为 OKHttp:
- 在
org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient的execute方法处打断点。
- 以 Debug 模式启动
cart-service,调用购物车查询接口,进入断点。 - 观察
client对象类型,确认是OkHttpClient。
4.3 最佳实践(Feign 客户端抽取)
多个服务(如cart-service、trade-service)可能需要调用同一服务(如item-service)的接口,若每个服务都重复定义 Feign 客户端,会导致代码冗余。解决方式是将 Feign 客户端抽取到公共模块。
4.3.1 思路分析
两种抽取方案对比:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 方案 1:公共模块抽取 | 抽取到独立的hm-api模块 |
结构清晰,抽取简单 | 服务间耦合度较高 |
| 方案 2:服务内抽取 | 每个服务单独抽取 API 模块 | 耦合度低 | 结构复杂,维护成本高 |
课程采用方案 1(适合现有项目结构)。
4.3.2 创建公共 API 模块(hm-api)
-
创建 Module:在
hmall父工程下创建hm-api模块。 -
配置依赖(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> -
拷贝 Feign 客户端及 DTO:将
cart-service中的ItemClient和ItemDTO拷贝到hm-api模块,最终结构:
4.3.3 服务引入公共模块
-
引入依赖:在
cart-service的pom.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> -
删除冗余代码:删除
cart-service中原有ItemClient和ItemDTO。 -
解决扫描问题:启动
cart-service会报错 ——ItemClient位于com.hmall.api.client包,而启动类扫描范围是com.hmall.cart,无法识别 Feign 客户端。两种解决方式:
- 方式 1:指定扫描包(推荐,适用于多个 Feign 客户端):
- 方式 2:指定 Feign 客户端类(适用于少量客户端):
4.4 日志配置
OpenFeign 的日志输出需满足两个条件:
- Feign 客户端所在包的日志级别为
DEBUG。 - 配置 Feign 的日志级别(默认
NONE,不输出日志)。
4.4.1 Feign 日志级别
| 级别 | 说明 |
|---|---|
| NONE | 不记录任何日志(默认) |
| BASIC | 记录请求方法、URL、响应状态码、执行时间 |
| HEADERS | 在 BASIC 基础上,增加请求 / 响应头信息 |
| FULL | 记录完整请求 / 响应(头、体、元数据) |
4.4.2 配置日志级别
-
定义日志配置类:在
hm-api模块中创建配置类:1
2
3
4
5
6
7
8
9
10
11package 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级别
}
} -
生效配置:
-
局部生效(仅对指定 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 { ... }
-
-
配置包日志级别:在
cart-service的application.yaml中添加:1
2
3logging:
level:
com.hmall.api.client: debug # Feign客户端所在包的日志级别为DEBUG
4.4.3 日志示例(FULL 级别)
1 | |
第四章完成微服务拆分后,实际联调中暴露了多入口维护、用户信息传递、配置冗余等问题。所以后续将会通过微服务网关解决请求入口与鉴权问题,通过Nacos 统一配置解决配置管理问题。
5. 网关路由
5.1 认识网关
5.1.1 网关的定义与作用
网关是网络数据传输的 “关口”,负责在不同网络间进行路由转发、安全校验、数据过滤。通俗来讲,网关类似园区传达室的 “大爷”,承担两大核心职责:
- 安全拦截:验证访问者身份,拦截非法请求;
- 路由转发:接收外部请求,传递给目标接收者。
在微服务架构中,前端请求不直接访问微服务,必须经过网关层,流程如下:
- 网关对请求进行登录身份校验,校验通过才允许放行;
- 校验通过后,网关根据请求特征判断目标微服务,将请求转发至对应服务。
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,作为网关微服务:
5.2.2 步骤 2:引入核心依赖
在hm-gateway的pom.xml中添加网关、Nacos 服务发现、负载均衡依赖:
1 | |
5.2.3 步骤 3:编写启动类
在com.hmall.gateway包下创建启动类,无需额外注解(@SpringBootApplication包含自动配置):
1 | |
5.2.4 步骤 4:配置路由规则
在hm-gateway的resources目录创建application.yaml,配置端口、Nacos 地址及路由规则(需替换 Nacos 地址为实际虚拟机 IP):
1 | |
5.2.5 步骤 5:测试路由功能
-
启动服务:依次启动 Nacos、
GatewayApplication、item-service; -
访问测试:通过网关访问商品接口,URL 格式为
http://网关IP:网关端口/微服务接口路径 -
验证结果:接口返回商品数据,说明路由转发成功。
启动user-service、cart-service后,前端可通过http://localhost:8080统一入口访问所有微服务功能,无需维护多个服务地址。
5.3 路由规则与断言详解
5.3.1 路由规则核心结构
路由规则的基本语法如下,routes是RouteDefinition的集合,支持多规则配置:
1 | |
routes对应的类型及RouteDefinition核心属性:
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 | |
6. 网关登录校验
6.1 鉴权思路分析
单体架构中,一次登录校验即可在所有业务中共享用户信息;但微服务拆分后,服务独立部署、数据不共享,若每个微服务单独实现登录校验,会存在秘钥泄露风险和代码冗余问题。
6.1.1 核心痛点
- 安全性差:每个微服务需存储 JWT 秘钥,易泄露;
- 开发效率低:重复编写登录校验、权限判断代码。
6.1.2 网关统一鉴权方案
网关作为所有请求的入口,可集中处理登录校验,解决上述问题:
- 秘钥集中管理:仅网关和用户服务保存 JWT 秘钥;
- 代码统一开发:登录校验逻辑仅在网关实现一次。
鉴权流程:
6.1.3 待解决问题
- 如何在网关转发请求前执行登录校验?
- 网关校验通过后,如何将用户信息传递给微服务?
- 微服务间调用(不经过网关)如何传递用户信息?
6.2 网关过滤器:鉴权的技术基础
网关的请求处理依赖过滤器链,登录校验需通过过滤器在请求转发前执行。
6.2.1 Gateway 工作原理
Gateway 处理请求的核心流程:
- 客户端请求进入网关,
HandlerMapping匹配对应的路由规则(Route); WebHandler加载该路由的过滤器链(Filter Chain),按顺序执行过滤器;- 过滤器逻辑分为
pre(转发前)和post(响应后)两部分,仅pre逻辑全部通过后,请求才会转发到微服务; - 微服务返回响应后,倒序执行过滤器的
post逻辑,最终返回给客户端。
关键结论:登录校验逻辑需实现为pre阶段的过滤器,且执行顺序需在请求转发过滤器(NettyRoutingFilter)之前。
6.2.2 网关过滤器类型
Gateway 提供两种核心过滤器,均支持自定义:
| 过滤器类型 | 作用范围 | 配置方式 |
|---|---|---|
GatewayFilter |
指定路由(灵活) | 需在 yaml 中配置 |
GlobalFilter |
所有路由(全局) | 无需配置,自动生效 |
两种过滤器的方法签名一致,最终会被统一封装到过滤器链中,按Order排序执行(值越小,优先级越高):
1 | |
6.2.3 内置 GatewayFilter 使用
Gateway 内置多种GatewayFilter(如添加请求头、路径重写),无需编码,直接在 yaml 中配置即可。
示例:添加请求头过滤器
1 | |
6.3 自定义过滤器
实际业务中,内置过滤器无法满足复杂需求(如登录校验),需自定义过滤器。
6.3.1 自定义 GatewayFilter
需继承AbstractGatewayFilterFactory,支持动态配置参数,作用于指定路由。
示例 1:基础无参过滤器
1 | |
配置使用:
1 | |
示例 2:带参数的过滤器
1 | |
配置使用:
1 | |
6.3.2 自定义 GlobalFilter
直接实现GlobalFilter和Ordered接口,作用于所有路由,无需配置,适合全局逻辑(如登录校验)。
示例:简单拦截过滤器
1 | |
6.4 实战:网关登录校验(基于 JWT)
利用GlobalFilter实现 JWT 校验,核心流程:排除白名单→获取 token→校验 token→传递用户信息→放行。
6.4.1 准备 JWT 工具
从hm-service拷贝 JWT 相关工具到网关模块:
JwtTool:JWT 生成、解析工具;JwtProperties:JWT 配置(秘钥、过期时间);AuthProperties:白名单配置(无需校验的路径);- 秘钥文件
hmall.jks。
配置 yaml 参数:
1 | |
6.4.2 实现登录校验过滤器
1 | |
6.4.3 测试鉴权效果
-
启动服务:网关、用户服务、商品服务;
-
测试白名单路径:访问 http://localhost:8080/items/1 ,未登录可正常返回数据;
-
测试受保护路径:访问 http://localhost:8080/carts ,未登录返回 401;
6.5 微服务获取用户信息
网关校验通过后,需将用户信息传递给微服务。通过请求头 + 拦截器 + ThreadLocal实现:
- 网关:将用户 ID 存入请求头;
- 微服务:通过拦截器读取请求头,存入 ThreadLocal 供业务使用。
6.5.1 网关传递用户信息
修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:
6.5.2 微服务拦截器获取用户
在hm-common模块实现拦截器,统一处理用户信息(所有微服务复用)。
步骤 1:ThreadLocal 工具类
hm-common中已提供UserContext,用于存储当前线程的用户信息:
步骤 2:实现拦截器
1 | |
步骤 3:自动装配拦截器
在hm-common中配置拦截器,并通过 SpringBoot 自动装配生效:
1 | |
步骤 4:配置自动装配
在hm-common的resources/META-INF/spring.factories中添加配置,确保微服务能扫描到:
1 | |
6.5.3 恢复业务代码
以购物车服务为例,恢复CartServiceImpl中读取当前用户的逻辑:
6.6 OpenFeign 传递用户信息
微服务间通过 OpenFeign 调用时,不经过网关,需手动传递用户信息。利用 Feign 的RequestInterceptor实现自动携带请求头。
6.6.1 实现 Feign 拦截器
在hm-api模块的DefaultFeignConfig中添加拦截器,自动将用户 ID 存入请求头:
1 | |
6.6.2 测试微服务间调用
以 “下单业务” 为例:
- 前端请求订单服务(
trade-service)创建订单; - 订单服务通过 OpenFeign 调用购物车服务(
cart-service)清理购物车; - 购物车服务通过拦截器读取
user-info请求头,获取当前用户 ID,完成清理。
此时,微服务间调用可正常获取用户信息,无需额外传递参数。
7. 配置管理
7.1 配置管理概述
7.1.1 微服务配置的核心痛点
前文解决了远程调用、服务注册发现、网关路由等问题,但仍存在三大配置相关痛点:
- 配置重复:每个微服务都包含数据库、日志、Swagger 等重复配置,维护成本高;
- 修改繁琐:配置写死在本地文件,修改后需重启服务才能生效;
- 路由静态:网关路由配置固化,变更需重启网关。
7.1.2 Nacos 配置中心解决方案
Nacos 兼具服务注册发现和配置管理双重能力,可实现配置的集中存储、动态更新,且无需重启服务。其核心价值:
- 集中管理共享配置,消除重复;
- 配置变更实时推送,实现热更新;
- 支持网关动态路由,无需重启网关。
7.2 配置共享:消除重复配置
配置共享是将多个微服务的通用配置(如数据库、日志)抽取到 Nacos 集中管理,微服务按需拉取合并。以cart-service为例,分两步实现:
7.2.1 步骤 1:在 Nacos 添加共享配置
识别cart-service中的重复配置,在 Nacos 控制台创建对应共享配置文件(配置管理→配置列表→+)。
1. 共享数据库配置(dataId:shared-jdbc.yaml)
配置内容(支持动态参数,默认值 + 覆盖机制):
1 | |
2. 共享日志配置(dataId:shared-log.yaml)
1 | |
3. 共享 Swagger 配置(dataId:shared-swagger.yaml)
1 | |
7.2.2 步骤 2:微服务拉取共享配置
微服务需通过bootstrap.yaml(引导阶段加载)指定 Nacos 地址及共享配置,再合并本地application.yaml。
1. 引入依赖
在cart-service的pom.xml中添加 Nacos 配置及 bootstrap 依赖:
1 | |
2. 创建 bootstrap.yaml
bootstrap.yaml在 SpringCloud 引导阶段加载,用于配置 Nacos 地址及共享配置(解决 “先有鸡还是先有蛋” 的 Nacos 地址获取问题):
1 | |
3. 简化本地 application.yaml
删除重复配置,仅保留服务特有配置:
1 | |
4. 验证效果
重启cart-service,验证数据库连接、日志输出、Swagger 文档均正常生效,说明共享配置拉取成功。
7.2.3 关键原理:配置加载顺序
SpringCloud 配置加载优先级(从高到低):
- Nacos 共享配置(
shared-configs); - Nacos 服务特有配置(
[服务名]-[环境].yaml); - 本地
application-[环境].yaml; - 本地
application.yaml。
7.3 配置热更新:无需重启生效
配置热更新指 Nacos 配置修改后,微服务实时感知并应用新配置,无需重启。以 “购物车上限” 为例实现。
7.3.1 步骤 1:在 Nacos 添加热更新配置
创建服务特有配置(dataId:cart-service.yaml,所有环境共享):
1 | |
7.3.2 步骤 2:微服务读取热更新配置
通过@ConfigurationProperties注解绑定配置,自动支持热更新(无需@RefreshScope)。
1. 创建配置绑定类
1 | |
2. 业务中使用配置
修改CartServiceImpl,替换硬编码的购物车上限:
7.3.3 步骤 3:测试热更新效果
-
初始测试:添加 2 个商品到购物车,触发 “超过上限 1” 的异常;
-
修改 Nacos 配置:将
maxAmount改为 5; -
无需重启测试:再次添加商品,可成功添加(上限变为 5),热更新生效。
7.4 动态路由:网关路由热更新
网关默认将路由配置缓存到内存,无法通过普通热更新修改。需手动监听 Nacos 配置变更,更新网关路由表。
7.4.1 核心难点与解决方案
| 难点 | 解决方案 |
|---|---|
| 监听 Nacos 配置变更 | 利用NacosConfigManager获取ConfigService,添加监听器 |
| 更新网关路由表 | 利用RouteDefinitionWriter接口保存 / 删除路由 |
7.4.2 步骤 1:网关整合 Nacos 配置
1. 引入依赖
在hm-gateway的pom.xml中添加依赖(同微服务配置共享):
1 | |
2. 配置 bootstrap.yaml
1 | |
3. 简化 application.yaml
删除本地路由配置,仅保留鉴权相关配置:
1 | |
7.4.3 步骤 2:实现动态路由加载器
编写DynamicRouteLoader类,监听 Nacos 路由配置并更新网关路由表。
1. 动态路由加载器代码
1 | |
7.4.4 步骤 3:在 Nacos 添加路由配置
创建 JSON 格式的路由配置(dataId:gateway-routes.json,类型:JSON):
1 | |
7.4.5 步骤 4:测试动态路由
-
初始测试:启动网关后,访问 http://localhost:8080/search/list,返回 404(无路由配置);
-
添加 Nacos 配置:提交
gateway-routes.json后,无需重启网关; -
验证路由:再次访问同一地址,返回商品列表(路由生效)。
7.5 总结
| 功能 | 实现方式 | 核心组件 / 注解 |
|---|---|---|
| 配置共享 | Nacos 集中存储,微服务通过 bootstrap 拉取 | bootstrap.yaml、shared-configs |
| 配置热更新 | @ConfigurationProperties绑定配置 |
@ConfigurationProperties |
| 动态路由 | 监听 Nacos 配置,更新网关路由表 | NacosConfigManager、RouteDefinitionWriter |
Nacos 配置中心彻底解决了微服务配置的重复、静态、难维护问题,是微服务架构的核心基础设施之一。
























































































6.5.2 微服务拦截器获取用户
























