Spring Cloud Gateway 学习笔记
版本说明:本文基于 Spring Cloud 2023.x / Spring Boot 3.x / JDK 17 核心依赖:
spring-cloud-starter-gateway(基于 Spring 5 WebFlux + Reactor Netty)
1. API 网关概述
⭐ 1.1 什么是 API 网关
API 网关是微服务架构的统一入口,所有外部请求都先经过网关,再由网关路由到后端服务。它是微服务架构中不可或缺的基础设施,承担着「门卫」和「调度中心」的双重角色。
客户端 (Browser/App)
│
▼
┌─────────────────────────────────────────┐
│ API Gateway (统一入口) │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 认证授权 │ │ 限流熔断 │ │ 日志监控 │ │
│ └─────────┘ └──────────┘ └─────────┘ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 路由转发 │ │ 负载均衡 │ │ 协议转换 │ │
│ └─────────┘ └──────────┘ └─────────┘ │
└─────────┬───────────┬───────────┬──────┘
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 用户服务│ │ 订单服务│ │ 商品服务│
└────────┘ └────────┘ └────────┘1.2 网关核心功能一览表
| 功能 | 说明 | 重要程度 |
|---|---|---|
| 路由转发 | 根据规则将请求转发到后端微服务 | ⭐⭐⭐ |
| 负载均衡 | 从多个服务实例中选择一个处理请求 | ⭐⭐⭐ |
| 认证授权 | 统一校验 Token、权限控制 | ⭐⭐⭐ |
| 限流熔断 | 保护后端服务不被压垮 | ⭐⭐⭐ |
| 统一跨域 | CORS 配置,前端无需单独处理 | ⭐⭐ |
| 日志监控 | 请求/响应统一记录 | ⭐⭐ |
| 协议转换 | HTTP ↔ gRPC 等 | ⭐ |
| 响应体修改 | 统一包装/脱敏返回结果 | ⭐⭐ |
💡 1.3 Gateway vs Zuul 对比
| 对比项 | Spring Cloud Gateway | Netflix Zuul |
|---|---|---|
| 基础框架 | Spring 5 WebFlux(响应式) | Servlet 2.5(阻塞) |
| 模型 | 异步非阻塞 | 同步阻塞 |
| 性能 | 高(支持高并发) | 一般(线程模型限制) |
| 维护状态 | 官方主推(Zuul 2 仅补丁维护) | 不再开发新功能 |
| 编程模型 | 函数式、Mono/Flux | 传统 Servlet Filter |
⚠️ 结论:新项目必须用 Gateway,Zuul 已停止新功能开发。
2. 快速入门
⚠️ 2.1 环境要求与依赖
⚠️ 关键警告:Gateway 不能引入 spring-boot-starter-web! Gateway 基于 WebFlux(响应式),与传统 Servlet 容器不兼容,引入 starter-web 会启动报错。
pom.xml 核心依赖:
<!-- Spring Cloud Gateway:这是唯一必需的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡(Spring Cloud 2020+ 默认使用 LoadBalancer 替代 Ribbon)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 注册中心客户端(任选 Nacos / Eureka / Consul)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 如果你要做 Redis 限流,才需要这个 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>2.2 启动类
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* API 网关启动类
* 注意:无需 @EnableDiscoveryClient,注册中心依赖自动装配
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}🚩 2.3 最小可运行示例 application.yml
server:
port: 8080 # 网关端口,客户端统一访问这个端口
spring:
application:
name: api-gateway # 注册到 Nacos 的服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 地址
gateway:
# 核心:路由规则列表
routes:
# 路由1:用户服务
- id: user-service-route # 路由唯一标识(字符串,随便取但不能重复)
uri: lb://user-service # lb:// = LoadBalancer,从注册中心找 user-service
predicates: # 匹配条件(全部满足才转发)
- Path=/api/users/** # 请求路径匹配,/api/users/xxx 都会匹配
filters: # 过滤器(可选)
- StripPrefix=1 # 去掉第1段路径:/api/users/123 → /users/123
# 路由2:订单服务
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
# 路由3:静态 URI(直接转发到固定地址,不走注册中心)
- id: baidu-route
uri: https://www.baidu.com
predicates:
- Path=/baidu/**启动后测试:
# 访问网关 → 网关路由到用户服务
curl http://localhost:8080/api/users/1
# 验证路由是否生效(Spring Cloud Gateway Actuator)
curl http://localhost:8080/actuator/gateway/routes3. 核心概念 ⭐
Gateway 的核心由三个术语组成:Route(路由)、Predicate(谓词)、Filter(过滤器)。
3.1 Route(路由)—— 网关的基本构建块
一个 Route 就是一条路由规则,由以下要素组成:
| 属性 | 说明 | 必填 |
|---|---|---|
| id | 路由唯一标识 | ✅ |
| uri | 转发目标(lb://服务名 或 http://ip:port) | ✅ |
| predicates | 匹配条件,全部满足才转发 | ✅(至少一个) |
| filters | 过滤器,请求/响应前后处理 | ❌ |
| order | 优先级,数字越小优先级越高(默认 0) | ❌ |
# 路由 = id + uri + predicates + filters + order
- id: my-route
uri: lb://target-service
predicates: [...]
filters: [...]
order: -1 # 优先级高于默认值 03.2 Predicate(谓词)—— 路由匹配条件
Predicate 就是 Java 8 的 java.util.function.Predicate<ServerWebExchange>,根据 HTTP 请求(路径、方法、头、参数等)判断是否匹配该路由。多个 Predicate 之间是 AND 关系,必须全部满足。
3.3 Filter(过滤器)—— 请求/响应处理链
过滤器分两种:
| 类型 | 作用范围 | 生命周期 |
|---|---|---|
| GatewayFilter | 仅对某条路由生效 | 路由级别 |
| GlobalFilter | 对所有路由生效 | 全局级别 |
执行链路:
请求 → GlobalFilter(pre) → GatewayFilter(pre) → 代理请求 → GatewayFilter(post) → GlobalFilter(post) → 响应4. 路由配置详解
4.1 YAML 配置方式(推荐)
YAML 是最常用的配置方式,直观易读,支持动态刷新(配合 Nacos Config)。
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=14.2 Java 代码配置方式
适合需要写动态逻辑的场景:
package com.example.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Java 代码方式定义路由
* 与 YAML 方式等效,可以共存
*/
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 路由1:用户服务
.route("user-service", r -> r
.path("/api/users/**") // 路径匹配
.filters(f -> f
.stripPrefix(1) // 去掉 /api
.addRequestHeader("X-Source", "gateway"))
.uri("lb://user-service"))
// 路由2:只允许 GET 方法访问的订单服务
.route("order-service-get-only", r -> r
.path("/api/orders/**")
.and()
.method("GET") // 路径 + GET 方法同时满足
.uri("lb://order-service"))
// 路由3:带权重的分流(金丝雀发布)
.route("weighted-route", r -> r
.path("/api/products/**")
.weight("product-group", 90) // 90% 流量
.uri("lb://product-service"))
.build();
}
}🚩 4.3 内置谓词大全(11 种)
① Path —— 路径匹配(最常用)
predicates:
# 精确匹配一个路径
- Path=/api/users
# 通配符匹配一个前缀下的所有子路径
- Path=/api/users/**
# 多个路径任选其一
- Path=/api/users/**,/api/public/**② Method —— HTTP 方法匹配
predicates:
# 只允许 GET 请求
- Method=GET
# 允许多个方法
- Method=GET,POST,PUT,DELETE③ Header —— 请求头匹配(支持正则)
predicates:
# 请求头名为 X-Request-Id 且值匹配正则 \d+(纯数字)
- Header=X-Request-Id, \d+
# 请求头存在即可
- Header=X-Api-Version④ Query —— 查询参数匹配
predicates:
# URL 中存在 token 参数即可
- Query=token
# token 参数值匹配正则
- Query=token, abc_⑤ Cookie —— Cookie 匹配
predicates:
# cookie 名为 session,值匹配正则
- Cookie=session, .+⑥ Host —— Host 头匹配
predicates:
# 二级域名任意子域名都匹配
- Host=**.example.com
# 具体子域名
- Host=api.example.com⑦ RemoteAddr —— 远程 IP 匹配
predicates:
# CIDR 表示法:IP段 / 子网掩码位数
- RemoteAddr=192.168.1.1/24
- 单个 IP
- RemoteAddr=10.0.0.1⑧ After / Before / Between —— 时间匹配
predicates:
# 只匹配某个时间之后的请求(时区 Asia/Shanghai)
- After=2024-01-01T00:00:00+08:00[Asia/Shanghai]
# 某个时间之前
- Before=2025-12-31T23:59:59+08:00[Asia/Shanghai]
# 两个时间之间
- Between=2024-01-01T00:00:00+08:00[Asia/Shanghai],2024-12-31T23:59:59+08:00[Asia/Shanghai]⑨ Weight —— 权重分流(金丝雀发布)
predicates:
# group1 组,权重 90 = 90% 流量到此路由
- Weight=group1, 90
# 同一 group 的其他路由权重为 10,合为 100⑩ CloudFoundryRouteService —— 专用(跳过)
⑪ XForwardedRemoteAddr —— 代理后 IP
predicates:
# 识别 X-Forwarded-For 头中的 IP
- XForwardedRemoteAddr=10.0.0.0/84.4 组合谓词
多个谓词之间默认是 AND 关系,全部满足才转发。用 Java 代码可实现更复杂的逻辑:
predicates:
# 路径是 /api/users/** 并且方法是 GET
- Path=/api/users/**
- Method=GET5. 过滤器详解
🚩 5.1 内置 GatewayFilter(20+ 种)
路径处理类
filters:
# StripPrefix:去掉前 N 段路径
# 请求 /api/users/123 → 后端收到 /users/123
- StripPrefix=1
# PrefixPath:给后端路径加前缀
# 请求 /users/123 → 后端收到 /api/users/123
- PrefixPath=/api
# SetPath:重写整个路径(使用占位符提取)
# 请求 /api/users/123 → /users/123 (把 /api/ 去掉)
- SetPath=/{segment}
# 配合 predicate:Path=/api/{segment}
# RewritePath:正则替换(最灵活)
# 请求 /api/users/123 → /users/123
- RewritePath=/api/(?<segment>.*), /$\{segment}
# ⚠️ YAML 中 $ 要转义为 $\{segment} 或用 $${segment}请求头/参数类
filters:
# 添加请求头传给后端
- AddRequestHeader=X-Request-Id, 12345
# 从请求属性取值(动态)
- AddRequestHeader=X-Trace-Id, #{traceId}
# 添加请求参数
- AddRequestParameter=source, gateway
# 修改请求头
- SetRequestHeader=Content-Type, application/json
# 去掉请求头
- RemoveRequestHeader=X-Internal-Only响应头类
filters:
# 给客户端的响应加头
- AddResponseHeader=X-Response-Time, 30ms
- SetResponseHeader=Content-Type, application/json
- RemoveResponseHeader=X-Powered-By状态码/重定向类
filters:
# 直接设置响应状态码并返回
- SetStatus=401
# 从响应头取值作为状态码
- SetStatus=fromResponseStatus
# 临时重定向
- Redirect=302, https://example.com/new-url限流熔断类
filters:
# 基于 Redis 的令牌桶限流
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒恢复 token 数
redis-rate-limiter.burstCapacity: 20 # 总 token 容量
redis-rate-limiter.requestedTokens: 1 # 每个请求消耗 token 数
key-resolver: "#{@ipKeyResolver}" # 按 IP 限流
# 熔断
- name: CircuitBreaker
args:
name: userCircuitBreaker
fallbackUri: forward:/fallback/users其他实用过滤器
filters:
# 保存原始 Host 头(防止网关 Host 覆盖)
- PreserveHostHeader
# 每请求超时
- name: RequestTimeout
args:
time: 5s
unit: seconds🚩 5.2 GlobalFilter 全局过滤器实战
GlobalFilter 对所有路由生效,适合做认证、日志、TraceId 透传等全局逻辑。
实战 1:TraceId 透传 + 请求日志
package com.example.gateway.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.UUID;
/**
* 全局过滤器:TraceId 透传 + 请求日志
* 优先级 -100,确保在认证等过滤器之前执行
*/
@Component
public class TraceLogFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(TraceLogFilter.class);
private static final String TRACE_ID_HEADER = "X-Trace-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
// 1. 获取或生成 TraceId(从上游请求头取,或新建)
String traceId = request.getHeaders().getFirst(TRACE_ID_HEADER);
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 2. 把 TraceId 写入日志上下文(MDC)+ 传给下游服务
String finalTraceId = traceId;
ServerHttpRequest mutatedRequest = request.mutate()
.header(TRACE_ID_HEADER, finalTraceId)
.build();
// 3. 请求前日志
log.info("[{}] → {} {} | IP: {}",
finalTraceId,
request.getMethod(),
request.getURI().getPath(),
request.getRemoteAddress());
// 4. 执行请求链
return chain.filter(exchange.mutate().request(mutatedRequest).build())
.then(Mono.fromRunnable(() -> {
// 5. 响应后日志
long duration = System.currentTimeMillis() - startTime;
int status = exchange.getResponse().getStatusCode() != null
? exchange.getResponse().getStatusCode().value()
: 0;
log.info("[{}] ← {} | Status: {} | {}ms",
finalTraceId,
request.getURI().getPath(),
status,
duration);
}));
}
@Override
public int getOrder() {
// 数字越小越先执行
// -200:最前(早于认证)
return -200;
}
}实战 2:JWT 认证过滤器
package com.example.gateway.filter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* JWT 认证全局过滤器
* - 白名单路径直接放行
* - 其他路径必须携带有效 Bearer Token
* - 解析出用户信息透传给下游服务
*/
@Component
public class JwtAuthFilter implements GlobalFilter, Ordered {
private static final String AUTH_HEADER = "Authorization";
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USERNAME_HEADER = "X-Username";
private static final String ROLE_HEADER = "X-Role";
@Value("${jwt.secret:your-256-bit-secret-key-here-for-jwt-signing}")
private String jwtSecret;
/** 认证白名单(这些路径不需要 Token)*/
private static final List<String> WHITELIST = List.of(
"/api/auth/login",
"/api/auth/register",
"/api/public/",
"/actuator/"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 1. 白名单直接放行
if (isWhitelisted(path)) {
return chain.filter(exchange);
}
// 2. 提取 Token
String token = extractToken(exchange.getRequest());
if (token == null) {
return unauthorized(exchange, "Missing token");
}
// 3. 验证并解析 Token
Claims claims;
try {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (Exception e) {
return unauthorized(exchange, "Invalid or expired token");
}
// 4. 把用户信息透传给下游服务
String userId = claims.getSubject();
String username = claims.get("username", String.class);
String role = claims.get("role", String.class);
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header(USER_ID_HEADER, userId)
.header(USERNAME_HEADER, username != null ? username : "")
.header(ROLE_HEADER, role != null ? role : "USER")
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}
private boolean isWhitelisted(String path) {
return WHITELIST.stream().anyMatch(path::startsWith);
}
private String extractToken(ServerHttpRequest request) {
String header = request.getHeaders().getFirst(AUTH_HEADER);
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":401,\"message\":\"" + msg + "\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
// -100:在 TraceLogFilter(-200) 之后执行
return -100;
}
}5.3 自定义 GatewayFilterFactory
当你需要在 YAML 里像内置过滤器一样使用自己的过滤器时,继承 AbstractGatewayFilterFactory:
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 自定义网关过滤器工厂
* 用法:filters: - name: CheckSign args: key=xxx, expire=300
*/
@Component
public class CheckSignGatewayFilterFactory
extends AbstractGatewayFilterFactory<CheckSignGatewayFilterFactory.Config> {
public CheckSignGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String sign = exchange.getRequest().getQueryParams().getFirst("sign");
String timestamp = exchange.getRequest().getQueryParams().getFirst("timestamp");
// 简单的签名校验示例(实际请用 HMAC 等安全算法)
if (sign == null || timestamp == null) {
exchange.getResponse().setStatusCode(org.springframework.http.HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
};
}
// 配置类:YAML 中 args 的值映射到这里的字段
public static class Config {
private String key;
private long expire = 300; // 默认 300 秒
// getter/setter 必须有!
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public long getExpire() { return expire; }
public void setExpire(long expire) { this.expire = expire; }
}
// YAML 里 args 的字段顺序(可选,决定是否支持位置参数写法)
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("key", "expire");
}
}使用方式(与内置过滤器完全一致):
spring:
cloud:
gateway:
routes:
- id: api-route
uri: lb://user-service
predicates:
- Path=/api/**
filters:
# 命名参数
- name: CheckSign
args:
key: my-secret-key
expire: 300
# 或者用位置参数(因为我们定义了 shortcutFieldOrder)
- CheckSign=my-secret-key,3006. 限流策略 💡
6.1 限流算法概述
| 算法 | 原理 | 适用场景 |
|---|---|---|
| 计数器 | 固定时间窗口内计数 | 粗略限流 |
| 滑动窗口 | 时间窗口滑动,精度更高 | 一般限流 |
| 漏桶 | 固定速率处理请求,突发流量整形 | 流量整形 |
| 令牌桶 | 固定速率生成 token,请求消耗 token | Gateway 默认用这个 |
Gateway 的 RequestRateLimiter 底层基于 Redis 令牌桶算法。
6.2 Redis 分布式限流(实战)
<!-- 必须加:Redis Reactive 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>spring:
redis:
host: localhost
port: 6379
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
# ↓↓↓ 限流过滤器(使用 SpEL 引用下面定义的 Bean)
- name: RequestRateLimiter
args:
# 每秒补充多少 token(= QPS 平均值)
redis-rate-limiter.replenishRate: 10
# 令牌桶最大容量(= 允许突发的最大 QPS)
redis-rate-limiter.burstCapacity: 30
# 每个请求消耗多少 token(默认 1)
redis-rate-limiter.requestedTokens: 1
# Key 解析器:按什么维度限流
key-resolver: "#{@ipKeyResolver}"6.3 自定义 KeyResolver(限流维度)
package com.example.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 限流维度配置
* 根据业务需求选择按哪个维度做限流
*/
@Configuration
public class RateLimiterConfig {
/** 按客户端 IP 限流(最常用) */
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest()
.getRemoteAddress()
.getAddress()
.getHostAddress()
);
}
/** 按用户 ID 限流(从请求头 X-User-Id 取) */
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id");
return Mono.just(userId != null ? userId : "anonymous");
};
}
/** 按 API 路径限流(保护具体接口) */
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getURI().getPath()
);
}
/** 全局默认(不区分用户,所有请求共享一个桶) */
@Bean
public KeyResolver defaultKeyResolver() {
return exchange -> Mono.just("global");
}
}💡 SpEL 语法:
#{@ipKeyResolver}表示从 Spring 容器中找到名为ipKeyResolver的 Bean。
7. 熔断降级 💡
7.1 熔断器原理(三态转换)
正常(Closed)───错误率≥50%───► 熔断(Open)
▲ │
│ 等待 10 秒后
│ ▼
└────半开(Half-Open)◄──── 放一个请求探测
│
├─ 成功 → 恢复 Closed
└─ 失败 → 回到 Open| 状态 | 含义 | 行为 |
|---|---|---|
| Closed | 正常(熔断器关闭) | 请求正常透传给后端服务 |
| Open | 熔断(熔断器打开) | 请求直接走降级逻辑,不访问后端 |
| Half-Open | 半开(探测恢复) | 只放一个请求过去测试 |
7.2 Resilience4j 集成(Spring Cloud 官方推荐)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>🚩 7.3 完整熔断配置
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
# 熔断器名称(对应 resilience4j.instances 下的配置名)
name: userCircuitBreaker
# 降级跳转:forward 表示网关内部转发到 FallbackController
fallbackUri: forward:/fallback/users
# Resilience4j 熔断器配置
resilience4j:
circuitbreaker:
configs:
# 默认配置(所有实例共享)
default:
slidingWindowSize: 20 # 滑动窗口大小(请求数)
slidingWindowType: COUNT_BASED # COUNT_BASED / TIME_BASED
failureRateThreshold: 50 # 失败率阈值(%),超过即熔断
slowCallDurationThreshold: 3s # 慢调用定义(>3秒)
slowCallRateThreshold: 30 # 慢调用比例阈值(%)
waitDurationInOpenState: 10s # Open 状态等待多久后进入 Half-Open
permittedNumberOfCallsInHalfOpenState: 5 # Half-Open 时允许通过几个探测请求
minimumNumberOfCalls: 10 # 最少多少个请求才开始统计(避免波动)
registerHealthIndicator: true # 注册到监控指标
instances:
# 这里的名称要与 filters 中 name: userCircuitBreaker 一致
userCircuitBreaker:
baseConfig: default # 继承 default
# 超时控制(与熔断器配合使用)
timelimiter:
configs:
default:
timeoutDuration: 10s # 接口超时时间
cancelRunningFuture: true
instances:
userCircuitBreaker:
baseConfig: default🚩 7.4 Fallback 降级处理
package com.example.gateway.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 降级处理控制器
* forward:/fallback/users 会转发到这里的方法
*/
@RestController
@RequestMapping("/fallback")
public class FallbackController {
/** 用户服务降级 */
@GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> userFallback() {
return buildFallbackResponse("用户服务暂时不可用");
}
/** 订单服务降级 */
@GetMapping(value = "/orders", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> orderFallback() {
return buildFallbackResponse("订单服务暂时不可用");
}
/** 通用降级(从异常中获取信息) */
@GetMapping(value = "/general", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> generalFallback() {
// 从网关上下文获取异常信息(Resilience4j 会塞进去)
return buildFallbackResponse("服务调用失败,已降级");
}
private ResponseEntity<Map<String, Object>> buildFallbackResponse(String message) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("code", 503);
body.put("message", message);
body.put("timestamp", LocalDateTime.now().toString());
body.put("data", null);
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(body);
}
}💡 fallbackUri 语法:
forward:/fallback/users是网关内部转发,不会再经过认证等过滤器。如果用redirect:/xxx则是 302 重定向到外部 URL。
8. 高级特性
8.1 跨域处理(CORS)
全局配置(推荐)
spring:
cloud:
gateway:
globalcors:
# 所有路由生效
cors-configurations:
'[/**]':
allowedOriginPatterns: "*" # 允许所有来源(生产环境请指定具体域名)
allowedMethods: # 允许的 HTTP 方法
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders: "*" # 允许所有请求头
exposedHeaders: # 暴露给前端读取的响应头
- X-Request-Id
- X-Total-Count
allowCredentials: true # 允许携带 Cookie/Session
maxAge: 3600 # 预检请求缓存时间(秒)
# 解决请求头中有多个 Origin 时的问题
add-to-simulated-request: false
# 解决预检请求 OPTIONS 404 的问题
allowed-headers: "*"Java 代码配置
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://app.example.com", "https://admin.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("X-Request-Id"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}8.2 动态路由
package com.example.gateway.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 动态路由管理服务
* 可配合 Nacos Config 或数据库实现路由热更新
*/
@Service
public class DynamicRouteService {
private final RouteDefinitionWriter writer;
private final RouteDefinitionLocator locator;
public DynamicRouteService(RouteDefinitionWriter writer, RouteDefinitionLocator locator) {
this.writer = writer;
this.locator = locator;
}
/** 添加/更新路由 */
public void save(RouteDefinition definition) {
writer.save(Mono.just(definition)).subscribe();
}
/** 删除路由 */
public void delete(String routeId) {
writer.delete(Mono.just(routeId)).subscribe();
}
/** 查看所有已生效路由 */
public Flux<RouteDefinition> listAll() {
return locator.getRouteDefinitions();
}
}/**
* 管理接口:可以用 Postman/curl 动态管理路由
*/
@RestController
@RequestMapping("/admin/routes")
public class RouteManageController {
private final DynamicRouteService service;
@PostMapping
public String add(@RequestBody RouteDefinition definition) {
service.save(definition);
return "success";
}
@DeleteMapping("/{id}")
public String delete(@PathVariable String id) {
service.delete(id);
return "success";
}
@GetMapping
public Flux<RouteDefinition> list() {
return service.listAll();
}
}8.3 请求体日志(⚠️ 重点)
问题:HTTP 请求体只能读一次!如果 GlobalFilter 里读了,后端服务就读不到了。
解决方案:用 CacheRequestBody 装饰器缓存请求体。
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* 请求体日志过滤器
* 使用 CacheRequestBody 确保后端服务仍能正常读取 body
*/
@Component
public class RequestBodyLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 用 DataBufferUtils.join 把 body 读取并缓存
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String bodyStr = new String(bytes, StandardCharsets.UTF_8);
// 打印请求体(注意:生产环境要脱敏!)
if (!bodyStr.isBlank()) {
System.out.println("Request Body: " + truncate(bodyStr, 2000));
}
// 2. 包装 Request,让 body 可重复读取
ServerHttpRequest mutated = new CachedBodyRequest(
exchange.getRequest(), bytes);
return chain.filter(exchange.mutate().request(mutated).build());
});
}
private String truncate(String s, int max) {
return s.length() > max ? s.substring(0, max) + "...(truncated)" : s;
}
@Override
public int getOrder() { return -500; } // 最前
}
/** 简单的 Body 缓存装饰器(实际项目建议用 spring-cloud-gateway-core 的现成实现) */
class CachedBodyRequest extends ServerHttpRequestDecorator {
private final byte[] cachedBody;
public CachedBodyRequest(ServerHttpRequest delegate, byte[] cachedBody) {
super(delegate);
this.cachedBody = cachedBody;
}
@Override
public org.springframework.core.io.buffer.DataBuffer getBody() {
// 每次被消费都返回同一个 body 副本(可重复读)
return bufferFactory().wrap(cachedBody);
}
}💡 更简单的方案:Spring Cloud 自带
ModifyRequestBodyGatewayFilter,配合ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR即可。
8.4 网关监控(Actuator)
management:
endpoints:
web:
exposure:
include: health,info,gateway,metrics,prometheus
endpoint:
gateway:
enabled: true
metrics:
tags:
application: api-gateway
# 访问路由列表
# curl http://localhost:8080/actuator/gateway/routes
# 网关健康检查
# curl http://localhost:8080/actuator/health9. 架构原理剖析 💡
9.1 WebFlux 响应式基础
Gateway 基于 Spring 5 WebFlux,底层是 Project Reactor(Publisher 模式)和 Netty(非阻塞 IO)。
请求处理链路(响应式管道):
HTTP Request
│
▼
Reactor Netty(非阻塞 IO 接收)
│
▼
HttpWebHandlerAdapter
│
▼
GlobalFilter Chain(依次执行)
│
▼
GatewayFilter Chain(依次执行)
│
▼
Route 匹配(Predicate 校验)
│
▼
LoadBalancer 选择实例
│
▼
Reactor Netty(非阻塞 IO 转发给后端)
│
▼
HTTP ResponseMono/Flux 简介:
Mono<T>:0 或 1 个元素的异步序列(类似 CompletableFuture)Flux<T>:0 到 N 个元素的异步序列(类似 Java Stream + 异步)
Gateway 所有过滤器方法都返回 Mono<Void>,表示"异步地完成过滤器逻辑"。
9.2 Reactor Netty 连接池配置
spring:
cloud:
gateway:
# 全局响应式 HTTP 客户端配置(Reactor Netty)
httpclient:
# 连接超时
connect-timeout: 5000
# 响应读取超时(注意不是每个请求的总超时)
response-timeout: 30s
# 连接池
pool:
type: elastic # elastic(弹性)/ fixed(固定)/ disabled
max-connections: 500
acquire-timeout: 4500ms
max-idle-time: 60s
# SSL
ssl:
trusted-x509-certificates:
- classpath:certs/ca.pem
# 也可以对单条路由设置超时
spring.cloud.gateway.routes[0].filters[0] = name:RequestTimeout args:time:5s10. 最佳实践与踩坑 ⚠️
⚠️ 10.1 不要引入 spring-boot-starter-web!
错误:APPLICATION FAILED TO START
Description:
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.解决:排除 starter-web 依赖。如果你的 pom 父 POM 里带了,用 <exclusions> 排除。
⚠️ 10.2 请求体只能读一次
问题:在 GlobalFilter 里 exchange.getRequest().getBody() 读了之后,后端服务收到空 body。
解决:用 DataBufferUtils.join() + 自定义 ServerHttpRequestDecorator 缓存 body,或用 Spring 自带的 @ServerCodecConfigurer + HttpMessageReader。
⚠️ 10.3 文件上传大小限制
spring:
cloud:
gateway:
# Netty 内存帧大小限制(默认 256KB,上传大文件必须改)
max-in-memory-size: 10MB
# 如果你用了 Multipart
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB⚠️ 10.4 超时配置建议
# 两个超时的区别:
spring:
cloud:
gateway:
httpclient:
connect-timeout: 5000 # 连接超时(建立 TCP 连接,通常几百ms 就够)
response-timeout: 30s # 响应超时(等待后端返回,业务总耗时)
# 路由级别覆盖
# filters: - name:RequestTimeout args:time:10s⚠️ 10.5 性能调优清单
| 项目 | 建议 |
|---|---|
| JVM | -Xms512m -Xmx2g -XX:+UseG1GC(4核8G 起步) |
| 连接池 | pool.type=elastic,max-connections=500 |
| 日志 | 生产环境 INFO 以上,TRACE 会丢性能 |
| Predicate | 用 Path + Method 最精确,避免 Host 等开销大的 |
| 过滤器 | 精简,能 GlobalFilter 做的不要在 GatewayFilter 重复 |
| Nacos | 开启 spring.cloud.nacos.discovery.watch.enabled |
| DNS | 网关到注册中心用内网地址,不要走公网 |
11. 完整实战示例 🚩
电商系统网关 application.yml 完整配置
server:
port: 8080
spring:
application:
name: mall-gateway
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:8848
namespace: public
gateway:
# 跨域全局配置
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
allowedHeaders: "*"
exposedHeaders: [X-Request-Id, X-Total-Count]
allowCredentials: true
maxAge: 3600
# HTTP 客户端(Reactor Netty)
httpclient:
connect-timeout: 5000
response-timeout: 15s
pool:
type: elastic
max-connections: 500
acquire-timeout: 4500ms
# 请求体缓存大小
max-in-memory-size: 10MB
# 路由表
routes:
# ===== 用户服务 =====
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**,/api/auth/**
- Header=X-Request-Id, \d+
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 50
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: userCB
fallbackUri: forward:/fallback/general
order: 1
# ===== 商品服务 =====
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**,/api/categories/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 50
redis-rate-limiter.burstCapacity: 100
key-resolver: "#{@ipKeyResolver}"
- name: CircuitBreaker
args:
name: productCB
fallbackUri: forward:/fallback/general
order: 2
# ===== 订单服务 =====
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**,/api/cart/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: orderCB
fallbackUri: forward:/fallback/general
order: 3
# ===== 搜索服务(灰度路由) =====
- id: search-service-v1
uri: lb://search-service
predicates:
- Path=/api/search/**
- Header=X-Env, prod
order: 10
- id: search-service-canary
uri: lb://search-service-canary
predicates:
- Path=/api/search/**
- Header=X-Env, canary
order: 10
# Resilience4j 熔断配置
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 20
slidingWindowType: COUNT_BASED
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 5
minimumNumberOfCalls: 10
registerHealthIndicator: true
instances:
userCB: { baseConfig: default }
productCB: { baseConfig: default, failureRateThreshold: 30 }
orderCB: { baseConfig: default, slidingWindowSize: 10 }
# Redis(限流用)
spring.redis:
host: ${REDIS_HOST:localhost}
port: 6379
timeout: 3000ms
lettuce.pool:
max-active: 32
max-idle: 16
min-idle: 4
# 监控
management:
endpoints:
web:
exposure:
include: health,info,gateway,metrics
endpoint:
gateway:
enabled: true
logging:
level:
root: info
org.springframework.cloud.gateway: warn
reactor.netty.http.client: warn12. 小结
┌──────────────────────────────────────────────────────┐
│ Spring Cloud Gateway 知识地图 │
├──────────────────────────────────────────────────────┤
│ ⭐ 核心概念 │
│ Route(路由)· Predicate(谓词)· Filter(过滤器) │
├──────────────────────────────────────────────────────┤
│ 🎯 必掌握功能 │
│ YAML 路由 · 11种谓词 · 20+过滤器 · JWT认证 │
│ Redis 限流 · Resilience4j 熔断 · 全局 CORS │
├──────────────────────────────────────────────────────┤
│ 💡 重点难点 │
│ WebFlux 响应式模型 · 连接池配置 │
│ RequestBody 只能读一次的问题 │
│ 熔断三态转换逻辑 │
├──────────────────────────────────────────────────────┤
│ ⚠️ 避坑要点 │
│ 不能引 starter-web · $转义 · 超时两层配置 │
│ 文件上传 max-in-memory-size │
├──────────────────────────────────────────────────────┤
│ 🚩 实战代码 │
│ TraceLogFilter · JwtAuthFilter · RequestRateLimiter │
│ FallbackController · 电商网关完整 application.yml │
└──────────────────────────────────────────────────────┘一句话总结:Gateway = WebFlux + Reactor Netty + 路由规则引擎,核心就是 Route(去哪儿) + Predicate(什么时候去) + Filter(去之前/回来后做什么)。