微服务实践--服务治理+网关管理+配置管理
黑马商城
《黑马商城》2025知识点整理-面试速成_黑马商城面试-CSDN博客
单体架构
架构简单,部署成本低;团队协作成本高,系统发布效率低,系统可用性差(功能之间的影响较大)
认识微服务
服务架构演变
单体架构:将业务的所有功能集中在一个项目中开发,打包成一个包部署
优点:架构简单;部署成本低
缺点:耦合度高
分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
优点:降低服务耦合;有利于服务升级拓展
缺点:需要考虑问题较多(服务拆分粒度如何?服务集群地址如何维护?远程调用?)
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
- 单一职责:拆分粒度小,每一个服务都对应唯一的业务能力
- 面向服务:对外暴露业务接口
- 隔离性强
微服务技术对比
| Dubbo | SpringCloud | SpringCloudAlibaba | |
|---|---|---|---|
| 注册中心 | zookeeper、redis | Eureka、Consul | Nacos、Eureka |
| 服务远程调用 | Dubbo协议 | Feign(http协议) | Dubbo、Feign |
| 配置中心 | 无 | SpringCloudConfig | SpringCloudConfig、Nacos |
| 服务网关 | 无 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |
| 服务监控和保护 | dubbo-admin、功能弱 | Hystrix | Sentinel |
SpringCloud
服务拆分+远程调用
服务拆分注意事项
1.不同微服务,不要重复开发相同业务
2.微服务数据独立,不要访问其他微服务的数据库
熟悉黑马商城

服务拆分原则
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高
- 低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖
拆分方式:
纵向拆分:按照业务模块来拆分;横向拆分:抽取公共服务,提高复用性
拆分
每个微服务都会有自己的模块
item-service,cart-service
1.新建模块
2.把pom.xml复制过来,然后把不需要的依赖删除
3.新建application类 也是可以复制粘贴过来 然后把controller mapper domain service子包创建
4.配置文件复制粘贴过来 修改端口号
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:
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
5.依次将各类进行导入
远程调用
1.启动类注入restTemplate
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@RequiredArgsConstructor //可以帮我们自动生成final修饰的成员的构造函数
这种构造器注入RestTemplate的方式,比直接用@Autowired字段注入更优,核心原因是它更符合依赖注入(DI)的设计原则、提升了代码的可测试性、可读性和稳定性,具体可以从以下6个关键维度分析:
一、先明确:两种注入方式的本质区别
先看对比,帮你直观理解差异:
| 注入方式 | 核心写法 | 依赖可见性 | 可测试性 | 依赖不可变 | 构造顺序 |
|---|---|---|---|---|---|
| 构造器注入(推荐) | 构造方法接收依赖,赋值给成员变量 | 显式(构造参数暴露依赖) | 极高(可手动传测试实例) | 支持(成员变量可加final) | 强制依赖优先初始化 |
@Autowired字段注入 | 成员变量加@Autowired,Spring直接赋值 | 隐式(依赖藏在字段里) | 低(需反射/ Spring上下文) | 不支持(字段不能final) | 依赖初始化顺序不确定 |
二、构造器注入更优的核心原因
1. 显式声明依赖,可读性更强(依赖透明)
构造器的参数列表就是“依赖清单”,任何人阅读代码时,不用看类内部的字段注解,就能立刻知道:CartServiceImpl必须依赖RestTemplate才能工作(无RestTemplate就无法创建实例)。
而@Autowired字段注入的依赖是“隐藏”的——只有点开类内部,看到字段上的注解才知道依赖,对于复杂类,很容易遗漏关键依赖。
2. 强制依赖不可缺少,避免“半初始化”对象(依赖非空保证)
构造器是创建对象的唯一入口(除非用工厂方法),如果依赖RestTemplate是必需的(CartServiceImpl没有它就无法完成核心功能),构造器注入会强制调用者(比如Spring容器)必须提供RestTemplate实例,否则无法创建CartServiceImpl对象。
而字段注入时,若Spring容器中没有RestTemplate实例(比如配置遗漏),启动时可能不报错(取决于@Autowired(required = true),默认是true,但报错信息不如构造器注入清晰),或运行时调用restTemplate才抛出NullPointerException——此时对象已经“半初始化”,排查问题更麻烦。
3. 支持依赖不可变(final修饰),提升线程安全
构造器注入时,成员变量restTemplate可以加final修饰:
private final RestTemplate restTemplate; // 不可变
public CartServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate; // 仅在构造时赋值一次
}
final变量一旦赋值就不能修改,避免了后续代码(比如误操作、多线程环境)篡改依赖的风险,天然提升线程安全。
而字段注入的变量不能加final(因为@Autowired是Spring在对象构造后,通过反射给字段赋值的)——如果字段是非final,就存在被修改的可能,埋下隐患。
4. 彻底解耦Spring,可测试性极强
构造器注入让CartServiceImpl不依赖任何Spring注解(@Autowired),它本质是一个纯Java类(POJO),依赖通过构造方法“外部传入”,而非由Spring“内部注入”。
这意味着:测试时不需要启动Spring容器,直接手动创建RestTemplate的测试实例(比如Mock对象),传入构造器即可初始化CartServiceImpl,测试效率极高。
示例(用Mockito测试):
// 测试类,无需启动Spring
public class CartServiceImplTest {
@Test
public void testCartOperation() {
// 1. Mock RestTemplate(不依赖Spring容器)
RestTemplate mockRestTemplate = Mockito.mock(RestTemplate.class);
// 2. 手动传入依赖,创建测试对象
CartServiceImpl cartService = new CartServiceImpl(mockRestTemplate);
// 3. 执行测试...
}
}
而字段注入的类,测试时必须依赖Spring容器(或用反射给私有字段赋值),否则restTemplate是null,测试成本高、耦合度高。
5. 避免循环依赖(Spring环境下)
Spring的依赖注入流程中,构造器注入能更早发现循环依赖,而字段注入可能隐藏循环依赖,导致运行时异常。
举个例子:如果A依赖B,B又依赖A:
- 若用构造器注入:Spring创建
A时需要B,创建B时需要A,会直接在启动阶段抛出CircularReferenceException,明确告知循环依赖,便于及时修复; - 若用字段注入:Spring会先创建
A和B的“半成品实例”(无依赖注入的空对象),再通过反射注入字段,启动时不报错,但运行时调用依赖方法时可能出现NullPointerException,排查难度大。
6. 符合“依赖注入(DI)的核心思想”
DI的核心是“依赖反转”:类不主动创建依赖,而是由外部提供依赖,类只关注自身功能,不关注依赖的创建逻辑。
构造器注入正是这种思想的完美体现:依赖通过构造方法“注入”,类对依赖的创建完全无感知;而字段注入是Spring“主动注入”到类的私有字段,类被动接收依赖,耦合了Spring的注入机制,不符合“依赖反转”的纯粹性。
三、补充:@Autowired字段注入的适用场景
不是说@Autowired字段注入完全不能用,它适合:
- 依赖是“可选的”(可加
@Autowired(required = false)); - 类的依赖极多(构造器参数列表会过长,此时可结合
@RequiredArgsConstructor简化构造器注入); - 非核心的辅助类(对可测试性、稳定性要求不高)。
但对于CartServiceImpl这种核心业务类,依赖RestTemplate是必需的,构造器注入是更优选择。
四、简化构造器注入的小技巧
如果类的依赖较多,手动写构造器麻烦,可以用Lombok的@RequiredArgsConstructor注解,自动生成“包含所有final字段”的构造器,简化代码:
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // 自动生成含final字段的构造器
@Service
public class CartServiceImpl {
private final RestTemplate restTemplate; // final触发构造器生成
// 无需手动写构造方法
}
这种方式兼顾了构造器注入的优点,又避免了手动写冗长的构造器,是实际开发中的常用写法。
总结
构造器注入RestTemplate的核心优势是:显式依赖、非空保证、不可变、高可测试性、解耦Spring、避免循环依赖,完全契合依赖注入的设计原则,让代码更健壮、更易维护。
而@Autowired字段注入是一种“便捷但不严谨”的方式,适合简单场景,不推荐用于核心业务类的必需依赖注入。
2.修改handleCartItems业务,使用restTemplate进行远程调用
private void handleCartItems(List<CartVO> vos) {
//1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
//2.1发起请求
String urls = "http://localhost:8081/items?ids={ids}";
//泛型传递
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(urls, HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() {
},Map.of("ids", CollUtil.join(itemIds, ",")));//转为逗号拼接
//2.2解析响应
if(!response.getStatusCode().is2xxSuccessful()){
//查询失败
return;
}
List<ItemDTO> items = response.getBody();
if(CollUtils.isEmpty(items))
return;
// // 1.获取商品id
// Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// // 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// if (CollUtils.isEmpty(items)) {
// return;
// }
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
服务治理
提供者与消费者
- 服务提供者:被其他微服务调用的服务
- 服务消费者:调用其他服务的服务
注册中心原理分析
服务调用出现的问题
restTemplate使用的是硬编码方式,就是说url是固定的,业务众多,不方便。
所以如何获取提供者的地址信息?如果有多个服务提供者,消费者该如何选择,如何知道节点的健康状态。
服务提供者启动时像eureka注册自己的信息,eureka保存这些信息,消费者拉取提供者信息。
服务会向server发心跳,提供健康信息。

Nacos注册中心
windows注册
1.https://nacos.io/download/release-history/?spm=5238cd80.47ee59c.0.0.189fcd36q9eYnz 下载nacos安装包
2.配置mysql
将nacos.sql导入mysql
修改nacos/conf/application.properties,添加MYSQL连接配置
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456
3.启动nacos单机模式
3.1修改启动将本,打开nacos/bin目录,编辑startup.cmd文件,找到以下代码行
set MODE="cluster"
#修改为单机模式
set MODE="standalone"
3.2启动nacos
cd D:\nacos\bin
startup.cmd -m standalone # -m standalone 显式指定单机模式

浏览器访问http://localhost:8848/nacos
默认账号/密码:nacos/nacos
服务注册
1.引入依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.配置nacos地址
spring:
application:
name: # 服务名称
cloud:
nacos:
server-addr: # nacos地址
3.启动服务实例

复制配置后修改端口
可以在nacos控制台中看到两个实例信息

服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现。
1.引入依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
这个以来同时包含注册和发现的功能。
2.配置Nacos地址
3.发现并调用服务
服务调用者利用负载均衡的算法,从多个实例中挑选一个去访问。常见的有随机,轮询,最近最少访问等
我们需要使用discoveryClient,可以直接注入
private final DiscoveryClient discoveryClient;//cartserviceimpl中
private void handleCartItems(List<CartVO> vos) {
//1.获取商品id
Set<Long> ids = vos.stream().map(CartVO::getId).collect(Collectors.toSet());
//2.1 获取消费者实例表
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("item-service");
//2.2 负载均衡 挑选实例
ServiceInstance serviceInstance = serviceInstances.get(RandomUtil.randomInt(serviceInstances.size()));
String url = serviceInstance.getUri()+"/items?ids={ids}";
//2.3 发起请求
ResponseEntity<List<ItemDTO>> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
Map.of("ids",CollUtil.join(ids,",")));
//2.4 解析响应
if(!responseEntity.getStatusCode().is2xxSuccessful()){
return;
}
List<ItemDTO> itemDTOS = responseEntity.getBody();
if(CollUtils.isEmpty(itemDTOS)){
return;
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = itemDTOS.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
OpenFeign
使用nacos实现服务的治理,利用restTemplate进行远程调用。但是代码太复杂,且与本地方法的调用差异较大,我们得让远程调用像本地调用方法一样简单。
因此引入openFeign,其实远程调用关键点就在于四个:
- 请求方式
- 请求路径
- 请求参数
- 返回值
OF就是利用相关注解来声明上述参数,然后基于动态代理来生成远程调用的代码,无需再手动编写。
快速入门
1.引入依赖
<!--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>
2.启用openFeign
启动类上添加注解@EnableFeignClients,启动OF功能
3.编写openFeign客户端
.在cart-service中,定义一个新接口,编写feign客户端
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")//声明服务名称
public interface ItemClient {
//此处只需声明接口 无需实现
@GetMapping("/items")//请求方式与路径
List<ItemDTO> queryItemsByIds(@RequestParam("ids") Collection<Long> ids);
}
4.使用
直接调用上述方法即可
List<ItemDTO> itemDTOS = itemClient.queryItemsByIds(ids);
记得注入feignClient
private final ItemClient itemClient;
连接池
feign底层发起http请求,依赖于其他框架。其底层支持的http客户端包括:
httpurlConnection:默认实现,不支持连接池
HttpClient:支持连接池
OKHttp:支持连接池
1.引入依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2.开启连接池
feign:
okhttp:
enabled: true
最佳实践
我们会发现很多微服务都会使用同样的模块函数,那么如果每个服务都进行这样地调用会非常复杂,因此我们需要将重复的部分进行抽取。有两种抽取思路:抽取到微服务之外的公共module;每个微服务自己抽取一个module

但是我们采用第一种方式
1.创建hm-api模块
a.依赖为
<?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>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<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>
b.把itemDto和itemClient copy过来
c.然后现在想要调用外部服务接口,直接引入hm-api模块就可以了
2.在cart中引入模块
a.添加依赖
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
b.启动类上声明扫描包
@EnableFeignClients(basePackages = "com.hmall.api.client")
日志输出
OF只会在日志级别为DEBUG时,才会输出日志。
1.定义日志级别
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;
}
}
2.配置
-
局部生效 放在feignClient中
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class) -
全局生效 放在启动类上
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
网关路由
认识网关
网关就是网络的端口,数据在网络间传输,从一个网络传输到另一个网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
网关可以做安全控制,也就是登录身份校验,通过才放行;通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去

快速入门
1.创建项目hm-gateway
2.引入依赖
<?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>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<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>
3.启动类
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);
}
}
4.配置路由
a.新建application.yml文件
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848 # 自己的nacos地址
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
测试端口localhost:8080/items/page?pageNo=1&pageSize=1
应该可以正常获取内容
路由过滤
网关登录校验
鉴权思路分析
我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到密钥。如果每个微服务都去做校验就存在两大问题:
- 每个微服务都需要知道JWT的密钥,不安全
- 每个微服务重复编写登录校验代码、权限校验代码
既然网关是所有微服务的入口,一切请求都需要经过网关。我们可以把登录校验的工作放到网关中,
- 只需要在网关和用户服务保存密钥
- 只需要在网关开发登录校验功能
校验流程

不过会有以下几个问题:
- 网关路由是配置的,请求转发是gateway内部代码,如何在转发之前做登录校验?
- 网关校验JWT之后,如何将用户信息传递给微服务?
- 微服务之间也会相互调用,这种调用不经过网关,该如何传递用户信息?
网关过滤器
登录校验必须在请求转发在微服务之前做。而网关的请求转发是gateway内部代码实现的,要想在请求转发之前做登录校验,需了解gateway原理。

1.客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则,然后将请求交给webHandler去处理。
2.webHandler加载当前路由下需要执行的过滤器链,然后按照顺序逐一执行过滤器。
3.filter逻辑分为pre和post两部分,分别在请求路由到微服务之前和之后被执行。
4.只有filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
5.微服务返回结果后,再倒序执行filter的post逻辑。
6.最终把响应结果返回。
请求转发是由NettyRoutingFilter执行的。如果我们能定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,就可以了。
实现网关过滤器,我们选择的是gatewayFilter。
使用的方式就是再application.yaml中进行配置
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
如果想让过滤器作用于所有的路由
spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
自定义过滤器
gatewayFilter
固定后缀名,且需要自定义动态配置
自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
注意:该类的名称一定要以GatewayFilterFactory为后缀!
然后在yaml配置中这样使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
然后在yaml文件中使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
globalFilter
不可动态配置
package com.hmall.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
//模拟登录逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println(headers);
//放行
return chain.filter(exchange);
}
@Override
public int getOrder(){ //确定优先级 因为我们要再netty上面执行 所有设置比它小的
return -2;
}
}
登录校验
密钥工厂生成密钥,把他们都复制过来就会根据jwt生成密钥
AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置SecurityConfig:工具的自动装配JwtTool:JWT工具,其中包含了校验和解析token的功能hmall.jks:秘钥文件
其中AuthProperties和JWTProperties所需的属性要再application.yaml中配置
登录校验过滤器
package com.hmall.gateway.filters;
import com.hmall.common.exception.UnauthorizedException;
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.GatewayFilterChain;
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;
import java.util.List;
@Component //注册为组件
@RequiredArgsConstructor //自动装配
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); //没有注册为spring的bean 所以需要自己new出来
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
//1.获取request
ServerHttpRequest request = exchange.getRequest();
if(isExclude(request.getPath().toString()))
{
//无需拦截 直接放行
return chain.filter(exchange);
}
//2.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
//3.校验并解析token
if(headers != null && !headers.isEmpty())//不为空 获取token
{
token = headers.get(0);
}
Long userId = null;
try {
userId= jwtTool.parseToken(token);
}
catch (UnauthorizedException e){
//无效 进行拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
//4. TODO 如果有效,传递用户信息
System.out.println("userId:"+userId);
//5.放行
return chain.filter(exchange);
}
private boolean isExclude(String antPath){
for(String pathPattern : authProperties.getExcludePaths()) //看是否和排除之外的匹配 匹配则返回true
{
if(antPathMatcher.match(pathPattern, antPath))
{
return true;
}
}
return false;
}
@Override
public int getOrder(){
return -2;
}
}
微服务获取用户
现在,网关已经可以完成登录校验并或者去登录用户身份信息,但是当网关将请求转发给微服务时,如何获取呢?
由于网关请求到微服务依然采用的是http请求,因此可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。我们可以利用SpringMVC的拦截器来实现登录用户信息获取,用存入ThreadLocal,方便后序使用

- 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
- 编写微服务拦截器,拦截请求获取用户信息,保存到threadLocal放行
保存用户到请求头
// 5.如果有效,传递用户信息
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate()
.request(b->b.header("user-info",userInfo)).build();
System.out.println("userId = " + userId);
拦截器获取用户
我们直接在hm-common中编写拦截器,并写好自动装配,这样其他的微服务只需要引入hm-common就可以直接具备拦截器功能。
编写拦截器
package com.hmall.common.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import com.hmall.common.utils.UserContext;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
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) throws Exception {
//1.获取请求头
String userInfo = request.getHeader("user-info");
//2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
//不为空 保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
//3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
//移除用户
UserContext.removeUser();
}
}
编写配置类,配置登录拦截器
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)
public class SpringMVC implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
添加到spring工厂中

购物车代码
直接把UserContext.getUser()恢复就好,现在是可用的了
OpenFeign传递用户
但是刚刚我们只是实现了网关的拦截器配置,很多时候是微服务之间的调用,那么我们必须在微服务发起调用之前将用户信息存入请求头。
OF提供了一个拦截器接口,我们可以直接使用
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}
这个问题核心是 “微服务调用的路由路径不同” —— 网关是「外部请求进入集群的统一入口」,而微服务间调用是「集群内部直接通信」,所以拦截器的生效范围和实现方式才会不一样。下面用通俗的方式拆解清楚:
一、先明确两个核心场景的调用链路
场景1:外部请求 → 网关 → 微服务(比如前端调用后端)
调用路径:浏览器/APP → 网关(Gateway) → 目标微服务(如订单服务)
- 网关是「流量入口」,所有外部请求必须经过它(相当于小区的大门,外人只能走大门进);
- 网关的拦截器(如Spring Cloud Gateway的GlobalFilter)会拦截所有经过网关的请求,所以能在这里统一处理用户登录校验、token解析、添加用户信息到上下文(UserContext)等操作。
场景2:微服务A → 微服务B(内部调用,比如订单服务调用库存服务)
调用路径:订单服务(Feign客户端) → 库存服务(直接HTTP调用)
- 微服务间调用是「内部直接通信」,不会走网关(相当于小区里的住户互相串门,不用再走小区大门);
- 因为跳过了网关,网关的拦截器自然不会生效——所以无法通过网关给内部调用添加用户信息、token等上下文。
二、为什么微服务间调用不走网关?
不是“不能走”,而是“没必要走”,甚至“不建议走”,原因有3点:
- 性能损耗:网关会做路由转发、过滤、校验等操作,内部调用走网关会多一层网络跳转和处理,降低性能;
- 耦合风险:如果内部调用依赖网关,网关挂了会导致整个集群的内部通信瘫痪(大门坏了,住户也不能互相串门了,不合理);
- 职责边界:网关的核心职责是「对外统一入口、限流、鉴权、跨域」,而内部服务间的通信是「集群内部协作」,应该直接点对点调用,更高效灵活。
实际项目中,只有外部请求(前端、第三方系统)才会走网关,微服务间调用都是直接通过服务发现(如Nacos/Eureka)找到目标服务的地址,直接发起HTTP请求。
三、为什么内部调用需要用Feign(OpenFeign)实现拦截器?
因为内部调用跳过了网关,网关的拦截器失效了,但我们又需要传递一些上下文信息(比如当前登录用户ID、token、链路追踪ID等),所以必须在「内部调用的发起端」做拦截——而Feign正是微服务间调用的核心工具(封装了HTTP调用,集成了服务发现),所以Feign的拦截器(RequestInterceptor)是最适合的选择。
简单说:
- 网关拦截器:处理「外部→内部」的请求,在入口处统一加上下文;
- Feign拦截器:处理「内部→内部」的请求,在调用发起时补全上下文;
- 两者是「互补关系」,共同保证整个系统的上下文传递(比如用户信息从前端一路传到最下游的微服务)。
四、举个完整例子,看上下文如何传递
假设流程:前端(带token)→ 网关 → 订单服务 → Feign调用 → 库存服务
- 前端发起请求,请求头携带用户token;
- 网关拦截器生效:解析token得到用户ID,存入
UserContext(线程局部变量),然后转发请求到订单服务; - 订单服务处理业务时,需要调用库存服务(内部调用);
- Feign拦截器生效:从
UserContext中取出用户ID,添加到Feign请求的user-info头中; - 库存服务收到请求后,从
user-info头中获取用户ID,完成权限校验、数据隔离等操作。
这里Feign拦截器的作用,就是把网关已经解析好的用户信息,“接力”传递给下一个微服务——因为内部调用没走网关,必须靠Feign来补这个“传递”的动作。
五、补充:有没有可能让内部调用走网关?
可以,但不推荐,除非有特殊需求(比如内部调用也需要统一限流、日志记录)。如果要走,配置如下:
- Feign调用时,不直接调用目标服务的地址,而是调用网关的地址,再由网关路由到目标服务;
- 比如:Feign请求
http://网关地址/库存服务/接口,而不是http://库存服务/接口。
但这样会带来两个问题:
- 性能下降:多了一次网关转发;
- 网关压力增大:内部调用通常很频繁,会让网关成为瓶颈。
所以实际项目中,内部调用优先走“服务间直接调用+Feign拦截器”,只有外部请求走网关。
总结
| 调用场景 | 是否走网关 | 拦截器实现方式 | 核心作用 |
|---|---|---|---|
| 外部→微服务(前端→后端) | 是 | 网关拦截器(GlobalFilter) | 统一鉴权、解析上下文、路由转发 |
| 微服务→微服务(内部调用) | 否 | Feign拦截器(RequestInterceptor) | 传递上下文(用户ID、链路ID) |
核心逻辑:网关是外部入口,内部调用绕开网关以提高效率,Feign拦截器补全上下文传递,两者配合保证系统的一致性。
配置管理

微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。
网关的路由同样是配置,因此可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。
配置共享
可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
- 在nacos中添加共享配置
- 微服务拉取配置
添加共享配置
1.jdbc 2.日志 3.swagger和openfeign

拉取共享配置
读取nacos配置是springcloud上下文初始化时处理的,发生在项目的引导阶段。然后才会初始化springboot上下文,去读取application.yaml。也就是说引导阶段,yml文件尚未读取,不知道nacos地址。**也就是说在读nacos配置发生在读yml文件前,而只有yml文件中有nacos地址,因此无法成立,所以我们需要把nacos地址配置在bootstrap.yaml中,因为这个是在上下文初始化之前的,我们就可以获取到地址啦。**这是需要使用bootstrap.yaml文件的原因 ,实现Nacos服务注册的时候不需要bootstrap.yaml文件的原因是因为不需要读取Nacos中的配置文件,只需要在项目启动后注册服务和拉取服务就行了

1.引入依赖
<!--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
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
3.修改application.yaml
server:
port: 8082
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持
hm:
swagger:
title: 购物车服务接口文档
package: com.hmall.cart.controller
db:
database: hm-cart
springCloud和springBoot的关系
- 关系:Spring Boot 是 “基础容器”,Spring Cloud 是 “基于 Spring Boot 的微服务套件”,依赖 Spring Boot 才能运行;
- 加载顺序:不是 “Spring Cloud 早于 Spring Boot”,而是 “Spring Cloud 的 Bootstrap Context(引导上下文)早于 Spring Boot 的 Application Context(主应用上下文)”;
- 核心原因:Spring Cloud 的微服务配置(如注册中心地址、统一配置)需要在 Spring Boot 主应用启动前准备好,否则主应用无法完成微服务相关的初始化(如服务注册)。
简单说:Spring Boot 负责 “跑起来”,Spring Cloud 负责 “协同好”;要协同,先配置,所以引导上下文先启动。
配置热更新
1.在Nacos中添加配置
添加配置
hm:
cart:
maxAmount: 1 # 购物车商品数量上限
2.在微服务读取配置
2.1新建一个Cartproperties读取类
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;
}
2.2业务代码中使用该属性加载类获取购物车商品最大值
if (count >= cartProperties.getMaxAmount()) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxAmount()));
动态路由
网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。监听配置变更+更新路由表
监听Nacos配置变更
可以使用Nacos动态监听配置接口实现
核心是两步:1.创建configService,目的是连接到Nacos (已有,NacosConfigManager中间创建了ConfigService)2.添加配置监听器,编写配置变更的通知处理逻辑(getConfigAndSignListener,可以配置监听器,并且会根据dataId和group读取配置并返回。我们可以在项目启动时先更新依次路由,后序随着配置变更通知到监听器,完成路由更新)
更新路由
更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口
实现动态路由
1.gateway引入依赖
<!--统一配置管理-->
<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.gateway创建bootstrap.yaml
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml
shared-configs:
- dataId: shared-log.yaml # 共享日志配置
3.修改之前的application.yml
把之前的路由删掉
server:
port: 8080 # 端口
hm:
jwt:
location: classpath:hmall.jks # 秘钥地址
alias: hmall # 秘钥别名
password: hmall123 # 秘钥文件密码
tokenTTL: 30m # 登录有效期
auth:
excludePaths: # 无需登录校验的路径
- /search/**
- /users/login
- /items/**
4.gateway中定义配置监听器
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 {
private final RouteDefinitionWriter routeDefinitionWriter;
private final NacosConfigManager nacosConfigManager;
//路由配置文件的id和分组
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.注册监听器并首次拉取配置
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("update config info:{}", configInfo);
//1.反序列化
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
//2.更新前先清空旧路由
//2.1清除旧路由
for(String routeId : routeIds) {
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
//2.2判断是否有新的路由要更新
if(CollUtils.isEmpty(routeDefinitions)) {
return;
}
//3.更新
routeDefinitions.forEach(routeDefinition -> {
//3.1更新路由
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
//3.2记录路由id 方便将来删除
routeIds.add(routeDefinition.getId());
});
}
}
5.Nacos控制台添加路由
添加新配置 gateway-routes.json
[
{
"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"
}
]
或许感兴趣的同学可以用nacos和lion+zookeeper对比以下,有很多企业都是使用这套中间件进行服务和配置管理的~
更多推荐
所有评论(0)