好的,我们开始。
Spring Cloud Gateway 断言与过滤链执行顺序详解
大家好,今天我们来深入探讨 Spring Cloud Gateway 的核心概念:断言(Predicate)和过滤(Filter)链,以及它们至关重要的执行顺序。理解这些机制对于构建高效、可维护和可扩展的微服务网关至关重要。
1. Spring Cloud Gateway 架构概览
首先,我们快速回顾一下 Spring Cloud Gateway 的架构。它本质上是一个基于 Spring WebFlux 的反应式 API 网关,负责接收客户端请求,将其路由到下游服务,并处理响应。其核心组件包括:
- RouteLocator: 负责定义路由规则,将请求映射到特定的下游服务。
- Predicate: 决定路由是否应该匹配给定请求的条件。
- Filter: 修改请求或响应,或执行其他横切关注点,如认证、授权、限流等。
2. 断言(Predicate)
断言是路由规则的关键组成部分,它们用于评估请求的属性,例如请求头、路径、查询参数等。只有当所有断言都评估为 true 时,路由才会被匹配。Spring Cloud Gateway 提供了多种内置的断言,同时也支持自定义断言。
2.1 内置断言
Spring Cloud Gateway 提供了丰富的内置断言,可以满足各种常见的路由需求。以下是一些常用的断言及其示例:
| 断言 | 描述 | 示例 |
|---|---|---|
Path |
基于请求路径匹配路由。 | Path=/api/** (匹配以 /api/ 开头的任何路径) |
Method |
基于 HTTP 请求方法匹配路由。 | Method=GET,POST (匹配 GET 或 POST 请求) |
Query |
基于请求查询参数匹配路由。 | Query=name,value (匹配包含 name 参数且值为 value 的请求) Query=name (匹配包含name参数,不关心参数值) |
Header |
基于请求头匹配路由。 | Header=X-Request-Id,.* (匹配包含 X-Request-Id 请求头,且其值不为空的请求) |
Cookie |
基于 Cookie 值匹配路由。 | Cookie=my-cookie,my-value (匹配包含名为 my-cookie 的 Cookie,且其值为 my-value 的请求) |
Host |
基于 Host 请求头匹配路由。 | Host=*.example.com (匹配 Host 头以 .example.com 结尾的请求) |
RemoteAddr |
基于客户端 IP 地址匹配路由。 | RemoteAddr=192.168.1.0/24 (匹配来自 192.168.1.0/24 网段的请求) |
Weight |
基于权重进行路由,用于实现灰度发布或 A/B 测试。 | Weight=group1,80 和 Weight=group2,20 (将 80% 的流量路由到 group1,20% 的流量路由到 group2) |
After |
匹配指定时间之后到达的请求。 | After=2023-12-31T23:59:59+08:00[Asia/Shanghai] (匹配 2023 年 12 月 31 日 23:59:59 之后到达的请求,时区为上海) |
Before |
匹配指定时间之前到达的请求。 | Before=2023-12-31T23:59:59+08:00[Asia/Shanghai] (匹配 2023 年 12 月 31 日 23:59:59 之前到达的请求,时区为上海) |
Between |
匹配指定时间范围内的请求。 | Between=2023-01-01T00:00:00+08:00[Asia/Shanghai],2023-12-31T23:59:59+08:00[Asia/Shanghai] (匹配 2023 年全年的请求,时区为上海) |
2.2 自定义断言
如果内置断言无法满足你的需求,你可以创建自定义断言。自定义断言需要实现 org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory 接口。
示例:自定义断言,检查请求头中是否包含特定的值
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.function.Predicate;
import java.util.List;
import java.util.Arrays;
@Component
public class CustomHeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomHeaderRoutePredicateFactory.Config> {
public CustomHeaderRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("headerName", "headerValue");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String headerValue = exchange.getRequest().getHeaders().getFirst(config.getHeaderName());
return headerValue != null && headerValue.equals(config.getHeaderValue());
};
}
public static class Config {
private String headerName;
private String headerValue;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public String getHeaderValue() {
return headerValue;
}
public void setHeaderValue(String headerValue) {
this.headerValue = headerValue;
}
}
}
这个自定义断言 CustomHeaderRoutePredicateFactory 检查请求头是否包含指定的名称和值。shortcutFieldOrder 方法定义了配置参数的顺序。
2.3 断言的组合
可以使用逻辑运算符(AND、OR、NOT)组合多个断言,构建更复杂的路由规则。
- AND: 所有断言都必须为
true。 - OR: 至少一个断言必须为
true。 - NOT: 断言的结果取反。
3. 过滤(Filter)
过滤器用于在请求被路由到下游服务之前或之后修改请求和响应。Spring Cloud Gateway 提供了两种类型的过滤器:
- GatewayFilter: 作用于单个路由。
- GlobalFilter: 作用于所有路由。
3.1 GatewayFilter
GatewayFilter 只能在特定的路由配置中使用。Spring Cloud Gateway 提供了大量的内置 GatewayFilter,例如:
| 过滤器 | 描述 | 示例 |
|---|---|---|
AddRequestHeader |
向请求添加请求头。 | AddRequestHeader=X-Request-Foo,Bar (添加 X-Request-Foo 请求头,值为 Bar) |
AddResponseHeader |
向响应添加响应头。 | AddResponseHeader=X-Response-Foo,Bar (添加 X-Response-Foo 响应头,值为 Bar) |
RewritePath |
重写请求路径。 | RewritePath=/api/(?<segment>.*),/${segment} (将 /api/xxx 重写为 /xxx) |
StripPrefix |
从请求路径中移除指定数量的前缀。 | StripPrefix=1 (从请求路径中移除 1 个前缀) |
RequestRateLimiter |
基于令牌桶算法进行限流。 | RequestRateLimiter=redis-rate-limiter.#{@redisRateLimiter.redisRateLimiter()},1,1 (使用 Redis 实现限流,每秒允许 1 个请求,令牌桶容量为 1) |
Retry |
重试失败的请求。 | Retry=3 (重试 3 次) |
CircuitBreaker |
集成 Hystrix 或 Resilience4j 实现断路器模式。 | CircuitBreaker=myCircuitBreaker (使用名为 myCircuitBreaker 的断路器) |
PrefixPath |
为请求路径添加前缀。 | PrefixPath=/my-service (为所有匹配路由的请求路径添加 /my-service 前缀) |
ModifyRequestBody |
修改请求体。 | ModifyRequestBody=String,String,application/json,application/json,T(java.lang.String).valueOf('{"newKey":"newValue"}') (将请求体修改为 {"newKey":"newValue"}) |
ModifyResponseBody |
修改响应体。 | ModifyResponseBody=String,String,application/json,application/json,T(java.lang.String).valueOf('{"newKey":"newValue"}') (将响应体修改为 {"newKey":"newValue"}) |
SaveSession |
保存 WebSession。 | SaveSession (保存当前的 WebSession) |
RemoveRequestHeader |
移除请求头。 | RemoveRequestHeader=X-Request-Foo (移除名为 X-Request-Foo 的请求头) |
RemoveResponseHeader |
移除响应头。 | RemoveResponseHeader=X-Response-Foo (移除名为 X-Response-Foo 的响应头) |
RedirectTo |
将请求重定向到另一个 URL。 | RedirectTo=302,http://example.com (将请求重定向到 http://example.com,状态码为 302) |
SecureHeaders |
添加安全相关的请求头,如 X-Content-Type-Options, Strict-Transport-Security 等。 | SecureHeaders (添加默认的安全头) |
SetPath |
设置请求路径。 | SetPath=/new/path (将请求路径设置为 /new/path) |
SetRequestHeader |
设置请求头。 | SetRequestHeader=X-Request-Foo,Bar (设置 X-Request-Foo 请求头,值为 Bar,如果存在则覆盖) |
SetResponseHeader |
设置响应头。 | SetResponseHeader=X-Response-Foo,Bar (设置 X-Response-Foo 响应头,值为 Bar,如果存在则覆盖) |
SetStatus |
设置响应状态码。 | SetStatus=404 (设置响应状态码为 404) |
3.2 GlobalFilter
GlobalFilter 作用于所有路由,通常用于处理全局性的横切关注点,例如:
- 日志记录: 记录所有请求和响应。
- 认证和授权: 验证用户身份和权限。
- 请求追踪: 集成分布式追踪系统,如 Zipkin 或 Jaeger。
实现 GlobalFilter 需要实现 org.springframework.cloud.gateway.filter.GlobalFilter 接口。
示例:自定义 GlobalFilter,记录请求信息
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoggingGlobalFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(LoggingGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
logger.info("Request received: {} {}", exchange.getRequest().getMethod(), exchange.getRequest().getURI());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 设置过滤器的优先级,值越小优先级越高
}
}
这个 LoggingGlobalFilter 记录所有请求的 HTTP 方法和 URI。getOrder() 方法用于指定过滤器的执行顺序。
3.3 Filter 的组合与执行顺序
Spring Cloud Gateway 允许你将多个过滤器组合成一个过滤器链。过滤器的执行顺序非常重要,因为它会影响请求和响应的处理结果。
4. 断言与过滤链的执行顺序:核心逻辑
现在,我们来深入探讨断言和过滤链的执行顺序。Spring Cloud Gateway 的执行流程如下:
- 接收请求: Gateway 接收到客户端请求。
- 路由匹配: Gateway 遍历所有配置的路由规则,并评估每个路由的断言。
- 断言评估: 对于每个路由,Gateway 依次评估其所有断言。只有当所有断言都评估为
true时,路由才会被匹配。 - 选择路由: 如果多个路由都匹配,Gateway 会选择优先级最高的路由。路由的优先级由
order属性决定,值越小优先级越高。如果没有指定order,则按照配置的顺序决定优先级。 - 执行过滤器链: 一旦选择了路由,Gateway 将执行与该路由关联的过滤器链。
- 过滤器链执行: 过滤器链由 GatewayFilter 和 GlobalFilter 组成。它们的执行顺序如下:
- GlobalFilter (pre): 所有 GlobalFilter 的
filter方法按照getOrder()方法返回值的升序执行(值越小,优先级越高)。这些过滤器在请求被路由到下游服务之前执行。 - GatewayFilter (pre): 路由特定的 GatewayFilter 按照配置的顺序执行。这些过滤器也在请求被路由到下游服务之前执行。
- 请求路由: Gateway 将请求路由到下游服务。
- 响应接收: Gateway 接收到下游服务的响应。
- GatewayFilter (post): 路由特定的 GatewayFilter 按照配置顺序的逆序执行。这些过滤器在响应返回给客户端之前执行。
- GlobalFilter (post): 所有 GlobalFilter 的
filter方法按照getOrder()方法返回值的降序执行(值越大,优先级越高)。这些过滤器在响应返回给客户端之前执行。
- GlobalFilter (pre): 所有 GlobalFilter 的
- 返回响应: Gateway 将处理后的响应返回给客户端。
可以用下面的表格来总结这个顺序:
| 执行阶段 | 过滤器类型 | 执行顺序 |
|---|---|---|
| Pre | GlobalFilter | 按照 getOrder() 返回值的升序执行 (值越小,优先级越高) |
| Pre | GatewayFilter | 按照配置的顺序执行 |
| Routing | 请求被路由到下游服务 | |
| Post | GatewayFilter | 按照配置顺序的逆序执行 |
| Post | GlobalFilter | 按照 getOrder() 返回值的降序执行 (值越大,优先级越高) |
代码示例:演示过滤器执行顺序
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class FilterA implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(FilterA.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("Filter A - Pre");
return chain.filter(exchange).then(Mono.fromRunnable(() -> logger.info("Filter A - Post")));
}
@Override
public int getOrder() {
return 1;
}
}
@Component
public class FilterB implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(FilterB.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("Filter B - Pre");
return chain.filter(exchange).then(Mono.fromRunnable(() -> logger.info("Filter B - Post")));
}
@Override
public int getOrder() {
return 2;
}
}
@Component
public class FilterC implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(FilterC.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("Filter C - Pre");
return chain.filter(exchange).then(Mono.fromRunnable(() -> logger.info("Filter C - Post")));
}
@Override
public int getOrder() {
return 3;
}
}
假设我们有三个 GlobalFilter:FilterA、FilterB 和 FilterC,它们的 order 分别为 1、2 和 3。那么,它们的执行顺序如下:
- Filter A – Pre
- Filter B – Pre
- Filter C – Pre
- … (请求路由到下游服务)
- Filter C – Post
- Filter B – Post
- Filter A – Post
这个例子清晰地展示了 GlobalFilter 的 pre-filtering 按照 order 升序执行,post-filtering 按照 order 降序执行。GatewayFilter 也是类似,只是作用域是路由级别的。
5. 重要注意事项
- 短路: 如果任何一个断言评估为
false,路由匹配过程将立即停止,Gateway 将尝试匹配下一个路由。 - 异常处理: 在过滤器中处理异常非常重要。如果过滤器抛出异常,可能会中断整个过滤器链。可以使用
onErrorResume或onErrorReturn等反应式操作符来处理异常。 - 性能优化: 过度使用过滤器会影响 Gateway 的性能。尽量减少过滤器的数量,并优化过滤器的代码。
- 过滤器共享上下文: 可以使用
ServerWebExchange对象在不同的过滤器之间共享数据。ServerWebExchange提供了getAttributes()方法,可以用于存储和检索数据。
6. 如何配置断言与过滤器
断言与过滤器可以通过多种方式配置,包括:
- application.yml/application.properties: 使用 YAML 或 Properties 文件进行配置。
- Java 代码: 使用 Java 代码创建和配置 RouteLocator Bean。
- Actuator API: 使用 Actuator API 动态管理路由规则。
示例:使用 application.yml 配置路由规则
spring:
cloud:
gateway:
routes:
- id: my_route
uri: http://example.com
predicates:
- Path=/api/**
- Method=GET
filters:
- AddRequestHeader=X-Request-Foo,Bar
- StripPrefix=1
这个配置定义了一个名为 my_route 的路由,它将以 /api/ 开头的 GET 请求路由到 http://example.com,并添加一个名为 X-Request-Foo 的请求头,值为 Bar,然后移除请求路径的第一个前缀。
7. 调试技巧
调试 Spring Cloud Gateway 的路由规则和过滤器链可能比较困难。以下是一些常用的调试技巧:
- 日志记录: 在过滤器中添加详细的日志记录,可以帮助你了解请求的处理过程。
- 断点调试: 使用 IDE 的断点调试功能,可以逐步执行过滤器链,并检查请求和响应的状态。
- Actuator 端点: Spring Cloud Gateway 提供了多个 Actuator 端点,可以用于查看路由规则、过滤器配置和性能指标。常用的端点包括
/actuator/gateway/routes和/actuator/gateway/filters。 - 请求追踪: 集成分布式追踪系统,可以帮助你跟踪请求在不同服务之间的流转。
8. 配置路由的更多示例
以下是一些更详细的配置路由的例子,涵盖了各种常见的场景。
8.1. 基于 Host 和 Path 的路由
spring:
cloud:
gateway:
routes:
- id: host_path_route
uri: http://example.com
predicates:
- Host=*.mycompany.com
- Path=/api/**
这个路由将所有 Host 为 .mycompany.com 结尾,并且 Path 以 /api/ 开头的请求转发到 http://example.com。
8.2. 基于 Header 和 Query 参数的路由
spring:
cloud:
gateway:
routes:
- id: header_query_route
uri: http://example.com
predicates:
- Header=X-Version,v1
- Query=debug,true
这个路由将所有包含 Header X-Version 值为 v1,并且包含 Query 参数 debug=true 的请求转发到 http://example.com。
8.3. 使用 Weight 进行流量分发
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: http://high-priority.com
predicates:
- Weight=group1,80
- id: weight_low
uri: http://low-priority.com
predicates:
- Weight=group1,20
这个配置将 80% 的流量转发到 http://high-priority.com,20% 的流量转发到 http://low-priority.com。 注意,Weight 断言需要指定一个组名(这里是 group1),同一个组内的权重才会生效。
8.4. 使用 After 和 Before 进行路由切换
spring:
cloud:
gateway:
routes:
- id: route_before
uri: http://new-service.com
predicates:
- Before=2024-01-01T00:00:00+08:00[Asia/Shanghai] # 在这个时间之前路由到 new-service
- id: route_after
uri: http://old-service.com
predicates:
- After=2024-01-01T00:00:00+08:00[Asia/Shanghai] # 在这个时间之后路由到 old-service
这个配置实现了基于时间的路由切换。在 2024 年 1 月 1 日之前,请求会被路由到 http://new-service.com,之后会被路由到 http://old-service.com。
9. 总结思考
理解Spring Cloud Gateway的断言和过滤链执行顺序对于构建可靠的微服务网关至关重要。正确配置断言能够精准匹配请求,而合理安排过滤器链则能有效处理各种横切关注点。记住,GlobalFilter和GatewayFilter的执行顺序是不同的,务必根据需求进行调整。