☰
Spring Cloud Gateway GatewayFilter 源码解析与责任链机制
2026/10/16 5:31:40 网站建设 项目流程

网关过滤器这个东西,平时写路由配置时天天见,什么StripPrefix=2、AddRequestHeader=X-Real-IP, 127.0.0.1,但很多人其实没认真想过这些配置背后到底是怎么变成一个个过滤器、又是怎么按照什么顺序跑起来的。这篇源码解析就是专门针对GatewayFilter这一层来写的,把接口定义、责任链构造、内置过滤器家族、自定义姿势和执行顺序全部过一遍。适合正在用 Spring Cloud Gateway 做网关改造的开发者,也适合想深入理解 WebFlux 响应式链路的人。

1. GatewayFilter 在网关架构里的位置

1.1 从一次请求的完整旅程说起

先抛开源码,从一次请求的视角看看网关里发生了什么。假设客户端发了一个/api/order/list的请求,网关拿到之后大致要经历这样几个阶段:先由RoutePredicateHandlerMapping根据配置的Predicate(比如Path=/api/order/**)找到匹配的Route;路由一旦确定,请求就交给FilteringWebHandler;这个 handler 会把当前路由上配置的过滤器列表取出来,再和全局过滤器合并排序,形成一个过滤器链;请求沿着这条链往下走,最后到达真正的下游服务。

所以 GatewayFilter 就是这条链上的一个节点,它的作用是在请求转发到下游之前、以及响应返回给客户端之前,做各种横切处理——加头、减前缀、限流、重试、熔断、改请求体,几乎你能想到的网关能力都是靠过滤器组合出来的。

注意:FilteringWebHandler并不是直接转发请求的组件,它把过滤器链执行完之后返回空信号,后续真正的转发动作由DispatcherHandler继续分发给对应的RoutingFilter(比如 NettyRoutingFilter、WebSocketRoutingFilter)去完成。

1.2 GatewayFilter 与 GlobalFilter 的分工逻辑

Spring Cloud Gateway 里其实有两套过滤器体系。一套是GatewayFilter,它是绑定在单个路由上的,只在匹配到该路由的请求上生效;另一套是GlobalFilter,它天然对所有请求生效,你可以把它理解成"全局拦路虎"。

这里有个容易混淆的点:很多GlobalFilter最终也会适配成GatewayFilter来参与链路执行。在FilteringWebHandler中,这两类过滤器会被合并到同一个列表中,然后排序、串成一条链。所以执行顺序上,全局过滤器和路由过滤器并没有绝对的先后边界,完全取决于各自的order值。

从职责划分的角度,GlobalFilter更适合做和具体业务路由无关的横切逻辑,比如负载均衡选择、路由前缀转换、请求日志、统一鉴权;GatewayFilter更适合做某个路由私有的定制逻辑,比如给某个服务单独加一个签名头、单独调整超时重试参数。明确这个边界之后,设计自定义过滤器时一上来就能判断该往哪个方向走。

2. GatewayFilter 接口源码精读与责任链设计

2.1 接口方法的含义

直接看接口源码,Spring Cloud Gateway 4.x 里的GatewayFilter长这样:

package org.springframework.cloud.gateway.filter; public interface GatewayFilter extends ShortcutConfigurable { String NAME_KEY = "name"; Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain); default GatewayFilter apply(ShortcutConfigurable config) { return this; } default String name() { return ""; } }

核心方法只有一个:filter(ServerWebExchange exchange, GatewayFilterChain chain)。参数里exchange是 WebFlux 中的请求上下文对象,里面可以拿到ServerHttpRequest、ServerHttpResponse、路由属性、请求属性等等;chain是过滤器链,调用chain.filter(exchange)表示把请求交给下一个过滤器,如果不调用它,请求就会在这里被拦截住。

apply(ShortcutConfigurable config)和name()这两个默认方法主要是给GatewayFilterFactory用的。当你通过配置工厂创建过滤器时,工厂会把配置项解析成具体的GatewayFilter实例,必要时可以用apply对过滤器做二次调整;name()用于给过滤器起个标识名,方便日志和排查。

在实际使用中,我们绝大多数时候并不直接实现GatewayFilter,而是继承AbstractGatewayFilterFactory或者AbstractNameValueGatewayFilterFactory,因为这样可以获得配置文件的快捷参数绑定能力。

2.2 FilteringWebHandler 与链的构造

过滤器链是怎么拼出来的?关键代码在FilteringWebHandler:

public Mono<Void> handle(ServerWebExchange exchange) { Route route = exchange.getRequiredAttribute(ServerWebExchangeUtils.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); }

逻辑很简单:从exchange里取出当前匹配到的Route,拿到路由上配置的gatewayFilters,然后和全局过滤器列表合并,排序后用DefaultGatewayFilterChain包装。

DefaultGatewayFilterChain的内部实现是典型的责任链模式:

private static class DefaultGatewayFilterChain implements GatewayFilterChain { private final int index; private final List<GatewayFilter> filters; public DefaultGatewayFilterChain(List<GatewayFilter> filters) { this.filters = filters; this.index = 0; } private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) { this.filters = parent.getFilters(); this.index = index; } @Override 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(); } }); } }

每次执行时,如果当前index还没到末尾,就取出对应下标位置的过滤器,把index + 1的新链作为参数传进去递归调用。index每进一层就加一,直到链全部走完。

这里有两个值得细品的点。第一,每递归一层就重建一个DefaultGatewayFilterChain对象,而不是原地修改状态,这保证了并发安全——同一份过滤器列表可以被无数并发请求共用,每个请求持有自己的index。第二,Mono.defer包装确保了整个执行过程是惰性的,真正订阅发生时才去取过滤器并执行,避免了启动期就提前创建无用的链路。

2.3 责任链为什么是 Mono 的天下

很多刚接触这个源码的人会问:为什么filter方法返回的是Mono<Void>而不是直接同步返回?这要从 WebFlux 的响应式编程模型说起。网关底层是 Netty 驱动的,所有 IO 操作都是非阻塞的。如果一个过滤器在业务逻辑中需要异步调用外部服务、等待响应、或者做异步限流,那么它必须返回一个可以代表"未来完成"的信号,Mono<Void>就是干这个用的。

chain.filter(exchange)返回的Mono<Void>会在整个下游链路完成后发出完成信号。所以你在自定义过滤器里可以很自然地这样写:

return chain.filter(exchange) .doFinally(signalType -> { // 链路完成后的清理逻辑 });

这个模型也带来一个惯性思维:不要在一个过滤器里用Thread.sleep或者阻塞调用,否则会把 Netty 的 IO 线程卡死。遇到需要同步等待的场景,要主动切换到boundedElastic调度器或者改用异步回调。

3. 内置 GatewayFilter 全家桶:功能与实现盘点

3.1 请求头、参数与响应头操作三件套

Spring Cloud Gateway 内置了几十种过滤器,其中操作头部和参数的几个是使用频率最高的。先看AddRequestHeaderGatewayFilterFactory,它配置起来非常简单:

filters: - AddRequestHeader=X-Request-Foo, Bar

对应的工厂会在apply方法里返回一个GatewayFilter,给请求头追加一个键值对。注意它用的是"追加"而不是"覆盖"。如果下游服务对同名请求头有去重逻辑,用SetRequestHeader更合适——这个过滤器会先移除同名头再写入新值。

和它类似的还有AddRequestParameterGatewayFilterFactory。这个过滤器给请求追加查询参数,同样适用于参数名冲突的场景,需要配合RemoveRequestParameter使用。

响应头方向有AddResponseHeader和SetResponseHeader,它们修改的是响应返回客户端前的头部信息。一个常见用法是给所有响应统一添加X-Response-Time或跨域头。但要注意,过滤器的执行时机在响应被写回之前,如果此时 Netty 已经开始写响应体,某些头部变更可能不生效,这个坑后面排查章节会展开。

3.2 路径操作家族:StripPrefix、RewritePath、PrefixPath

路径操作是网关路由配置里的重头戏。StripPrefixGatewayFilterFactory用来去掉路径中的前几段:

filters: - StripPrefix=2

假设请求路径是/api/v1/order/list,经过StripPrefix=2之后会变成/order/list。它的实现并不复杂,从 request URI 的 raw path 里按/切分,去掉前 N 段后重新构造 URI。这个过滤器通常配合服务拆分场景使用,比如网关统一走/api/{service}前缀,转发时再把服务前缀剥掉。

RewritePathGatewayFilterFactory则更灵活,它基于正则表达式做替换:

filters: - RewritePath=/api/v1/(?<segment>.*), /microservice-a/$\{segment}

这里用命名分组segment捕获剩余部分,替换成目标服务的路径。正则在 Java 里走的是Pattern.matcher().replaceAll(),所以需要注意正则转义和$引用。

PrefixPathGatewayFilterFactory是加前缀的,场景相对少一些。三者对比起来看:StripPrefix做减法,PrefixPath做加法,RewritePath做的是随心所欲的变换。路由设计阶段把这三者的差异梳理清楚,后面不会因为路径不匹配白排半天。

3.3 可靠性与流量控制:重试、限流、熔断与请求大小

先看重试,RetryGatewayFilterFactory是我在实际项目里比较谨慎对待的过滤器。配置项很多:retries表示重试次数,statuses指定哪些 HTTP 状态码触发重试,methods指定哪些 HTTP 方法允许重试,series按状态码分类(SERVER_ERROR、CLIENT_ERROR 等),还有backoff配置退避策略。

filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY methods: GET backoff: firstBackoff: 50ms maxBackoff: 500ms factor: 2

实现上,它会对exchange中的请求做客户端重放。这里有个很重要的约束:如果请求体已经在第一次发送时被消费掉了,重试时拿不到原始请求体,很多重试就失效了。所以当下游抖动严重时,最好只对 GET 这类幂等请求配置重试,POST 请求的重试要慎之又慎。

限流方面,内置过滤器是RequestRateLimiterGatewayFilterFactory,核心逻辑依赖RedisRateLimiter,实现的是令牌桶算法。配置时还需要指定KeyResolver来解析限流键(比如按 IP、按用户 ID)。它不是在网关 JVM 内存里做计数,而是把令牌桶的状态存在 Redis 里,这保证了多实例网关的限流数据是共享的。

熔断方面,新版本内置的是CircuitBreakerGatewayFilterFactory,底层基于 Resilience4j。它的关键配置是fallbackUri,熔断触发后请求会转发到本地 fallback 路由:

filters: - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order

请求大小限制由RequestSizeGatewayFilterFactory负责:

filters: - name: RequestSize args: maxSize: 1MB

超过大小直接返回413。它是通过检查Content-Length以及读取请求体数据的DataBuffer来判定超限的,所以不要和后续修改请求体的过滤器一起做无谓的叠加。

3.4 请求体/响应体修改的实现机制

修改请求体是网关里最"重"的操作之一。Spring Cloud Gateway 提供了一个ModifyRequestBodyGatewayFilterFactory,但它不像其他工厂那样在 YAML 里直接配个字符串就完事,需要注入一个RewriteFunction的 Bean 来写具体的转换逻辑:

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("modify-req-body", r -> r.path("/sendBody/**") .filters(f -> f.modifyRequestBody( String.class, Map.class, (exchange, originalBody) -> { // originalBody 就是原始请求体对象 return Mono.just(Map.of( "original", originalBody, "modified", true )); } )) .uri("http://example.com")) .build(); }

关键在于modifyRequestBody的三个类型参数:入参类型、出参类型、以及转换函数。框架会先读取完整的请求体,解码成你声明的入参类型(这里例子是String),调用函数转换成出参类型,然后重新编码成DataBuffer写回ServerHttpRequest。这个过程的开销不小,因为它涉及请求体的完整缓冲和重新序列化,流式场景下要评估性能。

响应体修改对应ModifyResponseBodyGatewayFilterFactory,用法类似。响应体修改最常见的坑是:如果下游返回的是二进制数据(比如文件流、图片),硬要按String解码转文本再转回来,数据基本就废了。所以做响应体修改前一定要确认 Content-Type 是文本类,并对图像、视频等类型直接放行。

4. 自定义 GatewayFilter 两种姿势与执行顺序控制

4.1 姿势一:实现 GatewayFilter 接口 + 编程式注册

如果不需要让过滤器从配置文件中读参数,实现GatewayFilter接口是最直观的方式。下面是一个带顺序控制的简单签名校验过滤器:

@Component public class AuthGatewayFilter implements GatewayFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (token != null && token.startsWith("Bearer ")) { // 可以在这里做 token 解析、校验,通过后放行 return chain.filter(exchange); } // 校验失败,直接写出 401 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public int getOrder() { return -100; } }

这里返回exchange.getResponse().setComplete()表示响应已经完成,后续过滤器链不会再执行。在我实际项目里,鉴权类过滤器往往要放在最前面,所以 order 给一个比较小的负数。

如果想要把这个过滤器绑定到某个特定路由,可以用RouteLocatorBuilder编程式路由:

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder, AuthGatewayFilter authFilter) { return builder.routes() .route("secure-route", r -> r.path("/secure/**") .filters(f -> f.filter(authFilter)) .uri("http://backend-service")) .build(); }

这种方式的优点是类型安全、逻辑清晰,缺点是路由配置从 YAML 挪到了代码里,对喜欢集中管理配置的团队来说可能不那么友好。

4.2 姿势二:继承 AbstractGatewayFilterFactory 写配置化过滤器

想保持 YAML 配置风格,那就要写一个GatewayFilterFactory。最常见的实现是继承AbstractGatewayFilterFactory<Config>:

@Component public class CustomHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomHeaderGatewayFilterFactory.Config> { public CustomHeaderGatewayFilterFactory() { super(Config.class); } @Override public List<String> shortcutFieldOrder() { // 决定 YAML 里用简写模式时参数的顺序 return Arrays.asList("name", "value"); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest().mutate() .header(config.getName(), config.getValue()) .build(); return chain.filter(exchange.mutate().request(request).build()); }; } public static class Config { private String name; private String value; // getter/setter 省略 } }

配置文件中就可以这样用:

filters: - CustomHeader=username,zhangsan

注意两点。第一,shortcutFieldOrder定义了简写模式下参数和 Config 字段的映射顺序;如果不声明,则按 Config 字段顺序来。第二,Config必须是静态内部类,且字段有 getter/setter,否则配置绑定会失败。

我个人的习惯是:业务无关、可复用的过滤器尽量写成配置化过滤器;业务强相关、只在一两个路由里用的一次性逻辑,直接用编程式GatewayFilter更省事。

4.3 order 值的计算规则与踩坑

所有过滤器合并后都会经过AnnotationAwareOrderComparator.sort(combined)排序。这里有两个容易踩的坑。

第一个坑:OrderedGatewayFilter包装。路由里的过滤器配置定义通过RouteDefinitionRouteLocator加载时,如果某个GatewayFilterFactory返回的过滤器没有实现Ordered,框架会把它包装成OrderedGatewayFilter(gatewayFilter, 0)。也就是说,大部分路由配置过滤器默认的 offer 值是 0。

第二个坑:同 order 时的排序稳定性。如果全局过滤器和路由过滤器恰好都是 order=0,那排序结果取决于合并列表里元素的先后顺序。FilteringWebHandler中合并列表的顺序是先全局过滤器,后路由过滤器,所以同 order 下全局过滤器优先于路由过滤器。别依赖这个隐性规则,想控制顺序就老老实实给出不同的 order 值。

我在真实项目里见过一种诡异现象:自定义拦截器明明写在路由前面,却总是比AddRequestHeader后执行,原因就是这个自定义过滤器没有实现Ordered,默认 order=0,而另外一个过滤器也是 order=0,结果被合并顺序压过了。排查这类问题最快的办法是在自定义过滤器里打个日志,看执行时序,然后显式给 order。

5. 实战排查:过滤器不生效、顺序错乱、修改不了请求体的排障记录

5.1 过滤器没执行的排查路径

先收到的问题是过滤器整个没执行。这种场景我一般按下面这条路走。

第一步,确认路由确实匹配到了。如果请求没有匹配到对应的Predicate,路由都不会选中,路由过滤器自然不执行。可以开网关的 trace 日志,观察RoutePredicateHandlerMapping是否打印了匹配成功的路由 id。

第二步,确认过滤器名字写对了。Gateway 的FilterDefinition解析依赖快捷类名或全限定类名。比如AddRequestHeader会去找AddRequestHeaderGatewayFilterFactory,如果类名拼错,启动时就会报找不到工厂。检查配置最简单的方式是看启动日志,路由加载阶段如果有无法识别的过滤器,会在日志里直接抛出异常。

第三步,确认全局过滤器和路由过滤器是否被合并排序时排到了后面,导致看起来"没执行"。举个例子,某个全局日志过滤器 order=10000,路由里的业务过滤器 order=0,如果日志过滤器只在 doFinally 里打印,你可能会觉得它没执行,其实它执行了,只是时机在最后。

5.2 顺序错乱与全局过滤器叠加

顺序错乱的问题在引入了多个自定义过滤器之后会明显暴露。有一个我印象比较深的场景:某次网关改造要做一个统一的"接口耗时统计"过滤器,同时又有一个路由级的"加请求头"过滤器。期望是统计过滤器把耗时算准,结果统计结果总是把下游服务响应时间之外的部分漏掉了。

最后查下来是 order 设计有问题。耗时统计过滤器应该是"洋葱最外层",也就是 order 值要足够小,这样它会最先进入链路,最后退出链路;而加请求头的过滤器 order 更大,在链路内层执行。换言之,过滤器执行时机和数据统计口径强相关,不要随手给 order,先画一遍链路时序再定值。

我自己的经验法则:全局横切逻辑(日志、鉴权、统计)用负 order,路由业务级改动用正 order 或默认 0。如果需要在网关入口最先执行,给Ordered.HIGHEST_PRECEDENCE即Integer.MIN_VALUE附近的值。

5.3 修改请求体时的经典坑

请求体修改过滤器是踩坑重灾区。很多人在modifyRequestBody里遇到回调不触发的诡异情况,最典型的根因是上游请求体已经被前面的过滤器消费了。比如你先配置了RequestSize或者某些做 body 校验的过滤器,它内部已经读过DataBuffer,但没有把 body 重新塞回exchange,等ModifyRequestBody再去读的时候就只剩空流。

这时候排查手段是把过滤器执行顺序打出来,然后检查哪些过滤器订阅了请求体。一般来说,ModifyRequestBody应该放在链路中尽量靠前的位置,且前后不要同时存在其他消费 body 的过滤器。

另外一个坑是修改请求体后没有同步修改Content-Length头。Spring Cloud Gateway 在转发请求时,有些情况下会基于请求体实际大小重新计算 Content-Length,但如果你是在转发路径NettyRoutingFilter之前手动改了请求体且没有更新头信息,下游服务可能因为长度不匹配而读取失败。虽然框架的ServerHttpRequestDecorator内部做了很多补偿逻辑,但自己改 body 时依然要留意 Content-Length 是否被正确更新。

和响应体修改相关的坑也有一个值得提:ModifyResponseBody依赖响应体缓冲,如果下游返回的是大文件或流式响应,整个响应体都会被缓存进内存,对网关内存压力非常大。遇到大文件场景,建议跳过该过滤器或者只在特定 Content-Type 下启用。

6. 过滤器源码阅读与二次开发的建议

从投入产出比来说,读GatewayFilter这一层源码,比直接把整个 Spring Cloud Gateway 源码通读一遍要更划算。因为你日常 80% 的定制需求都发生在过滤器层,读透之后要么自己写过滤器,要么调整过滤器顺序,都能直接落地。我建议按这个顺序来读:

先读GatewayFilter、GatewayFilterChain、FilteringWebHandler、DefaultGatewayFilterChain,把责任链模型吃透;然后挑几个高频内置过滤器工厂读实现,比如AddRequestHeader、StripPrefix、RewritePath;之后再看AbstractGatewayFilterFactory的配置绑定逻辑和ShortcutConfigurable的快捷参数解析。

如果要在项目里大量使用自定义过滤器,强烈建议把网关的请求日志打到结构化文件里。每条请求记录:匹配到的 route id、过滤器链中的 order 序列、每个过滤器的执行耗时。这样当过滤器行为异常时,你能在五分钟内定位到是哪一环出了问题,而不是靠脑内推演链路的执行顺序。

最后说一个我自己的体会:过滤器越多,网关链路越重,排查复杂度呈指数上升。能用内置过滤器解决的需求,就不要自己重复造轮子;同时给每个自定义过滤器都留下清晰的 order 注释和用途说明,不然接手你网关代码的同事大概率会在半年后对着十几个过滤器一脸茫然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询