← 返回博客
2026-10-21 08:00:01

Spring Cloud Gateway 源码解析:过滤器链的组装与执行

Spring Cloud Gateway 源码解析:过滤器链的组装与执行

先看一个真实场景。线上网关配置了三条路由,某条路由上挂了一个自定义鉴权过滤器,一个限流过滤器,外加全局的日志过滤器。某天发现:鉴权失败了,日志过滤器仍然打印了耗时;限流触发了,响应却经过了日志过滤器两次。这类“过滤器执行顺序不受控”的问题,几乎每个用 Spring Cloud Gateway 的项目都会踩一次。

要搞清楚原因,得先知道一件事:Gateway 的过滤器链不是 Spring MVC 那种 Filter 链,它是两条独立的链拼起来的——全局过滤器链(GlobalFilter)和路由过滤器链(GatewayFilter)。两者怎么排序、怎么合并、异常怎么冒泡,全在源码里。

一、请求进来之后,链是怎么被拼出来的

请求先到 DispatcherHandler,再进 RoutePredicateHandlerMapping 匹配路由,最后落到 FilteringWebHandler。这个 Handler 是整条链的装配车间,核心就一个方法:

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);
    return new DefaultGatewayFilterChain(combined).filter(exchange);
}

这段代码信息量很大。globalFilters 来自容器里所有 GlobalFilter 的适配包装,route.getFilters() 是路由配置上的过滤器。两者合并成一个大 List,然后统一用 AnnotationAwareOrderComparator.sort 排序。

关键点有两个。第一,全局过滤器和路由过滤器是同一个池子里排序的,没有“全局先跑、路由后跑”的硬规则,谁 Order 小谁先跑。第二,route.getFilters() 里的顺序不代表执行顺序,配置里写的先后会被 Order 值覆盖。很多“我的鉴权过滤器没拦住请求”的坑,就是自定义过滤器没实现 Ordered,默认拿到 LOWEST_PRECEDENCE,排到了最后。

容易踩坑的地方:GlobalFilter 适配成 GatewayFilter 时,Order 取的是 GlobalFilter 自身的 getOrder();而路由过滤器如果来自 FilterDefinition,Order 是按配置列表下标生成的。两种来源的 Order 语义不一样,混在一起排序时容易错位。

二、链的执行:责任链 + 洋葱模型的混合体

DefaultGatewayFilterChain 是个标准的责任链实现,但它的 filter 方法里藏着一个洋葱结构:

public Mono<Void> filter(ServerWebExchange exchange) {
    return Mono.defer(() -> {
        if (this.index < filters.size()) {
            GatewayFilter filter = filters.get(this.index);
            DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
            return filter.filter(exchange, chain);
        } else {
            return Mono.empty();
        }
    });
}

每次调用都新建一个 DefaultGatewayFilterChain,index + 1 指向下一个过滤器。这是典型的链表式责任链,每个节点只关心“我处理完,把 exchange 交给下一个”。

但注意 filter.filter(exchange, chain) 的写法——过滤器拿到 chain 之后,可以选择在调用 chain.filter(exchange) 之前做事(pre),也可以 .then(...) 之后做事(post)。这就是洋葱模型:

请求 → 过滤器A.pre → 过滤器B.pre → 过滤器C.pre → 目标服务
                                                      ↓
响应 ← 过滤器A.post ← 过滤器B.post ← 过滤器C.post ← 响应

NettyRoutingFilter 在链尾发起真实 HTTP 请求,它前面的过滤器都是 pre 逻辑,后面的 .then 回调才是 post 逻辑。这就是为什么日志过滤器如果写成 chain.filter(exchange).then(Mono.fromRunnable(...)),耗时统计才是准的;如果直接 return chain.filter(exchange) 再在方法末尾打日志,打出来的是“发起请求的时间”,不是“响应回来的时间”。

三、异常和短路:为什么鉴权失败还会走日志

上一节说日志过滤器要在 .then 里做 post 逻辑。但鉴权过滤器如果直接 return exchange.getResponse().setComplete(),链就断了,后面的 chain.filter 根本不会调用。这时候日志过滤器的 .then 不会执行,可如果日志过滤器写成了“方法体末尾打日志”,照样会打印——这就是开头那个“鉴权失败日志还打印耗时”的根源。

源码层面,短路的本质是过滤器没有调用 chain.filter(exchange),直接返回一个 Mono。DefaultGatewayFilterChain 的递归到此为止,后续节点全部跳过。这是设计上的取舍:短路必须由过滤器自己负责,框架不做兜底。

异常的处理则交给 ExceptionHandler 体系的 ErrorWebExceptionHandler。过滤器抛出的异常会沿 Mono 的错误通道冒泡,最终被 DefaultErrorWebExceptionHandler 捕获。这里有个细节:NettyRoutingFilter 内部对连接异常做了 onErrorResume,会把异常转成 5xx 响应,而不是让它继续往上冒。所以你在自定义过滤器里 try-catch 到的异常,和最终用户看到的错误码,可能不是一回事。

四、这套设计解决了什么

回头看,Spring Cloud Gateway 没有用 Spring MVC 的 Filter,而是自建两条链合并排序,图的是两件事:一是全局过滤器和路由过滤器能自由混排,业务方不用关心“我这是全局的还是路由的”,只关心 Order;二是洋葱模型让 pre/post 逻辑写在同一个过滤器里,比 Servlet Filter 的 chain.doFilter 前后夹逻辑更直观。

代价也明显:排序完全依赖 Order,配置顺序形同虚设;短路逻辑没有框架兜底,全靠过滤器自觉。这两点恰恰是线上问题的高发区。

给一个可操作的建议:打开项目里所有自定义 GatewayFilter,逐个确认是否实现了 Ordered,把鉴权类放在 -100 到 0,日志类放 Integer.MAX_VALUE,限流类夹在中间。改完跑一遍“鉴权失败”和“限流触发”两个用例,看日志过滤器是否只打印一次。这一步做完,链的顺序问题基本就清干净了。

本文关键词:Spring Cloud Gateway、过滤器链、GlobalFilter、GatewayFilter、责任链模式