Gateway 过滤器与路由断言

候选人小陈在面试快手网关团队时,面试官问:"Gateway 的过滤器链执行顺序是怎样的?自定义过滤器的优先级怎么控制?"

小陈说:"可以用 @Order 注解..." 面试官追问:"@Order 和 GatewayFilter 的 Order 接口有什么区别?数字越小优先级越高还是越低?"

小陈说:"应该是越小越高..." 面试官继续追问:"Gateway 的路由断言有哪些?权重路由怎么配置?"

小陈支支吾吾答不上来。

面试官又问:"那 Filter 链的 Pre 和 Post 是什么意思?它们分别对应什么场景?"

小陈彻底卡住。

【面试官心理】

这道题我用来测试候选人对 Gateway 过滤器链执行机制的深度理解。Gateway 的 Filter 链是网关的核心,理解了 Filter 链就理解了网关的灵魂。只配置过路由的占 80%,能说出 Filter 链执行顺序的占 40%,能自定义 GlobalFilter 并控制优先级的占 20%。过滤器链是区分"用过"和"理解"的分水岭。

一、过滤器链执行机制 🔴

1.1 过滤器链的本质

Gateway 的过滤器链本质上是一个责任链模式(Chain of Responsibility),每个过滤器处理请求的某个切面:

graph LR
    A[请求进入] --> B[Pre Filter 1<br/>日志]
    B --> C[Pre Filter 2<br/>鉴权]
    C --> D[Pre Filter 3<br/>限流]
    D --> E[Netty 路由<br/>调用后端服务]
    E --> F[Post Filter 3<br/>响应处理]
    F --> G[Post Filter 2<br/>Header处理]
    G --> H[Post Filter 1<br/>日志]
    H --> I[响应返回]

Pre Filter:在请求转发到后端服务之前执行(处理请求) Post Filter:在后端服务返回响应之后执行(处理响应)

1.2 过滤器链源码解析

// FilteringWebHandler.java - 过滤器执行的核心类
public class FilteringWebHandler implements WebHandler {
    private final Route route;
    private final List<GatewayFilter> globalFilters;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        // 1. 合并全局过滤器和路由局部过滤器
        List<GatewayFilter> allFilters = new ArrayList<>();
        allFilters.addAll(this.globalFilters);   // GlobalFilter
        allFilters.addAll(route.getFilters());   // GatewayFilter

        // 2. 按优先级排序(Order 越小越先执行)
        allFilters.sort(Comparator.comparingInt(GatewayFilter::getOrder));

        // 3. 构建过滤器链
        return new DefaultGatewayFilterChain(allFilters, 0).filter(exchange);
    }
}

// DefaultGatewayFilterChain.java - 责任链实现
public class DefaultGatewayFilterChain implements GatewayFilterChain {
    private final List<GatewayFilter> filters;
    private final int index;

    public Mono<Void> filter(ServerWebExchange exchange) {
        if (this.index >= this.filters.size()) {
            // 所有过滤器执行完毕,发送请求到后端
            return handleNow(exchange);
        }

        // 执行当前索引的过滤器
        GatewayFilter filter = this.filters.get(this.index);

        // 递归调用下一个过滤器
        return filter.filter(exchange,
            new DefaultGatewayFilterChain(this.filters, this.index + 1));
    }
}

1.3 优先级控制

// Gateway 过滤器的 Order 来源有两种:

// 方式一:实现 Ordered 接口
@Component
public class MyFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 过滤器逻辑
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -100;  // 数字越小,优先级越高,越先执行
    }
}

// 方式二:使用 @Order 注解
@Component
@Order(-100)
public class AnotherFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange);
    }
}

// 常见内置过滤器的优先级
/*
 * NettyRoutingFilter              Integer.MIN_VALUE  最先执行
 * NettyWriteResponseFilter        Integer.MIN_VALUE + 1
 * ForwardRoutingFilter            Integer.MIN_VALUE + 1
 * RouteToRequestUrlFilter        10000
 * LoadBalancerClientFilter       10101
 * WebsocketRoutingFilter         Integer.MAX_VALUE  最后执行
 */
💡

Gateway 使用 Comparator.comparingInt(GatewayFilter::getOrder) 进行排序,数字越小排在越前面,也就是越先执行。如果两个 Filter 的 Order 相同,按照添加顺序执行。

二、内置过滤器详解 🔴

2.1 请求过滤器

过滤器功能示例
AddRequestHeader添加请求头AddRequestHeader=X-Gateway, Spring
RemoveRequestHeader移除请求头RemoveRequestHeader=X-Request-Id
AddRequestParameter添加请求参数AddRequestParameter=source, gateway
RemoveRequestParameter移除请求参数RemoveRequestParameter=debug
SetRequestHeader设置请求头(覆盖)SetRequestHeader=X-Real-IP, 1.2.3.4
SetPath修改请求路径SetPath=/api/v2{path}
StripPrefix去除路径前缀StripPrefix=1(去掉第一段路径)
# 路由配置示例
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/gateway/user/**
          filters:
            # 添加请求头
            - AddRequestHeader=X-Gateway, Gateway
            # 去除第一层路径前缀
            # /gateway/user/list -> /user/list
            - StripPrefix=1
            # 添加请求参数
            - AddRequestParameter=source, gateway

2.2 响应过滤器

过滤器功能示例
AddResponseHeader添加响应头AddResponseHeader=X-Response-Time, 100ms
RemoveResponseHeader移除响应头RemoveResponseHeader=Server
SetResponseHeader设置响应头(覆盖)SetResponseHeader=X-Real-IP, 1.2.3.4
SetStatus设置 HTTP 状态码SetStatus=UNAUTHORIZED
RedirectTo重定向RedirectTo=302, https://example.com
# 响应头处理示例
spring:
  cloud:
    gateway:
      routes:
        - id: api-route
          uri: lb://api-service
          filters:
            # 添加响应头:当前时间
            - AddResponseHeader=X-Response-Time, '#{T(System).currentTimeMillis()}'
            # 移除敏感响应头
            - RemoveResponseHeader=Server
            - RemoveResponseHeader=X-Powered-By

2.3 协议转换过滤器

# HTTP 转 WebSocket
spring:
  cloud:
    gateway:
      routes:
        - id: websocket-route
          uri: ws://echo-server:9000
          predicates:
            - Path=/ws/echo/**
          filters:
            - StripPrefix=1

三、自定义过滤器 🔴

3.1 全局过滤器(GlobalFilter)

// 自定义全局过滤器:记录请求耗时
@Component
@Slf4j
public class RequestTimeFilter implements GlobalFilter, Ordered {

    private static final String START_TIME = "startTime";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. Pre 阶段:记录开始时间
        exchange.getAttributes().put(START_TIME, System.currentTimeMillis());

        // 2. 继续执行过滤器链
        return chain.filter(exchange)
            // 3. Post 阶段:计算并记录耗时
            .then(Mono.fromRunnable(() -> {
                Long startTime = exchange.getAttribute(START_TIME);
                if (startTime != null) {
                    long duration = System.currentTimeMillis() - startTime;
                    String path = exchange.getRequest().getPath().value();
                    log.info("请求路径: {}, 耗时: {}ms", path, duration);
                }
            }));
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;  // 最高优先级,最先执行
    }
}

3.2 局部过滤器(GatewayFilter)

局部过滤器只对特定路由生效:

// 1. 定义自定义过滤器工厂
@Component
public class MyGatewayFilterFactory
    extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {

    public static class Config {
        private String prefix;
        private boolean enabled = true;

        public String getPrefix() { return prefix; }
        public void setPrefix(String prefix) { this.prefix = prefix; }
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
    }

    public MyGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }

            String prefix = config.getPrefix() != null
                ? config.getPrefix()
                : "default";

            // 添加带前缀的 Header
            ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-Request-Prefix", prefix)
                .build();

            return chain.filter(
                exchange.mutate().request(request).build()
            );
        };
    }
}
# 使用自定义过滤器
spring:
  cloud:
    gateway:
      routes:
        - id: my-route
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            # 使用默认配置
            - MyGatewayFilter
            # 使用自定义配置
            - name: MyGatewayFilter
              args:
                prefix: custom-prefix
                enabled: true

3.3 过滤器工厂命名规则

Gateway 过滤器工厂有两种命名风格:

// 方式一:简写形式(Spring 自动推断)
- StripPrefix=1
- AddRequestHeader=X-Token, token123

// 方式二:完整形式
- name: StripPrefixFactory
  args:
    parts: 1
- name: AddRequestHeaderGatewayFilterFactory
  args:
    name: X-Token
    value: token123

Spring Boot 的命名推断规则:

  • 类名以 GatewayFilterFactory 结尾
  • 使用时去掉 GatewayFilterFactory 后缀
  • 例如:AddRequestHeaderGatewayFilterFactory -> AddRequestHeader

四、路由断言工厂 🔴

4.1 常用断言工厂

断言工厂功能配置示例
Path路径匹配- Path=/user/**
MethodHTTP 方法- Method=GET,POST
Header请求头匹配- Header=X-Request-Id, \d+
Query查询参数匹配- Query=name, zhang.*
CookieCookie 匹配- Cookie=session, abc.*
HostHost 匹配- Host=**.example.com
After时间在某个时间点之后- After=2024-01-01T00:00:00Z
Before时间在某个时间点之前- Before=2024-12-31T23:59:59Z
Between时间在某个时间范围内- Between=2024-01-01T00:00:00Z,2024-12-31T23:59:59Z
RemoteAddr客户端 IP- RemoteAddr=192.168.1.0/24
# 路由断言配置示例
spring:
  cloud:
    gateway:
      routes:
        # 多条件组合:同时满足所有断言才匹配
        - id: multi-condition-route
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=name, zhang.*
            - Host=**.example.com
            - After=2024-01-01T00:00:00Z

4.2 权重路由

权重路由用于灰度发布和金丝雀发布:

# 权重路由配置
spring:
  cloud:
    gateway:
      routes:
        # 版本 A(接收 80% 流量)
        - id: user-service-v1
          uri: http://user-service-v1:8080
          predicates:
            - Path=/user/**
            - Weight=user-service, 8  # 权重 8
          filters:
            - SetPath=/api/v1{path}

        # 版本 B(接收 20% 流量)
        - id: user-service-v2
          uri: http://user-service-v2:8080
          predicates:
            - Path=/user/**
            - Weight=user-service, 2  # 权重 2
          filters:
            - SetPath=/api/v2{path}
⚠️

权重路由基于请求的 group 进行哈希,同一个请求总是路由到同一个版本。如果需要按用户 ID 进行灰度,需要自定义 Predicate。

4.3 自定义路由断言

// 自定义断言工厂
@Component
public class UserIdHeaderRoutePredicateFactory
    extends AbstractRoutePredicateFactory<UserIdHeaderRoutePredicateFactory.Config> {

    public static class Config {
        private String headerName = "X-User-Id";

        public String getHeaderName() { return headerName; }
        public void setHeaderName(String headerName) {
            this.headerName = headerName;
        }
    }

    public UserIdHeaderRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String userId = exchange.getRequest()
                .getHeaders()
                .getFirst(config.getHeaderName());
            // 判断 userId 是否存在且为数字
            return userId != null && userId.matches("\\d+");
        };
    }
}

五、动态路由 🟡

5.1 基于服务名自动路由

Gateway 可以根据服务名自动路由,无需手动配置:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          # 开启服务名路由
          enabled: true
          # 服务名转换为小写
          lower-case-service-id: true
          # 路由前缀(默认 /serviceId/**)
          url-expression: "'/' + serviceId + '/**'"
          # 去除服务名前缀
          strip-prefix: true

配置后,可以直接访问:

GET /user-service/user/123
-> 自动路由到 lb://user-service/user/123

5.2 动态路由实现

// RouteDefinitionWriter.java - 动态路由接口
public interface RouteDefinitionWriter {
    // 保存路由定义
    Mono<Void> save(Mono<RouteDefinition> route);

    // 删除路由
    Mono<Void> delete(Mono<String> routeId);

    // 更新路由
    default Mono<Void> update(Mono<RouteDefinition> route) {
        return save(route);
    }
}

// 动态添加路由
@RestController
public class DynamicRouteController {
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    @PostMapping("/routes")
    public Mono<ResponseEntity<Void>> addRoute(@RequestBody RouteDefinition route) {
        return routeDefinitionWriter.save(Mono.just(route))
            .then(Mono.just(ResponseEntity.ok().<Void>build()));
    }

    @DeleteMapping("/routes/{id}")
    public Mono<ResponseEntity<Void>> deleteRoute(@PathVariable String id) {
        return routeDefinitionWriter.delete(Mono.just(id))
            .then(Mono.just(ResponseEntity.noContent().build()));
    }
}

六、常见翻车现场 🔴

❌ 翻车点一:Filter 顺序写错导致鉴权失效

// ❌ 错误:限流 Filter 在鉴权 Filter 之前执行
// 导致未登录用户也会被限流统计
public class RateLimitFilter implements GlobalFilter {
    @Override
    public int getOrder() {
        return 0;  // 鉴权是 -100,应该比它先执行(数字更小)
    }
}

public class AuthFilter implements GlobalFilter {
    @Override
    public int getOrder() {
        return -100;  // 鉴权应该最先执行
    }
}

// ✅ 正确:鉴权 Filter 的 Order 更小(优先级更高)
// 执行顺序:AuthFilter(-100) -> RateLimitFilter(0)

❌ 翻车点二:Post Filter 的 then() 使用错误

// ❌ 错误:没有正确使用 then(),导致 Post 阶段不执行
public class MyFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange)
            .doOnSuccess(aVoid -> {
                // 注意:这个 doOnSuccess 不会在 Post 过滤器中执行
                // 因为它没有包装成 filter 链的一部分
                System.out.println("执行了");
            });
    }
}

// ✅ 正确:使用 then() 将 Post 逻辑串联到过滤器链
public class MyFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Pre 逻辑
        System.out.println("Pre 阶段");

        return chain.filter(exchange)
            // Post 逻辑:then() 接收一个 Mono,在请求处理完成后执行
            .then(Mono.fromRunnable(() -> {
                System.out.println("Post 阶段");
            }));
    }
}

❌ 翻车点三:网关超时配置缺失导致请求挂死

# ❌ 缺少超时配置
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service

# ✅ 正确配置超时
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          filters:
            - name: RequestTimeout
              args:
                # 全局超时
                request-timeout: 5000
                # 后端超时
                backend-timeout: 3000

【面试官心理】

这道题我通常从过滤器链的执行顺序开始,逐步深入到自定义过滤器、断言工厂、动态路由。能说出 Pre/Post 概念的占 50%,能正确实现 GlobalFilter 并控制优先级的占 30%,能说出过滤器链的异步执行机制和 then() 语义的只有 15%。过滤器链是 Gateway 的灵魂,能把这个讲清楚的候选人对响应式编程和责任链模式有较深的理解。