Spring Cloud Gateway的断言(Predicate)与过滤(Filter)链执行顺序

好的,我们开始。

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,80Weight=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 断言的组合

可以使用逻辑运算符(ANDORNOT)组合多个断言,构建更复杂的路由规则。

  • 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 的执行流程如下:

  1. 接收请求: Gateway 接收到客户端请求。
  2. 路由匹配: Gateway 遍历所有配置的路由规则,并评估每个路由的断言。
  3. 断言评估: 对于每个路由,Gateway 依次评估其所有断言。只有当所有断言都评估为 true 时,路由才会被匹配。
  4. 选择路由: 如果多个路由都匹配,Gateway 会选择优先级最高的路由。路由的优先级由 order 属性决定,值越小优先级越高。如果没有指定 order,则按照配置的顺序决定优先级。
  5. 执行过滤器链: 一旦选择了路由,Gateway 将执行与该路由关联的过滤器链。
  6. 过滤器链执行: 过滤器链由 GatewayFilter 和 GlobalFilter 组成。它们的执行顺序如下:
    • GlobalFilter (pre): 所有 GlobalFilter 的 filter 方法按照 getOrder() 方法返回值的升序执行(值越小,优先级越高)。这些过滤器在请求被路由到下游服务之前执行。
    • GatewayFilter (pre): 路由特定的 GatewayFilter 按照配置的顺序执行。这些过滤器也在请求被路由到下游服务之前执行。
    • 请求路由: Gateway 将请求路由到下游服务。
    • 响应接收: Gateway 接收到下游服务的响应。
    • GatewayFilter (post): 路由特定的 GatewayFilter 按照配置顺序的逆序执行。这些过滤器在响应返回给客户端之前执行。
    • GlobalFilter (post): 所有 GlobalFilter 的 filter 方法按照 getOrder() 方法返回值的降序执行(值越大,优先级越高)。这些过滤器在响应返回给客户端之前执行。
  7. 返回响应: 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:FilterAFilterBFilterC,它们的 order 分别为 1、2 和 3。那么,它们的执行顺序如下:

  1. Filter A – Pre
  2. Filter B – Pre
  3. Filter C – Pre
  4. … (请求路由到下游服务)
  5. Filter C – Post
  6. Filter B – Post
  7. Filter A – Post

这个例子清晰地展示了 GlobalFilter 的 pre-filtering 按照 order 升序执行,post-filtering 按照 order 降序执行。GatewayFilter 也是类似,只是作用域是路由级别的。

5. 重要注意事项

  • 短路: 如果任何一个断言评估为 false,路由匹配过程将立即停止,Gateway 将尝试匹配下一个路由。
  • 异常处理: 在过滤器中处理异常非常重要。如果过滤器抛出异常,可能会中断整个过滤器链。可以使用 onErrorResumeonErrorReturn 等反应式操作符来处理异常。
  • 性能优化: 过度使用过滤器会影响 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的执行顺序是不同的,务必根据需求进行调整。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注