Spring Cloud Gateway 源码拆解:一个请求从进入到转发经历了什么

引言

"网关登录接口一直 401,JWT 密钥、Redis、白名单全查了一遍,最后发现是过滤器顺序问题——自定义 AuthFilter 和 StripPrefix 的 order 都是 0,谁先执行看运气。" 这个真实排查故事的关键线索是网关启动日志里一行不起眼的 Sorted gatewayFilterFactories:两个过滤器 order 相同,路径有时先被 StripPrefix 改写成 /auth/login,白名单配的 /api/auth/login 就永远匹配不上。

Spring Cloud Gateway(SCG)是 Spring Cloud 全家桶里少数基于 WebFlux/Netty 的全异步组件,很多从 Spring MVC 转过来的开发者读它的源码会不适应:没有 Servlet、没有线程池阻塞、到处是 Mono/Flux。这篇文章按一个请求的真实执行路径,从 DispatcherHandler 一路跟到 NettyRoutingFilter,讲透三件事:路由怎么匹配(优先级)、过滤器怎么排序(执行顺序)、响应式链路怎么串起来(Mono 套 Mono)。读完你会明白:网关里几乎所有"玄学 401/路径错乱/过滤器不生效"问题,本质都是这三件事没理清。


一、全景:请求生命周期总览

Netty(Reactor Netty,事件循环线程)
  │ HTTP 请求解码成 ServerWebExchange(请求/响应/属性的响应式容器)
  ↓
DispatcherHandler.handle(exchange)                     ← WebFlux 总入口
  │
  ├─① HandlerMapping 列表 → RoutePredicateHandlerMapping.getHandler()
  │     遍历所有 Route,Predicate 匹配 → 找到第一个命中的路由
  │     把 Route 塞进 exchange 属性
  ↓
  ├─② HandlerAdapter → SimpleHandlerAdapter 支持 FilteringWebHandler
  ↓
FilteringWebHandler.handle(exchange)
  │ 把 GlobalFilter 全部取出 + 当前 Route 的 GatewayFilter
  │ 按 order 排序 → 组装成 DefaultGatewayFilterChain(责任链)
  ↓
过滤器链依次执行:
  -100  ReactiveLoadBalancerClientFilter(负载均衡,解析 service 名)
  -1    NettyWriteResponseFilter(把下游响应写回客户端的"回程"起点)
   0    RouteToRequestUrlFilter(把 lb://service 拼写成真实 URL)
   1    自定义 AuthFilter(鉴权,生产建议放这里)
   2    AddRequestHeader / StripPrefix 等
   …
   3    NettyRoutingFilter(order=Integer.MAX_VALUE,最后执行,真正发请求)
  ↓
下游服务响应 → 沿过滤器链反向回传(after 阶段/post 逻辑)→ 客户端

记住两个锚点:匹配阶段只找路由不转发转发永远发生在 NettyRoutingFilter(order 最大,链尾)。所有鉴权、限流、改路径的过滤器都在它之前。


二、DispatcherHandler:WebFlux 的总调度

2.1 它不是 Servlet 的 DispatcherServlet

// spring-webflux 核心类,代码做了简化
public class DispatcherHandler implements WebHandler {
    private final List<HandlerMapping> handlerMappings;       // 有序
    private final List<HandlerAdapter> handlerAdapters;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return Flux.fromIterable(this.handlerMappings)
                .concatMap(mapping -> mapping.getHandler(exchange))   // ① 找处理器
                .next()                                                // 第一个非空的
                .switchIfEmpty(DispatcherHandler.this.createNotFoundError(exchange))
                .flatMap(handler -> invokeHandler(exchange, handler)) // ② 执行处理器
                .flatMap(result -> handleResult(exchange, result));   // ③ 处理结果
    }
}

三个关键点:

  1. handlerMappings 是列表:SCG 启动时自动装配的 RoutePredicateHandlerMapping 是其中之一,和静态资源、actuator 等 Mapping 共存,按顺序匹配,第一个给出 handler 的胜出。
  2. 返回值全是 Mono:没有任何阻塞调用。concatMap 顺序尝试各 Mapping,next() 取第一个非空——这就是响应式的"短路"。
  3. 没匹配上走 createNotFoundError:对应网关返回 404 的场景(路由表根本没这条路径)。

2.2 ServerWebExchange:贯穿全链路的上下文

public interface ServerWebExchange {
    ServerHttpRequest getRequest();
    ServerHttpResponse getResponse();
    Map<String, Object> getAttributes();   // 过滤器之间传递数据靠它
    // ...
}

网关是异步的,没有 ThreadLocal 可用(一个请求可能在多个 Netty 线程间跳)。过滤器之间传数据(用户信息、开始时间、路由对象)全部塞 exchange.getAttributes()——这也是为什么你在自定义过滤器里拿到当前路由要用:

Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);

三、RoutePredicateHandlerMapping:路由怎么匹配

3.1 核心源码

// spring-cloud-gateway-server,简化
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {

    private final RouteLocator routeLocator;

    @Override
    protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
        return lookupRoute(exchange)
                .flatMap(route -> {
                    exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
                    return Mono.just(this.webHandler);   // 永远返回 FilteringWebHandler
                })
                .switchIfEmpty(Mono.empty());
    }

    protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
        return this.routeLocator.getRoutes()
                .concatMap(route -> Mono.just(route)
                        .filterWhen(r -> {
                            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
                            return r.getPredicate().apply(exchange);     // 执行 Predicate
                        })
                        .doOnError(e -> logger.error(...))
                        .onErrorResume(e -> Mono.empty()))
                .next();    // ← 第一个 Predicate 返回 true 的路由胜出
    }
}

注意 routeLocator.getRoutes() 返回 Flux<Route>——路由是有序的流,按顺序逐个试 Predicate,第一个命中的立即 next() 胜出,后面的路由不看了

3.2 路由的顺序从哪来

路由来源排序规则
Java DSL routes.route(...)按定义顺序(可用 .order(n) 显式指定)
YAML 配置按列表顺序,spring.cloud.gateway.routes 数组从上到下
服务发现自动路由(lb://)DiscoveryClientRouteDefinitionLocator,order 默认很大(靠后)
显式 orderorder 值小的优先
spring:
  cloud:
    gateway:
      routes:
        - id: auth-route
          uri: lb://auth-service
          predicates:
            - Path=/api/auth/**      # ← 更具体的路由放前面
        - id: user-route
          uri: lb://user-service
          predicates:
            - Path=/api/**           # ← 通配范围大的放后面,否则会截胡

3.3 Predicate 组合:AND 语义

一条路由可以配多个 Predicate,它们之间是逻辑与

// 等价于:Path 且 Header 且时间在 0~12 点
predicates:
  - Path=/api/order/**
  - Header=X-Caller, app
  - Between=2026-09-12T00:00:00+08:00, 2026-09-12T12:00:00+08:00

3.4 两个常见排查点

  • 404 但服务明明在线:路由 Predicate 没匹配。开 logging.level.org.springframework.cloud.gateway=DEBUG,日志会打印每条路由的匹配情况;同时确认 Path 匹配是匹配原始请求路径(StripPrefix 还没执行,见第五章的顺序坑)。
  • 路由被"截胡"/api/auth/** 配在了 /api/** 后面,请求全被通配路由接走。原则:具体路由在前、通配路由在后

四、FilteringWebHandler:过滤器链的组装

4.1 核心源码:两类过滤器合并排序

public class FilteringWebHandler implements WebHandler {

    private final List<GatewayFilter> globalFilters;

    public FilteringWebHandler(List<GlobalFilter> globalFilters) {
        // GlobalFilter 启动时收集,包装成 GatewayFilter
        this.globalFilters = loadFilters(globalFilters);
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
        List<GatewayFilter> gatewayFilters = route.getFilters();   // 路由级过滤器

        List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
        combined.addAll(gatewayFilters);                          // 全局 + 路由级合并
        AnnotationAwareOrderComparator.sort(combined);           // ← 按 @Order/getOrder 排序

        return new DefaultGatewayFilterChain(combined).filter(exchange);
    }
}

4.2 GlobalFilter vs GatewayFilter

类型作用范围定义方式典型
GlobalFilter所有路由生效Spring Bean,实现 GlobalFilter + getOrder()NettyRoutingFilter、负载均衡、鉴权
GatewayFilter只对配置它的路由生效YAML filters / RouteFilter 工厂StripPrefix、AddRequestHeader、限流

本质上它们在同一条链里——启动时 GlobalFilter 被 GatewayFilterAdapter 包成 GatewayFilter,然后两类放一起统一按 order 排序。所以"全局过滤器一定在路由过滤器前面"是误解:谁的 order 小谁在前。

4.3 内置过滤器 order 速查表(必须背下来)

过滤器order干什么
RemoveCachedBodyFilter-2147483648(最小)清理缓存 body
AdaptCachedBodyGlobalFilter-2147482648缓存请求体(让 body 可重复读)
NettyWriteResponseFilter-1把下游响应写回(回程过滤器的注册点)
RouteToRequestUrlFilter0用路由信息拼出真实目标 URL
ReactiveLoadBalancerClientFilter10150lb:// 服务名解析为实例
NettyRoutingFilterInteger.MAX_VALUE(链尾)真正用 Netty 发 HTTP 请求
ForwardRoutingFilterInteger.MAX_VALUEforward:// 本地转发
Route 级 GatewayFilter(StripPrefix 等)大多在 0~10改写路径/头/参数

启动时打开 DEBUG 日志可以直接看到排序结果:Sorted gatewayFilterFactories: [...]——排查顺序问题先看这行日志。


五、DefaultGatewayFilterChain:责任链与执行顺序

5.1 链式调用源码

private static class DefaultGatewayFilterChain implements GatewayFilterChain {

    private final int index;
    private final List<GatewayFilter> filters;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange) {
        if (this.index < this.filters.size()) {
            GatewayFilter filter = this.filters.get(this.index);
            DefaultGatewayFilterChain next =
                    new DefaultGatewayFilterChain(this, this.index + 1);
            return filter.filter(exchange, next);   // 当前过滤器执行,并把"下一个链"传给它
        }
        return Mono.empty();
    }
}

和 Servlet 责任链神似,但有一个本质区别:filter.filter(exchange, next) 返回的是 Mono,链的"前进"是声明式组装而不是立即执行。一个典型过滤器长这样:

public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (!verify(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();   // 短路:不调 chain.filter()
        }
        exchange.getAttributes().put("userId", parseUserId(token));
        return chain.filter(exchange);                     // 放行:组装下一个过滤器
    }

    @Override
    public int getOrder() { return 1; }
}

5.2 pre / post 两阶段:Mono 的 defer 效应

return chain.filter(exchange)
        .then(Mono.fromRunnable(() -> {
            // post 逻辑:下游响应回来之后才执行
            log.info("响应状态: {}", exchange.getResponse().getStatusCode());
        }));
执行时序(同步视角的等价描述):
  pre-AuthFilter     ──→ pre-StripPrefix ──→ … ──→ NettyRoutingFilter(发请求)
                                                        │ 等下游 I/O
  post-AuthFilter    ←── post-StripPrefix ←─ … ←─────── 响应返回
  (.then 里的逻辑,顺序反向)

关键认知chain.filter() 之前的代码是 pre(请求出去前),.then()/doOnSuccess() 里的是 post(响应回来后)。因为整条链是"拼装 Mono 图 + 订阅时才执行",post 逻辑的实际执行顺序与 pre 相反——这和 Spring MVC 拦截器的 preHandle/postHandle 模型完全同构,只是用 Mono 算子表达。

5.3 实战坑:AuthFilter 与 StripPrefix 同为 order=0

这是开篇那个登录 401 事故的根因。排序时 order 相同的过滤器相对顺序不稳定(取决于 Bean 注册/列表合并顺序):

时序 A(有时):AuthFilter 先执行
  原始路径 /api/auth/login → 白名单 /api/auth/** 命中 → 放行 → StripPrefix 改写成 /auth/login ✅

时序 B(重启后可能变):StripPrefix 先执行
  路径被改成 /auth/login → AuthFilter 白名单配的是 /api/auth/** → 不命中 → 401 ❌

修复(任选其一,推荐第一种):

// ① 让鉴权明确在路径改写之前
@Override
public int getOrder() { return -100; }   // 早于所有路径改写类过滤器
② 白名单同时兼容改写前后两套路径:/api/auth/** 和 /auth/**
③ 把白名单判断建立在"原始路径"上:
   exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR)

通用规律:路径匹配/鉴权类逻辑要在 StripPrefix、RewritePath、SetPath 等"路径改写类"过滤器之前,order 必须拉开不要都写 0。 路径在过滤器链里是会变的,每个过滤器看到的 path 可能不同——这是网关排障的第一直觉。


六、转发三件套:URL 拼装 → 负载均衡 → Netty 发请求

6.1 RouteToRequestUrlFilter(order=0):拼接目标 URL

// 核心逻辑简化
URI routeUri = route.getUri();                    // lb://user-service
String scheme = routeUri.getScheme();
if ("lb".equalsIgnoreCase(scheme)) {
    // 把 lb:// 替换成 http://,host 保留服务名,交给负载均衡过滤器解析
    URI requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
            .host(routeUri.getHost())
            .scheme("http")
            .build(true).toUri();
    exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
}

6.2 ReactiveLoadBalancerClientFilter(order=10150):服务名换实例

从 LoadBalancer 选一个实例(轮询/随机/权重),把 URL 中的 user-service 替换成 10.0.3.12:8080,还附带处理同一实例的连接复用。配合 Nacos 等注册中心,实例列表来自服务发现(参见 Nacos vs Eureka 那篇)。

6.3 NettyRoutingFilter(order=Integer.MAX_VALUE):真正的转发

链尾的它才是真正发 HTTP 请求的地方,也是全链路的订阅触发点

public class NettyRoutingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

        // 用响应式 HttpClient 发请求(非阻塞,事件循环)
        return this.httpClient.request(requestMethod, url, req -> {
                    // 复制请求头、应用过滤器改写后的 body
                    return req.headers(headers -> addHeaders(...))
                              .send(request.getBody());
                })
                .doOnNext(res -> {
                    // 把下游响应状态/响应头放回 exchange
                    exchange.getResponse().setStatusCode(res.status());
                    // 响应体不在这写回!交给 NettyWriteResponseFilter
                    exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
                })
                .then(chain.filter(exchange));   // 回到链上,由 -1 号过滤器写回响应
    }

    @Override
    public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}

要点:

  1. 全异步非阻塞httpClient.request(...) 返回 Mono<HttpClientResponse>,发出请求的 Netty 线程不等待,响应就绪后回调——这是 SCG 单机能扛上万 QPS 而 Tomcat 版 Zuul 扛不住的根本原因。
  2. 它在链尾:前面所有过滤器改的头、路径、body 在此刻定型发出,所以在它之后的 pre 逻辑没有任何意义。
  3. 响应回写是另一个过滤器NettyWriteResponseFilter(order=-1)在请求阶段就注册好了响应的回写管道,拿到 CLIENT_RESPONSE_ATTR 后把下游响应体流式地写给客户端——请求和响应在这里"会师"。

七、串起响应式链路:一个请求的 Mono 图

把前面的类串起来,一次转发在订阅发生前构建的逻辑图大致是:

onErrorResume(全局异常 → 500 JSON)
  └─ filterChain.filter(exchange)
       ├─ AdaptCachedBody(-2147482648)
       ├─ AuthGlobalFilter(-100)        pre: 校验 token,短路 setComplete(401)
       ├─ NettyWriteResponse(-1)         pre: 装配响应回写
       ├─ RouteToRequestUrl(0)           pre: 拼 URL
       ├─ StripPrefix(1,路由级)          pre: 改路径
       ├─ LoadBalancer(10150)            pre: 选实例
       └─ NettyRoutingFilter(MAX)        pre: 发请求 → 订阅点
                                          post: 响应沿链反向回流

三条响应式心智模型:

  1. 组装 ≠ 执行handle() 一路返回新 Mono,只是在描述"要做什么",直到 Netty 层订阅才真正执行。
  2. 短路靠"不调 chain.filter()":鉴权失败直接返回 response.setComplete(),链上后续过滤器和下游服务一个都不会执行。
  3. 背压天然支持:下游响应体是 Flux<DataBuffer>,客户端读得慢就自然给网关和下游施压背压,整条链路没有"把大响应读进内存再发"的环节——所以不要在过滤器里无脑 DataBufferUtils.join(body) 把流式 body 聚合成大对象,会破坏内存优势。

八、自定义过滤器实战:鉴权 + 计时 + 限流

@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtVerifier jwtVerifier;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // 白名单(原始路径判断,此时 StripPrefix 还没执行,order=-100 保证)
        if (path.startsWith("/api/auth/") || path.equals("/actuator/health")) {
            return chain.filter(exchange);
        }

        String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (token == null || !jwtVerifier.valid(token)) {
            return unauthorized(exchange, "缺少或无效的令牌");
        }

        // 用户身份传给下游:改写请求头(响应式地 mutate)
        String userId = jwtVerifier.userId(token);
        ServerHttpRequest mutated = request.mutate()
                .header("X-User-Id", userId)
                .build();
        long start = System.currentTimeMillis();

        return chain.filter(exchange.mutate().request(mutated).build())
                .doFinally(signalType ->
                        log.info("[GW] {} {} userId={} cost={}ms",
                                request.getMethod(), path, userId,
                                System.currentTimeMillis() - start));
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
        ServerHttpResponse resp = exchange.getResponse();
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        DataBuffer buffer = resp.bufferFactory()
                .wrap(("{\"code\":401,\"message\":\"" + msg + "\"}").getBytes(UTF_8));
        return resp.writeWith(Mono.just(buffer));
    }

    @Override
    public int getOrder() { return -100; }   // 早于 StripPrefix(1)和路由级过滤器
}

这个例子同时用上了三个知识点:order=-100 保证看到原始路径(呼应 401 事故)、短路返回不调 chain.filter()doFinally 做 post 计时(无论成功失败都执行)。


九、常见问题

9.1 为什么我的 GlobalFilter 对某个路由不生效?

检查三件事:① 是否注册成了 Spring Bean(@Component 漏了);② 是否被该路由的 Predicate 覆盖——GlobalFilter 只在"有路由匹配成功"后才会进 FilteringWebHandler,404 的请求走不到任何 GlobalFilter;③ order 是否在 NettyRoutingFilter 之后——getOrder 返回 Integer.MAX_VALUE 之后的逻辑不参与请求阶段。

9.2 多个过滤器 order 相同会怎样?

排序稳定但相对顺序依赖 Bean 注入列表的顺序,重启/加新过滤器后可能变化——开篇 401 事故就是这么来的。铁律:自定义过滤器显式给 order,且与内置过滤器拉开间隔,鉴权类放 -100 或更小,路径改写类保持默认(0~10),自定义业务改写放 100~1000,不要挤在 0。

9.3 过滤器里怎么读请求 body?为什么读完下游就收不到了?

Reactor 的请求体是一次性 Flux,读了就没了。需要读 body(如验签、日志)时用 ServerWebExchangeUtils.cacheRequestBody(exchange, ...)ModifyRequestBodyGatewayFilterFactory,底层由 AdaptCachedBodyGlobalFilter 缓存后可重复读。生产中对大 body 的验签要评估内存,只缓冲必要范围。

9.4 怎么在过滤器里拿到请求开始时间和路由信息?

开始时间自己在 pre 阶段 exchange.getAttributes().put("startTime", ...);路由信息用 exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);真实目标 URL(负载均衡后)用 GATEWAY_REQUEST_URL_ATTR;原始 URL(路径改写前的集合)用 GATEWAY_ORIGINAL_REQUEST_URL_ATTR。这些属性名都是 ServerWebExchangeUtils 里的常量。

9.5 NettyRoutingFilter 之后还能改响应吗?

pre 阶段不能影响已发出的请求,但响应处理可以:NettyWriteResponseFilter(order=-1)在请求前就注册了回写管道,自定义 post 逻辑(.then()/doOnNext 操作 CLIENT_RESPONSE_ATTR)可以改响应状态和头;要改响应体用 ModifyResponseBodyGatewayFilterFactory,本质是在回写管道上插一层 map。顺序上你的 post 逻辑虽然写在自己的过滤器里,但执行时机在下游响应返回之后。

9.6 全异步链路里怎么做阻塞操作(如 JDBC 查权限)?

绝不能在过滤器线程里直接调阻塞 JDBC——会卡住 Netty 事件循环。正确姿势:Mono.fromCallable(() -> jdbcAuth(token)).subscribeOn(Schedulers.boundedElastic()),把阻塞调用调度到弹性线程池,过滤器链仍然是非阻塞的 Mono。更好的选择是权限数据走 Redis(用 Lettuce 的响应式 API),全程不离开事件循环。


十、总结

请求链路速查卡

Netty 收请求 → ServerWebExchange
  → DispatcherHandler.handle(handlerMappings 顺序匹配,第一个非空胜出)
    → RoutePredicateHandlerMapping
        · routeLocator.getRoutes() 按顺序试 Predicate,第一个命中即止
        · 路由顺序:YAML 上下顺序 / 显式 order;具体在前通配在后
    → FilteringWebHandler
        · GlobalFilter(所有路由)+ GatewayFilter(路由级)合并统一排序
        · DefaultGatewayFilterChain 责任链:不调 chain.filter() 即短路
  → -2147482648 缓存body
  → -100 鉴权(自定义,务必早于路径改写)
  → -1   NettyWriteResponse(装配回写)
  → 0    RouteToRequestUrl(拼 URL)
  → 1    StripPrefix 等(路径在此被改写!)
  → 10150 LoadBalancer(lb:// 服务名→实例IP)
  → MAX  NettyRoutingFilter(Netty 非阻塞发请求,链尾订阅点)
  → 响应沿链反向回流到客户端

一句话

Spring Cloud Gateway 的源码主干只有三跳:DispatcherHandler 按顺序试 HandlerMapping,RoutePredicateHandlerMapping 用路由的 Predicate 顺序匹配、第一个命中的路由胜出(所以具体路由必须排在通配路由前面);FilteringWebHandler 把 GlobalFilter 和路由级 GatewayFilter 合并成一个列表按 order 统一排序,组成 DefaultGatewayFilterChain 责任链——所有"过滤器玄学"的答案都在 order 里:鉴权 order=-100 早于 StripPrefix(1)才能看到原始路径,order 相同相对顺序不稳定就是登录接口时好时坏 401 的根因,NettyRoutingFilter 固定在 Integer.MAX_VALUE 的链尾,它之前的 pre 逻辑才有效,它用 Reactor Netty 非阻塞发出请求,响应再由 order=-1 的 NettyWriteResponseFilter 沿链反向回写。读懂 WebFlux 只需记住三个心智模型:Mono 是组装后订阅才执行、短路就是不调 chain.filter()、整条链路靠 exchange 属性传上下文而不是 ThreadLocal——理解了"匹配按顺序、过滤器按 order、全程非阻塞"这三件事,网关 90% 的问题都能自己推导出来。

给团队的建议

建议
排障先开 gateway DEBUG 日志:看路由匹配结果和 Sorted gatewayFilterFactories
路由具体路径在前、通配在后;必要时显式 order
过滤器自定义 order 拉开间隔(鉴权 -100,业务 100+),不要挤 0
路径鉴权/白名单在 StripPrefix/RewritePath 之前判断,用原始路径
响应式阻塞调用包 subscribeOn(boundedElastic),绝不阻塞事件循环
body读取后用 cacheRequestBody/ModifyRequestBody,保证下游仍可收
测试针对过滤器顺序写集成测试(白名单路径 + StripPrefix 组合)

互动话题:你们在网关里遇到过过滤器顺序导致的诡异问题吗?鉴权逻辑放网关还是放各个服务内部?评论区聊聊。


参考资料


标题:Spring Cloud Gateway 源码拆解:一个请求从进入到转发经历了什么
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/19/1789226385866.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消