资讯动态

Spring Cloud Gateway过滤器执行顺序源码解析与实战调优

发布时间:2026/10/17 23:05:08 来源:尧图企业网站定制
1. 网关过滤器链为何成为微服务流量的咽喉微服务架构里网关是流量的第一道关卡。所有外部请求先到网关再由网关转发到后端各个服务。Spring Cloud Gateway 作为 Spring 生态里的第二代网关组件底层基于响应式编程模型和第一代 Zuul 1.x 的阻塞式模型有本质区别。这个区别直接决定了过滤器执行顺序的设计逻辑也是很多人在实际使用中踩坑的根源。我接触过的项目中网关层出问题的场景特别集中鉴权过滤器没生效、限流过滤器排在路由之后、日志过滤器拿不到响应体、跨域配置被自定义过滤器覆盖。这些问题追到根上十有八九是过滤器执行顺序没搞明白。Spring Cloud Gateway 的过滤器不是简单地按你写的顺序执行它有一套自己的排序机制涉及Ordered接口、Order注解、OrderedGatewayFilter包装类还有GlobalFilter和GatewayFilter两套体系的优先级关系。这篇文章面向的是已经在用 Spring Cloud Gateway、但对其过滤器执行顺序感到困惑的开发者。我会从源码层面拆解排序原理结合我实际调试过的案例把 pre 和 post 两个阶段的执行链路讲清楚。读完你至少能搞明白三件事为什么你的过滤器没按预期顺序跑、怎么精确控制过滤器的执行位置、以及遇到顺序问题时该从哪里下手排查。提示本文基于 Spring Cloud Gateway 3.x 和 4.x 的通用机制分析不同小版本在细节上可能有差异但核心排序逻辑一致。2. 过滤器体系的全景拆解与排序机制2.1 两套过滤器体系的本质区别Spring Cloud Gateway 里的过滤器分两大类GatewayFilter和GlobalFilter。这两个接口名字看着像但定位完全不同。GatewayFilter是路由级别的你可以在路由配置里针对某一条路由单独指定。比如给/api/order/**这条路由加一个鉴权过滤器给/api/user/**加一个日志过滤器。它的作用范围是局部的配置方式通常是通过RouteLocatorBuilder或者 YAML 文件。GlobalFilter是全局级别的一旦注册到容器里对所有路由都生效。它不需要在路由配置里显式声明Spring 容器扫描到就自动装配。典型的全局过滤器包括NettyRoutingFilter、NettyWriteResponseFilter、ForwardRoutingFilter这些框架自带的以及你自己实现的鉴权、限流、日志过滤器。关键点来了这两套过滤器最终会被合并到同一个链里执行。合并的地方在FilteringWebHandler这个类里。它会把所有GlobalFilter和当前路由匹配到的GatewayFilter收集起来统一排序然后逐个执行。所以你不能孤立地看某一类过滤器的顺序必须放在合并后的链里分析。2.2 Ordered 接口与 Order 注解的排序规则排序的核心是Ordered接口。这个接口只有一个方法getOrder()返回一个 int 值。值越小优先级越高越先执行。这是 Spring 框架里通用的排序约定Spring Cloud Gateway 沿用了这套规则。GlobalFilter接口本身继承了Ordered接口所以每个全局过滤器都必须实现getOrder()方法。你可以直接实现这个方法返回一个固定值也可以用Order注解标注在类上。两种方式同时存在时getOrder()方法的返回值优先级更高Order注解会被忽略。GatewayFilter接口本身没有继承Ordered但框架提供了一个OrderedGatewayFilter包装类。当你在配置里指定order参数时框架会用这个包装类把你的过滤器包一层把 order 值传进去。如果你没指定 order那这个GatewayFilter在排序时会被当作 order 值为 0 处理这往往就是顺序混乱的源头。我见过一个典型的坑某开发者在路由配置里加了一个自定义的GatewayFilter没设 order同时项目里有一个全局的鉴权过滤器 order 设成了 100。结果自定义过滤器跑在鉴权之前请求还没鉴权就被处理了。原因就是没设 order 的GatewayFilter默认排在最前面。2.3 合并排序的源码级流程FilteringWebHandler的handle方法里排序逻辑大致是这样的ListGatewayFilter gatewayFilters route.getFilters(); ListGatewayFilter combined new ArrayList(this.globalFilters); combined.addAll(gatewayFilters); AnnotationAwareOrderComparator.sort(combined);这段代码做了三件事先把全局过滤器放进一个列表再把路由级别的过滤器追加进去最后用AnnotationAwareOrderComparator排序。这个 Comparator 会先看对象有没有实现Ordered接口有就用getOrder()的值没有就看有没有Order注解都没有就返回默认值Ordered.LOWEST_PRECEDENCE也就是 Integer.MAX_VALUE。这里有个细节值得注意全局过滤器和路由过滤器在合并前是两个独立的列表合并后统一排序。所以一个 order 为 10 的全局过滤器和一个 order 为 5 的路由过滤器排序后路由过滤器在前。它们之间没有体系上的优先级差异完全看 order 值。2.4 框架自带过滤器的 order 值分布理解框架自带过滤器的 order 值对自定义过滤器的定位至关重要。下面这张表是我从源码里整理出来的常用过滤器 order 值过滤器类名order 值作用阶段功能说明AdaptCachedBodyGlobalFilterHIGHEST_PRECEDENCE 1000pre缓存请求体ForwardPathFilter10000pre处理 forward 路径RouteToRequestUrlFilter10000pre构建目标 URLNettyRoutingFilterLOWEST_PRECEDENCEpre实际发起后端请求NettyWriteResponseFilter-1post写回响应ForwardRoutingFilterLOWEST_PRECEDENCEpreforward 路由处理HIGHEST_PRECEDENCE是Integer.MIN_VALUELOWEST_PRECEDENCE是Integer.MAX_VALUE。NettyWriteResponseFilter的 order 是 -1这个值很关键它决定了响应写回发生在大多数过滤器的 post 阶段之前。注意NettyRoutingFilter的 order 是LOWEST_PRECEDENCE意味着它几乎排在最后。你的自定义过滤器如果 order 值比它还大那就会在它之后执行但那时候请求已经发出去了pre 阶段的操作就没意义了。3. pre 与 post 两阶段执行链的深度剖析3.1 责任链模式下的两阶段执行Spring Cloud Gateway 的过滤器链用的是责任链模式但和传统的责任链有个重要区别它把每个过滤器的执行分成了 pre 和 post 两个阶段。pre 阶段是filter方法中chain.filter(exchange)调用之前的代码post 阶段是调用之后的代码。public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { // pre 阶段逻辑 return chain.filter(exchange).then(Mono.fromRunnable(() - { // post 阶段逻辑 })); }这个设计带来的效果是pre 阶段的执行顺序和过滤器排序一致从小到大post 阶段的执行顺序正好相反从大到小。因为 post 阶段是嵌套在then里的外层过滤器的 post 要等内层过滤器的 post 执行完才轮到。我用一个具体例子说明。假设有三个过滤器 A、B、Corder 值分别是 1、2、3。执行流程是这样的A 的 pre 执行B 的 pre 执行C 的 pre 执行C 的 post 执行B 的 post 执行A 的 post 执行这个顺序很像栈的入栈出栈。理解这一点你就能明白为什么日志过滤器如果想记录响应体必须把 order 设得足够小让它的 post 阶段在NettyWriteResponseFilter之后执行。3.2 NettyRoutingFilter 作为分界线的意义NettyRoutingFilter是整个过滤器链的分水岭。它的 order 是LOWEST_PRECEDENCE在所有过滤器中几乎排最后。它的 pre 阶段负责把请求真正转发到后端服务post 阶段负责处理响应。这意味着order 值小于LOWEST_PRECEDENCE的过滤器pre 阶段在请求转发前执行post 阶段在响应回来后执行。order 值等于LOWEST_PRECEDENCE的过滤器如果排在NettyRoutingFilter之后那它的 pre 阶段执行时请求已经发出去了只能做一些收尾工作。实际项目中我建议把自定义过滤器的 order 值控制在 -100 到 10000 之间。太小了可能被框架过滤器覆盖太大了可能跑到NettyRoutingFilter后面去。这个区间是个比较安全的范围。3.3 响应体处理的顺序陷阱响应体处理是顺序问题的高发区。很多人想写一个过滤器记录响应内容结果发现拿到的响应体是空的。原因通常是过滤器 order 值太大post 阶段执行时NettyWriteResponseFilter已经把响应写回客户端了响应体被消费掉了。正确的做法是把 order 值设成负数比如 -2 或 -10确保你的 post 阶段在NettyWriteResponseFilterorder 为 -1之后执行。但这里还有个坑响应体是响应式流只能被消费一次。如果你想读取响应体必须用ServerHttpResponseDecorator包装原始响应在writeWith方法里拦截数据缓冲区。public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpResponse originalResponse exchange.getResponse(); DataBufferFactory bufferFactory originalResponse.bufferFactory(); ServerHttpResponseDecorator decoratedResponse new ServerHttpResponseDecorator(originalResponse) { Override public MonoVoid writeWith(Publisher? extends DataBuffer body) { if (body instanceof Flux) { Flux? extends DataBuffer fluxBody (Flux? extends DataBuffer) body; return super.writeWith(fluxBody.map(dataBuffer - { byte[] content new byte[dataBuffer.readableByteCount()]; dataBuffer.read(content); DataBufferUtils.release(dataBuffer); // 在这里处理 content return bufferFactory.wrap(content); })); } return super.writeWith(body); } }; return chain.filter(exchange.mutate().response(decoratedResponse).build()); }这段代码的关键是exchange.mutate().response(decoratedResponse).build()它把包装后的响应替换回 exchange后续过滤器拿到的就是包装过的响应。这个操作必须在 pre 阶段完成所以过滤器的 order 值要足够小确保在NettyWriteResponseFilter之前执行。3.4 鉴权过滤器的顺序定位鉴权过滤器是另一个对顺序敏感的场景。它必须在请求转发到后端之前完成校验所以 order 值要小于NettyRoutingFilter的LOWEST_PRECEDENCE。但具体设多少要看它和其他过滤器的依赖关系。如果鉴权需要读取请求体比如验证签名那 order 值要大于AdaptCachedBodyGlobalFilter的HIGHEST_PRECEDENCE 1000因为请求体缓存是在那个过滤器里做的。如果鉴权需要用到路由信息那 order 值要大于RouteToRequestUrlFilter的 10000。我一般把鉴权过滤器的 order 设成 100 左右。这个值足够小能保证在请求转发前执行又足够大能保证在请求体缓存和路由解析之后执行。当然具体值要根据项目里其他过滤器的 order 分布来调整。4. 自定义过滤器顺序控制的实操方案4.1 全局过滤器的 order 设置方式实现GlobalFilter接口时有两种方式设置 order。第一种是直接实现getOrder()方法Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 鉴权逻辑 return chain.filter(exchange); } Override public int getOrder() { return 100; } }第二种是用Order注解Component Order(100) public class AuthGlobalFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange); } }两种方式效果一样但第一种更灵活因为getOrder()方法可以返回动态计算的值。比如根据配置中心下发的参数决定 order 值实现运行时调整。第二种方式的值是编译期固定的改起来要重新打包。提示如果同时用了两种方式getOrder()方法的返回值会覆盖Order注解的值。这个行为容易让人困惑建议一个项目里统一用一种方式。4.2 路由级过滤器的 order 配置路由级GatewayFilter的 order 配置稍微复杂一些。如果你是通过 YAML 配置的可以在过滤器参数里加orderspring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 order: 50这个order参数会被框架用来构造OrderedGatewayFilter。如果你是通过 Java 代码配置的可以用OrderedGatewayFilter显式包装Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(order-service, r - r .path(/api/order/**) .filters(f - f .filter(new OrderedGatewayFilter(new CustomFilter(), 50)) ) .uri(lb://order-service)) .build(); }这里有个容易忽略的点OrderedGatewayFilter的 order 值只在路由过滤器之间比较时有效和全局过滤器比较时还是看合并后的排序结果。所以不要以为给路由过滤器设了 order 就万事大吉还要考虑全局过滤器的 order 分布。4.3 用配置中心动态调整 order 值生产环境里过滤器顺序可能需要根据流量情况动态调整。比如大促期间想把限流过滤器提前平时可以往后放。这时候硬编码 order 值就不合适了需要借助配置中心。思路是把 order 值抽到配置里过滤器实现ApplicationListener监听配置变更事件动态更新 order 值。但这里有个限制getOrder()方法在过滤器链构建时就被调用了运行时改 order 值不会影响已经构建的链。所以动态调整需要配合路由刷新机制让网关重新构建过滤器链。Component public class DynamicOrderFilter implements GlobalFilter, Ordered, ApplicationListenerEnvironmentChangeEvent { private volatile int order 100; Override public int getOrder() { return order; } Override public void onApplicationEvent(EnvironmentChangeEvent event) { // 从配置中心读取新的 order 值 this.order getOrderFromConfig(); } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange); } }这个方案在实际项目里用过效果不错但要注意并发问题。order字段用volatile修饰保证可见性。配置变更事件触发后需要调用RefreshRoutesEvent让网关重新构建路由和过滤器链。4.4 顺序验证的调试手段写完过滤器后怎么验证顺序对不对我常用的方法是在每个过滤器的 pre 和 post 阶段打日志日志里带上过滤器名字和 order 值。启动网关后发一个请求看日志输出顺序。Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info({} pre, order{}, getClass().getSimpleName(), getOrder()); return chain.filter(exchange).then(Mono.fromRunnable(() - { log.info({} post, order{}, getClass().getSimpleName(), getOrder()); })); }日志会清晰地展示 pre 阶段从大到小、post 阶段从小到大的执行顺序。如果发现某个过滤器的位置不对对照前面那张 order 值分布表调整它的 order 值。另一个手段是用 Spring Boot Actuator 的/actuator/gateway/routes端点查看每条路由上挂载的过滤器列表。这个端点返回的是排序后的结果能直观看到过滤器的最终顺序。不过要注意这个端点展示的是路由级过滤器全局过滤器不在里面需要结合日志一起看。5. 顺序错乱的典型故障与排查实录5.1 鉴权过滤器被绕过的排查过程某次项目上线后安全测试反馈说部分接口没鉴权就能访问。排查发现鉴权过滤器确实注册了但只对部分路由生效。进一步看日志发现鉴权过滤器的 pre 阶段在某些请求里根本没执行。原因是有个自定义的GatewayFilter在路由配置里没设 order默认排在了鉴权过滤器前面。这个过滤器做了请求转发请求转发后chain.filter的后续链路就断了鉴权过滤器自然没机会执行。解决办法是给那个GatewayFilter显式设置 order 值让它排在鉴权过滤器之后。同时给鉴权过滤器加了一个兜底逻辑在 pre 阶段检查请求是否已经转发如果已转发就直接拒绝。这个兜底逻辑后来救过好几次场。注意任何做请求转发的自定义过滤器order 值一定要设得比鉴权过滤器大否则鉴权形同虚设。5.2 响应日志丢失的定位思路有个需求是记录所有接口的响应体和响应时间。开发者写了个全局过滤器order 设成 0结果响应体记录不到响应时间也不准。响应体记录不到是因为 order 为 0 的过滤器 post 阶段在NettyWriteResponseFilterorder 为 -1之前执行那时候响应还没写回拿不到数据。响应时间不准是因为 post 阶段执行太早没算上响应写回的时间。调整方案是把 order 改成 -2让 post 阶段在NettyWriteResponseFilter之后执行。同时用ServerHttpResponseDecorator包装响应在writeWith里拦截数据。改完后响应体和响应时间都能正确记录了。这个案例说明post 阶段的过滤器 order 值要仔细设计。做响应处理的过滤器order 值要小于 -1做请求处理的过滤器order 值要大于 0。两者不要混在一起。5.3 跨域配置失效的原因分析跨域问题是网关层的常见故障。Spring Cloud Gateway 提供了CorsWebFilter来处理跨域但它是个WebFilter不是GatewayFilter不参与网关过滤器链的排序。问题出在自定义过滤器上。如果自定义过滤器的 order 值比CorsWebFilter小pre 阶段先执行可能会修改请求头导致跨域校验失败。更常见的情况是自定义过滤器直接返回了响应比如鉴权失败没有走CorsWebFilter的处理逻辑响应里缺少跨域头。解决办法有两个一是把自定义过滤器的 order 值调大让CorsWebFilter先处理二是在自定义过滤器返回响应时手动添加跨域头。我一般用第一种方案简单可靠。5.4 常见顺序问题速查表问题现象可能原因排查方法解决方案鉴权过滤器不生效被其他过滤器提前转发看日志确认 pre 阶段是否执行调整 order 值加兜底逻辑响应体记录为空post 阶段执行太早检查 order 是否小于 -1order 设为负数用 ResponseDecorator限流过滤器排在路由后order 值太大对比 NettyRoutingFilter 的 orderorder 设为 10000 以下跨域头缺失自定义过滤器提前返回检查响应头调大 order 或手动加跨域头请求体读取失败请求体缓存过滤器未执行检查 AdaptCachedBodyGlobalFilter 位置order 设为 HIGHEST_PRECEDENCE 1000 之后过滤器执行顺序与预期不符全局和路由过滤器混排打印合并后的链统一规划 order 值分布5.5 我踩过的几个坑第一个坑是Order注解和getOrder()方法混用。有个项目里一个过滤器同时用了两种方式Order(50)和getOrder()返回 100。结果实际生效的是 100Order被忽略了。这个行为在 Spring 文档里有说明但很容易忽略。后来我定了个规矩一个项目里只用一种方式要么全用注解要么全实现接口。第二个坑是路由过滤器的默认 order。没设 order 的GatewayFilter默认值是 0这个值比很多全局过滤器都小导致路由过滤器跑在全局过滤器前面。有次一个路由过滤器做了请求参数校验跑在鉴权之前未鉴权的请求也能触发校验逻辑浪费了资源。后来给所有路由过滤器都显式设了 order。第三个坑是NettyWriteResponseFilter的 order 值。我一直以为它是LOWEST_PRECEDENCE后来看源码发现是 -1。这个值意味着它的 post 阶段在大多数过滤器之前执行。如果你想在响应写回后做点什么order 值必须小于 -1。这个细节在官方文档里没写清楚是我调试时发现的。6. 顺序设计的工程化建议6.1 建立 order 值分配规范项目里过滤器多了以后order 值容易冲突。建议在项目初期就定一套分配规范比如-100 到 -1响应处理类过滤器0 到 99请求预处理类过滤器100 到 999鉴权、限流类过滤器1000 到 9999业务逻辑类过滤器10000 以上路由转发类过滤器每个区间留出足够的空隙方便后续插入新过滤器。这个规范写进项目文档新加过滤器时按区间取值避免冲突。6.2 过滤器职责单一化一个过滤器只做一件事。我见过一个过滤器同时做了鉴权、限流、日志三件事order 值很难调。调小了日志记录不全调大了鉴权太晚。后来拆成三个过滤器各自设 order问题迎刃而解。职责单一还有个好处是复用。鉴权过滤器可以复用到多个路由日志过滤器可以全局生效互不干扰。拆开以后每个过滤器的 order 值只需要考虑自己的依赖关系不用兼顾其他功能。6.3 用测试保障顺序正确过滤器顺序是运行时行为单元测试不好覆盖。我一般用集成测试来验证启动一个轻量级网关配置好过滤器发请求后断言执行顺序。Test public void testFilterOrder() { // 启动网关发请求 // 断言日志里 pre 阶段顺序为 A - B - C // 断言 post 阶段顺序为 C - B - A }这个测试跑起来有点重但能有效防止顺序回归。每次调整过滤器 order 值后跑一遍心里有底。6.4 监控过滤器执行耗时顺序问题有时表现为性能问题。某个过滤器 order 值不对导致它执行时依赖的数据还没准备好走了慢路径。给每个过滤器加上耗时监控能间接发现顺序异常。实现方式是在 pre 阶段记录开始时间post 阶段计算耗时打到监控系统里。如果某个过滤器的耗时突然变大可能是它前面的过滤器顺序变了导致它拿到的数据状态不同。Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { long start System.currentTimeMillis(); return chain.filter(exchange).then(Mono.fromRunnable(() - { long cost System.currentTimeMillis() - start; metrics.record(getClass().getSimpleName(), cost); })); }这个监控方案在实际项目里帮我发现过好几次顺序问题。有一次限流过滤器的耗时从 1ms 涨到 50ms排查发现是它前面的鉴权过滤器 order 值被改了导致限流过滤器执行时请求已经转发走了降级逻辑。6.5 文档化过滤器链项目里的过滤器链应该有一份文档列出每个过滤器的名字、order 值、职责、依赖关系。这份文档在排查问题时特别有用不用每次都去翻源码。文档格式可以简单点一个表格就够过滤器名order阶段职责依赖AuthFilter100pre鉴权无RateLimitFilter200pre限流AuthFilterLogFilter-2post日志NettyWriteResponseFilter这份文档跟着代码一起维护改 order 值时同步更新。新同事接手时看这份文档能快速理解过滤器链的设计意图。6.6 版本升级时的顺序兼容性检查Spring Cloud Gateway 不同版本之间框架自带过滤器的 order 值可能有变化。升级版本时要检查自定义过滤器的 order 值是否还合理。我遇到过一次升级后鉴权失效的问题。排查发现新版本里RouteToRequestUrlFilter的 order 值从 10000 改成了 5000而鉴权过滤器的 order 是 6000升级后跑到了路由解析之前拿不到路由信息。把鉴权过滤器的 order 改成 4000 后恢复正常。升级前建议做两件事一是对比新旧版本框架过滤器的 order 值二是跑一遍集成测试验证顺序。这两步能避免大部分升级导致的顺序问题。7. 从源码看排序的边界情况7.1 order 值相同时的排序行为如果两个过滤器的 order 值相同排序结果是什么AnnotationAwareOrderComparator在 order 值相同时会保持它们的相对顺序也就是稳定排序。但相对顺序取决于它们加入列表的顺序全局过滤器先加入路由过滤器后加入。所以 order 相同的全局过滤器和路由过滤器全局过滤器在前。这个行为在大多数情况下没问题但如果你依赖 order 相同来保证某种顺序那就危险了。建议给每个过滤器分配唯一的 order 值避免依赖稳定排序的隐式行为。7.2 动态路由场景下的顺序变化动态路由比如从数据库或配置中心加载路由场景下路由过滤器的顺序可能每次刷新都不同。因为路由加载顺序不确定order 相同的路由过滤器相对顺序会变。解决办法还是给路由过滤器显式设 order 值。如果路由是动态生成的在生成路由时就把 order 值算好写进过滤器配置里。不要依赖默认值。7.3 过滤器抛异常时的链式反应过滤器 pre 阶段抛异常后续过滤器的 pre 阶段不会执行但已经执行过的过滤器的 post 阶段会执行。这个行为在排查问题时很重要。如果你在 post 阶段做了资源清理即使 pre 阶段抛异常清理逻辑也会跑。post 阶段抛异常异常会向上传播外层过滤器的 post 阶段可能不执行。所以 post 阶段的代码要加 try-catch避免一个过滤器的异常影响整条链。return chain.filter(exchange).then(Mono.fromRunnable(() - { try { // post 阶段逻辑 } catch (Exception e) { log.error(post 阶段异常, e); } }));这个 try-catch 是必须的。我见过因为 post 阶段没加 try-catch一个日志过滤器抛异常导致响应写回失败的案例。响应写回失败客户端拿到的是超时错误排查了半天才发现是日志过滤器的问题。7.4 响应式编程对顺序的隐性影响Spring Cloud Gateway 基于 Reactor过滤器的 pre 和 post 阶段是异步的。chain.filter(exchange)返回的是MonoVoid它的执行时机由 Reactor 调度器决定。这意味着过滤器的实际执行顺序可能和排序结果不完全一致特别是在有异步操作的情况下。比如你在 pre 阶段做了一个异步的 Redis 查询查询结果还没回来就调用了chain.filter那后续过滤器的 pre 阶段会先执行。这种顺序错乱是响应式编程特有的排查起来比较麻烦。解决办法是在 pre 阶段需要异步操作时用flatMap把异步结果和chain.filter串起来return redisTemplate.opsForValue().get(key) .flatMap(value - { // 用异步结果做判断 return chain.filter(exchange); });这样能保证异步操作完成后才继续过滤器链顺序符合预期。这个坑我在做分布式限流时踩过限流计数还没更新就放行了请求导致限流不准。改成flatMap后问题解决。7.5 过滤器链的短路机制过滤器可以通过不调用chain.filter(exchange)来短路整条链。短路后后续过滤器的 pre 阶段不执行但已经执行过的过滤器的 post 阶段会执行。这个机制常用于鉴权失败直接返回 401 的场景。短路时要注意响应状态码和响应体的设置。如果短路时没设置响应状态码默认是 200客户端会以为请求成功。我见过鉴权失败返回 200 空响应的案例客户端解析失败报了个莫名其妙的错误。if (!authorized) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] body {\code\:401,\msg\:\unauthorized\}.getBytes(StandardCharsets.UTF_8); DataBuffer buffer exchange.getResponse().bufferFactory().wrap(body); return exchange.getResponse().writeWith(Mono.just(buffer)); }这段代码设置了状态码、内容类型和响应体客户端能正确解析。短路时这些都要设全少一个都可能出问题。7.6 过滤器链的性能考量过滤器链越长每个请求的耗时越大。虽然单个过滤器的耗时可能只有几毫秒但几十个过滤器叠加起来延迟就很可观了。我测过一个项目网关层有 30 多个过滤器平均延迟增加了 15ms。优化方向有两个一是合并职责相近的过滤器减少链的长度二是把非必要的过滤器改成条件执行比如只在特定路由上生效。全局过滤器要慎用能做成路由过滤器的就不要做成全局的。另外过滤器的 order 值也会影响性能。order 值小的过滤器先执行如果它做了耗时操作后续过滤器都要等。把耗时操作尽量往后放让快速失败的过滤器先执行能提升整体吞吐。8. 实际项目中的顺序调优案例8.1 电商网关的过滤器链设计某电商项目的网关层有这些需求鉴权、限流、日志、灰度路由、请求体缓存、响应体加密。我设计的过滤器链是这样的过滤器order阶段职责BodyCacheFilter-2147482648pre缓存请求体AuthFilter100pre鉴权RateLimitFilter200pre限流GrayRouteFilter300pre灰度路由LogFilter-2post日志记录EncryptFilter-3post响应加密BodyCacheFilter的 order 是HIGHEST_PRECEDENCE 1000确保请求体最先缓存。AuthFilter和RateLimitFilter在请求转发前执行。GrayRouteFilter在鉴权限流之后根据用户信息决定路由。LogFilter和EncryptFilter在 post 阶段执行order 为负数确保在响应写回后处理。这个设计跑了一年多没出过顺序问题。关键点是每个过滤器的职责清晰order 值分布合理依赖关系明确。8.2 金融网关的鉴权与审计顺序金融项目对鉴权和审计的要求更严格。鉴权必须在最前面审计日志必须记录完整的请求和响应。我设计的链是这样的鉴权过滤器 order 设为 50审计过滤器 pre 阶段 order 设为 60post 阶段 order 设为 -5。鉴权先执行通过后审计过滤器记录请求信息。响应回来后审计过滤器的 post 阶段记录响应信息此时NettyWriteResponseFilter已经执行完能拿到完整响应。审计过滤器用ServerHttpResponseDecorator包装响应在writeWith里拦截数据。拦截到的数据同时写回客户端和审计日志。这个方案保证了审计日志的完整性也满足了合规要求。8.3 高并发场景下的限流顺序优化高并发场景下限流过滤器的位置很关键。放在鉴权之前能挡住无效流量减轻鉴权压力放在鉴权之后能针对已鉴权用户做精细限流。我一般放两个限流过滤器一个在鉴权之前order 设为 10做 IP 级别的粗粒度限流一个在鉴权之后order 设为 200做用户级别的细粒度限流。粗粒度限流挡住恶意流量细粒度限流保护后端服务。这个方案在压测中表现不错。粗粒度限流挡住了 80% 的无效请求细粒度限流把有效请求控制在后端承载范围内。两个限流器的 order 值差 190中间留了足够空间给其他过滤器。8.4 多租户网关的租户识别顺序多租户项目里租户识别过滤器要尽早执行因为后续的鉴权、限流、路由都依赖租户信息。我把租户识别过滤器的 order 设为 5在鉴权之前执行。租户识别从请求头或域名里解析出租户 ID放到 exchange 的属性里。后续过滤器从属性里取租户 ID做租户级别的鉴权、限流、路由。这个设计的关键是租户识别必须最先执行且不能抛异常否则后续过滤器拿不到租户信息。租户识别过滤器里加了兜底逻辑解析不出租户 ID 时用默认租户并打警告日志。这样即使请求头缺失也不会导致整条链失败。8.5 顺序调优的效果对比调优前后我做了对比测试。调优前网关平均延迟 25msP99 延迟 120ms。调优后平均延迟 12msP99 延迟 45ms。提升主要来自三个方面减少了过滤器数量、优化了 order 值分布、把耗时操作后移。具体数据指标调优前调优后提升平均延迟25ms12ms52%P99 延迟120ms45ms62%吞吐量3000 QPS5500 QPS83%过滤器数量321844%这个对比说明过滤器顺序调优不只是解决功能问题对性能也有实实在在的提升。当然这些数据是特定场景下的不同项目情况不同但方向是一致的。9. 过滤器顺序问题的排查工具箱9.1 日志埋点的最佳实践日志是排查顺序问题的第一工具。我习惯在每个过滤器的 pre 和 post 阶段打日志日志格式统一[FilterOrder] filterNameAuthFilter, order100, phasepre, traceIdxxx [FilterOrder] filterNameAuthFilter, order100, phasepost, traceIdxxx用 traceId 串联同一个请求的所有日志能清晰看到执行顺序。日志级别用 DEBUG生产环境默认关闭需要排查时动态打开。9.2 用断点调试过滤器链本地开发时断点调试是最直接的手段。在FilteringWebHandler的handle方法里打断点能看到合并排序后的过滤器列表。在DefaultGatewayFilterChain的filter方法里打断点能看到每个过滤器的执行顺序。断点调试的缺点是只能看单个请求没法看整体分布。我一般先用日志看整体再用断点看细节。9.3 自定义端点暴露过滤器链Spring Boot Actuator 的/actuator/gateway/routes端点能看到路由级过滤器但看不到全局过滤器。我写了个自定义端点把完整的过滤器链暴露出来Endpoint(id filterChain) Component public class FilterChainEndpoint { ReadOperation public ListMapString, Object filterChain() { // 返回排序后的过滤器列表 } }这个端点返回每个过滤器的名字、order 值、类型全局/路由排查时一目了然。生产环境要加权限控制避免暴露敏感信息。9.4 压测验证顺序稳定性顺序问题有时在高并发下才暴露。我一般用压测工具模拟高并发观察过滤器执行顺序是否稳定。如果发现顺序抖动可能是响应式编程的异步问题需要检查 pre 阶段有没有异步操作。压测时重点关注 P99 和 P999 延迟这两个指标能反映极端情况下的顺序问题。如果 P999 延迟远高于 P99可能是某个过滤器在特定条件下走了慢路径顺序异常。9.5 版本升级的顺序回归测试每次升级 Spring Cloud Gateway 版本我都会跑一遍顺序回归测试。测试用例覆盖鉴权、限流、日志、响应处理这几个关键场景。断言点包括过滤器执行顺序、响应状态码、响应体内容。回归测试用 Testcontainers 启动一个真实的网关实例发请求后断言结果。这个测试跑一遍大概 2 分钟能挡住大部分升级导致的顺序问题。10. 一些容易被忽略的细节10.1 过滤器名字冲突的影响Spring 容器里同名的 Bean 会冲突过滤器也一样。如果两个过滤器类名相同在不同包下注册到容器里可能只有一个生效。我见过因为类名冲突导致鉴权过滤器没注册的案例排查了半天才发现是两个同名类。解决办法是给过滤器类起唯一的名字或者在Component注解里指定 Bean 名字。路由过滤器在配置里用name指定时也要确保名字唯一。10.2 条件装配对顺序的影响用ConditionalOnProperty等条件注解控制过滤器是否装配时要注意条件不满足时过滤器不注册顺序链会变化。比如某个日志过滤器只在测试环境装配生产环境没有那生产环境的过滤器链就少了一个其他过滤器的相对顺序可能变化。建议在条件装配的过滤器上order 值留出足够的空隙即使它不装配其他过滤器的顺序也不受影响。10.3 过滤器继承带来的顺序继承如果过滤器有继承关系子类会继承父类的getOrder()方法。如果子类没重写用的就是父类的 order 值。这个行为有时会导致意外比如两个子类过滤器继承了同一个父类order 值相同排序时相对顺序不确定。建议每个过滤器类都显式实现getOrder()不要依赖继承。这样 order 值一目了然不会因为继承关系产生意外。10.4 响应式上下文传递的顺序依赖Reactor 的上下文Context在过滤器链里传递时顺序会影响上下文的可见性。pre 阶段设置的上下文post 阶段能拿到但 post 阶段设置的上下文pre 阶段拿不到。如果过滤器依赖上下文传递数据要注意设置和读取的时机。我一般用 exchange 的属性传递数据而不是 Reactor 上下文。exchange 的属性在整条链里都可见不受 pre/post 阶段限制。上下文适合传递跨请求的全局数据比如 traceId。10.5 过滤器链的线程切换问题响应式编程里过滤器链可能在不同线程上执行。pre 阶段和 post 阶段可能不在同一个线程用 ThreadLocal 存数据会丢。我见过用 ThreadLocal 存用户信息post 阶段拿不到的案例。解决办法是用 exchange 属性或 Reactor 上下文不要用 ThreadLocal。如果必须用 ThreadLocal要在切换线程时手动传递或者用 Reactor 的contextWrite把数据写进上下文。10.6 网关过滤器与 WebFilter 的顺序关系网关过滤器链和 WebFilter 链是两套体系。WebFilter 在网关过滤器之前执行CorsWebFilter、SecurityWebFilterChain这些都是 WebFilter。它们的顺序由Order注解控制和网关过滤器的 order 值不在一个维度上比较。理解这一点很重要。如果你在网关过滤器里做跨域处理可能和CorsWebFilter冲突。建议跨域统一用CorsWebFilter处理网关过滤器不要重复做。10.7 过滤器链的初始化时机过滤器链是在路由匹配后构建的每条路由的过滤器链可能不同。全局过滤器对所有路由生效路由过滤器只对匹配的路由生效。所以同一个请求走不同路由时过滤器链可能不一样。这个特性在排查问题时要注意。如果某个过滤器只对部分路由生效检查它是不是路由过滤器以及路由配置是否正确。全局过滤器如果没生效检查它有没有注册到 Spring 容器。10.8 顺序问题的预防胜于排查最后说点经验。顺序问题排查起来费时费力最好的办法是预防。项目初期就定好 order 值规范每个过滤器加日志埋点写集成测试验证顺序。这三件事做好能避免 90% 的顺序问题。我现在的习惯是每加一个过滤器先想清楚它的 order 值该在哪个区间依赖哪些过滤器被哪些过滤器依赖。想清楚了再写代码比写完再调顺序高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑