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、责任链模式