概述
网关里同时开着路由级 filters、default-filters 和若干个 GlobalFilter,它们不是各走各的,而是被合并成一条链 后按 order 值依次执行。顺序错了,鉴权可能发生在转发之后,日志统计也会漏掉一段。这篇把排序规则从源码层面摊开,再给一个能亲眼看到执行次序的最小实验。
纲要
- 三类过滤器的来源:路由级
filters/default-filters/GlobalFilter - 它们为什么能被塞进同一个
List里排序(适配器模式 +OrderedGatewayFilter) order值从哪来:Spring 指定 vs 自己指定,以及@Order与getOrder()的优先级- 排序四条规则,含"order 值相同时谁先"的坑
pre与post:为什么前置是升序、后置是逆序- 实测:两个
order为 1 和 2 的全局过滤器,打印进入/离开日志 - 工程上的
order分区建议 + 五个高频误用 - 内置过滤器的 order 对照表(
NettyRoutingFilter、NettyWriteResponseFilter等)
三类过滤器排进同一条链
先纠正一个常见误解:过滤器链不是在容器启动时就定死的。只有请求匹配上某条路由之后,网关才知道这条路由挂了哪些过滤器,此时才会做合并。
- 路由级
filters:写在某条routes下面,只对该路由生效 default-filters:写在spring.cloud.gateway下面,对所有路由生效GlobalFilter:用 Java 代码实现的过滤器,逻辑自己写
| 维度 | 路由级 filters |
default-filters |
GlobalFilter |
|---|---|---|---|
| 声明位置 | routes[].filters |
spring.cloud.gateway.default-filters |
Java 类,@Component |
| 生效范围 | 单条路由 | 全部路由 | 全部路由 |
| 逻辑 | 由 GatewayFilterFactory 固定 |
同左 | 自己写 filter() |
order 来源 |
Spring 按声明位置从 1 递增 | 同左(独立计数) | 自己指定 |
前两列的"长得一模一样"不是巧合。它们在 Java 层面是同一个类型:default-filters 和路由级 filters 都只是 FilterDefinition,由 GatewayFilterFactory 读配置、生产出一个 GatewayFilter。所以这两类可以认为是一类。
麻烦的是 GlobalFilter------它的接口签名和 GatewayFilter 完全不同:
java
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
两者怎么合并到同一个集合里?靠适配器模式。FilteringWebHandler 内部有个 GatewayFilterAdapter,实现了 GatewayFilter,同时持有被装饰的 GlobalFilter:
java
private static class GatewayFilterAdapter implements GatewayFilter {
private final GlobalFilter delegate;
GatewayFilterAdapter(GlobalFilter delegate) {
this.delegate = delegate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return this.delegate.filter(exchange, chain);
}
}
到这一步,网关里所有过滤器都变成了 GatewayFilter,一个 List 装得下,也就能排序了。合并过程用一张图表示:
#mermaid-svg-CdHMkGcrm5AcFFL8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CdHMkGcrm5AcFFL8 .error-icon{fill:#552222;}#mermaid-svg-CdHMkGcrm5AcFFL8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CdHMkGcrm5AcFFL8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .marker.cross{stroke:#333333;}#mermaid-svg-CdHMkGcrm5AcFFL8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CdHMkGcrm5AcFFL8 p{margin:0;}#mermaid-svg-CdHMkGcrm5AcFFL8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster-label text{fill:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster-label span{color:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster-label span p{background-color:transparent;}#mermaid-svg-CdHMkGcrm5AcFFL8 .label text,#mermaid-svg-CdHMkGcrm5AcFFL8 span{fill:#333;color:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .node rect,#mermaid-svg-CdHMkGcrm5AcFFL8 .node circle,#mermaid-svg-CdHMkGcrm5AcFFL8 .node ellipse,#mermaid-svg-CdHMkGcrm5AcFFL8 .node polygon,#mermaid-svg-CdHMkGcrm5AcFFL8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .rough-node .label text,#mermaid-svg-CdHMkGcrm5AcFFL8 .node .label text,#mermaid-svg-CdHMkGcrm5AcFFL8 .image-shape .label,#mermaid-svg-CdHMkGcrm5AcFFL8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CdHMkGcrm5AcFFL8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .rough-node .label,#mermaid-svg-CdHMkGcrm5AcFFL8 .node .label,#mermaid-svg-CdHMkGcrm5AcFFL8 .image-shape .label,#mermaid-svg-CdHMkGcrm5AcFFL8 .icon-shape .label{text-align:center;}#mermaid-svg-CdHMkGcrm5AcFFL8 .node.clickable{cursor:pointer;}#mermaid-svg-CdHMkGcrm5AcFFL8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .arrowheadPath{fill:#333333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CdHMkGcrm5AcFFL8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CdHMkGcrm5AcFFL8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CdHMkGcrm5AcFFL8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster text{fill:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 .cluster span{color:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CdHMkGcrm5AcFFL8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CdHMkGcrm5AcFFL8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CdHMkGcrm5AcFFL8 .icon-shape,#mermaid-svg-CdHMkGcrm5AcFFL8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CdHMkGcrm5AcFFL8 .icon-shape p,#mermaid-svg-CdHMkGcrm5AcFFL8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CdHMkGcrm5AcFFL8 .icon-shape .label rect,#mermaid-svg-CdHMkGcrm5AcFFL8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CdHMkGcrm5AcFFL8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CdHMkGcrm5AcFFL8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CdHMkGcrm5AcFFL8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不匹配
命中 user-service 路由
请求进入网关 端口 10010
路由断言匹配
404 结束
RouteDefinitionRouteLocator.getFilters
先加载 default-filters
order 从 1 递增
再加载本路由的 filters
order 独立从 1 递增
AnnotationAwareOrderComparator.sort 稳定排序
FilteringWebHandler.handle
combined 先放全部 GlobalFilter 再 addAll 上面的链
再 sort 一次 得到最终执行顺序
DefaultGatewayFilterChain 逐个调用 filter
涉及的关键源码位置,读一遍比记结论有用:
| 类 / 方法 | 作用 |
|---|---|
RouteDefinitionRouteLocator#getFilters |
加载 default-filters + 本路由 filters,合并后排序 |
RouteDefinitionRouteLocator#loadGatewayFilters |
给配置类过滤器分配 order,从 1 递增 |
FilteringWebHandler#loadFilters |
把 GlobalFilter 包成 GatewayFilterAdapter |
FilteringWebHandler#handle |
把全局过滤器和路由过滤器合成一条链,排序后执行 |
GatewayFilterAdapter |
全局过滤器到 GatewayFilter 的适配器 |
order 值从哪来
排序键是 order,int 类型。三个来源:
| 过滤器类型 | order 来源 | 谁决定 |
|---|---|---|
default-filters |
Spring 按声明位置从 1 递增 | 框架 |
路由级 filters |
Spring 按声明位置从 1 递增(与上面独立计数) | 框架 |
GlobalFilter |
实现 Ordered#getOrder() 或类上加 @Order |
开发者 |
loadGatewayFilters 里那句赋值是铁证:
java
GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
ordered.add(gatewayFilter);
}
else {
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
}
循环变量 i 从 0 开始,所以第一个配置的过滤器 order = 1,第二个是 2,依此类推。default-filters 和路由级 filters 各自调用一次这个方法,计数器互不干扰------两边都从 1 开始,这就是后面"撞档"的源头。
再看 yml,注意工程里实际长这样:
yaml
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
default-filters 里这一条 AddRequestHeader 的 order 就是 1。有个常被问到的点:能不能在 yml 里直接给它写 order 来改顺序?答案是不能------FilterDefinition 认的字段只有 name 和 args,order 不属于配置项,写了也不会进排序。照讲义和官方文档的口径,配置类过滤器的顺序由声明位置 决定;真要自己控,就得写 GatewayFilterFactory,在 apply 里返回一个带 order 的 OrderedGatewayFilter。
@Order 与 getOrder() 谁说了算
GlobalFilter 两种写法都有人用。Spring 官方 javadoc 对 AnnotationAwareOrderComparator 的说明很明确:"with an order value provided by an Ordered instance overriding a statically defined annotation value" ------Ordered 实例的返回值覆盖注解值 ,即 getOrder() 优先于 @Order。源码里也是先查 Ordered,查不到才回退去找注解。
但 SCG 2.2.9(本工程 Spring Cloud Hoxton.SR10 对应的版本)还有个更隐蔽的点。看 FilteringWebHandler#loadFilters:
java
private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
return filters.stream().map(filter -> {
GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
if (filter instanceof Ordered) {
int order = ((Ordered) filter).getOrder();
return new OrderedGatewayFilter(gatewayFilter, order);
}
return gatewayFilter;
}).collect(Collectors.toList());
}
它只认 instanceof Ordered。也就是说,只写 @Order 而没实现 Ordered 的全局过滤器,拿不到自己的 order 值 ,参与排序时的键会退化成 Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE),排到最后去。这一点在源码里绕了两层,光看注解很容易被误导。工程终态那份快照其实已经给出了正确姿势:
java
package cn.itcast.gateway;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// @Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.取 authorization 参数
String auth = params.getFirst("authorization");
// 3.等于 admin 才放行
if ("admin".equals(auth)) {
return chain.filter(exchange);
}
// 4.否则拦截
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}
@Order(-1) 被注释掉,改成 implements GlobalFilter, Ordered + getOrder()。两种方式只用一种,别同时留着。
顺带两个和讲义对不上的细节,实读工程要留意:讲义 3.5.2 把 AuthorizeFilter 放在 cn.itcast.gateway.filters 包,工程终态放在了 cn.itcast.gateway;讲义拦截时设的是 FORBIDDEN(403),工程里是 UNAUTHORIZED(401)。本文新增的过滤器沿用讲义的 filters 包,启动类在 cn.itcast.gateway,照样能被扫描到。
排序规则
记四条就够。
- 升序 :
order值越小,优先级越高,越先执行。负数比正数早。 - 配置类过滤器由 Spring 发号 :
default-filters与路由级filters分开计数,各自从 1 递增。 - 全局过滤器自己发号 :
@Order或getOrder(),两者同时存在时getOrder()赢。 - 同 order 的先后 :讲义给出的口径是
default-filters > 路由 filters > GlobalFilter。
第四条要多说两句,因为它和源码实际行为有分歧。RouteDefinitionRouteLocator#getFilters 里是先加载默认过滤器、再加载路由过滤器,然后稳定排序------所以"同 order 时默认过滤器在前"这一点,讲义和源码一致。官方文档还给了更细的例子:default-filters 声明 A、B、C、D,某条路由声明 X、Y,两边独立从 1 编号,稳定排序后执行顺序是 A → X → B → Y → C → D,也就是同档的位置交错合并。
但 GlobalFilter 这一层是反的。FilteringWebHandler#handle 里:
java
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
AnnotationAwareOrderComparator.sort(combined);
combined 是先放全局过滤器、再 addAll 路由那条链;List.sort 是稳定排序,相等元素保持插入顺序。所以当一个 GlobalFilter 和某个配置类过滤器order 完全相同时,全局过滤器反而排在前面,和讲义结论相反。
这不是要挑刺,结论很实用:同 order 的先后别依赖,把值显式错开。判断代码里出现过"鉴权过滤器有时拦得住、有时拦不住",八成就是撞档加上了版本差异。
pre 与 post:前置升序,后置逆序
官方文档对这两个阶段的说法值得逐字读一遍:"the filter with the highest precedence is the first in the 'pre'-phase and the last in the 'post'-phase" ------优先级最高的过滤器,在 pre 阶段排第一,在 post 阶段排最后。和 Spring AOP 的环绕通知一个道理,order 小的过滤器包在最外层。
user-service 8081 NettyRoutingFilter order 2147483647 过滤器 order 2 过滤器 order 1 过滤器 order -1 网关 10010 客户端 user-service 8081 NettyRoutingFilter order 2147483647 过滤器 order 2 过滤器 order 1 过滤器 order -1 网关 10010 客户端 #mermaid-svg-E12lNlC7A5Jt8Uoi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E12lNlC7A5Jt8Uoi .error-icon{fill:#552222;}#mermaid-svg-E12lNlC7A5Jt8Uoi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E12lNlC7A5Jt8Uoi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E12lNlC7A5Jt8Uoi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E12lNlC7A5Jt8Uoi .marker.cross{stroke:#333333;}#mermaid-svg-E12lNlC7A5Jt8Uoi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E12lNlC7A5Jt8Uoi p{margin:0;}#mermaid-svg-E12lNlC7A5Jt8Uoi .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E12lNlC7A5Jt8Uoi text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E12lNlC7A5Jt8Uoi .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-E12lNlC7A5Jt8Uoi .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-E12lNlC7A5Jt8Uoi #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-E12lNlC7A5Jt8Uoi .sequenceNumber{fill:white;}#mermaid-svg-E12lNlC7A5Jt8Uoi #sequencenumber{fill:#333;}#mermaid-svg-E12lNlC7A5Jt8Uoi #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-E12lNlC7A5Jt8Uoi .messageText{fill:#333;stroke:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E12lNlC7A5Jt8Uoi .labelText,#mermaid-svg-E12lNlC7A5Jt8Uoi .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .loopText,#mermaid-svg-E12lNlC7A5Jt8Uoi .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-E12lNlC7A5Jt8Uoi .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-E12lNlC7A5Jt8Uoi .noteText,#mermaid-svg-E12lNlC7A5Jt8Uoi .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-E12lNlC7A5Jt8Uoi .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E12lNlC7A5Jt8Uoi .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E12lNlC7A5Jt8Uoi .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-E12lNlC7A5Jt8Uoi .actorPopupMenu{position:absolute;}#mermaid-svg-E12lNlC7A5Jt8Uoi .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-E12lNlC7A5Jt8Uoi .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-E12lNlC7A5Jt8Uoi .actor-man circle,#mermaid-svg-E12lNlC7A5Jt8Uoi line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-E12lNlC7A5Jt8Uoi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /user/1 1 pre 进入 2 chain.filter 向下 3 chain.filter 向下 4 chain.filter 向下 5 真正发起下游 HTTP 请求 6 200 响应 7 post 退出 8 post 退出 9 post 退出 10 把响应回写客户端 11 200 12
转发这件事是 NettyRoutingFilter 干的,它的 order 是 Ordered.LOWEST_PRECEDENCE(2147483647),永远排在最后------正好让它成为"最内层",被所有过滤器包住。响应回写是 NettyWriteResponseFilter 干的,order 是 -1,在 pre 阶段几乎最早进来,于是它的后置处理也几乎最晚做。
常用内置全局过滤器的 order(2.2.9 实测值,随版本可能微调,别写死依赖):
| 过滤器 | order | 干什么 |
|---|---|---|
RemoveCachedBodyFilter |
Integer.MIN_VALUE |
清理缓存的请求体 |
AdaptCachedBodyGlobalFilter |
-2147482648 |
适配缓存请求体 |
NettyWriteResponseFilter |
-1 |
把下游响应回写给客户端 |
ForwardPathFilter / GatewayMetricsFilter |
0 |
转发路径处理 / 指标埋点 |
RouteToRequestUrlFilter |
10000 |
按路由改写请求 URI |
LoadBalancerClientFilter |
10100 |
解析 lb://userservice 的服务名 |
WebsocketRoutingFilter |
2147483646 |
WebSocket 路由 |
NettyRoutingFilter |
2147483647 |
真正发起下游请求 |
从这张表能直接推出两个硬约束:
- 想在请求被转发出去之前 处理请求,
order必须小于2147483647。 - 想在响应被回写客户端之前 加工响应,
order必须大于-1(因为 post 阶段是逆序,order 大的先执行后置)。
实测:两个全局过滤器,order 分别 1 和 2
工程结构(cloud-demo 下五个模块,只看网关):
text
cloud-demo
├── gateway
│ ├── pom.xml
│ └── src/main
│ ├── java/cn/itcast/gateway
│ │ ├── GatewayApplication.java
│ │ ├── AuthorizeFilter.java # 已有,order = -1
│ │ └── filters
│ │ ├── PreLogFilter.java # 新增,order = 1
│ │ └── PostLogFilter.java # 新增,order = 2
│ └── resources
│ └── application.yml
├── user-service
├── order-service
├── eureka-server
└── feign-api
第一个过滤器:
java
package cn.itcast.gateway.filters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class PreLogFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(PreLogFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[order=1] 进入 pre,uri={}", exchange.getRequest().getURI());
// 注意:后置逻辑必须挂在 then 上,直接写在下一行不是 post 阶段
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> log.info("[order=1] 响应返回,执行 post")));
}
@Override
public int getOrder() {
return 1;
}
}
第二个只有 order 和日志文案不同:
java
package cn.itcast.gateway.filters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class PostLogFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(PostLogFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[order=2] 进入 pre,uri={}", exchange.getRequest().getURI());
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> log.info("[order=2] 响应返回,执行 post")));
}
@Override
public int getOrder() {
return 2;
}
}
顺手把网关的日志级别开大一点,能直接看到排序结果------FilteringWebHandler 在 debug 级别会打印整条链:
yaml
server:
port: 10010
logging:
level:
cn.itcast: debug
org.springframework.cloud.gateway: debug # 打开后能看到 Sorted gatewayFilterFactories
pattern:
dateformat: MM-dd HH:mm:ss:SSS
启动 gateway、user-service,然后带参数访问一次:
bash
curl "http://localhost:10010/user/1?authorization=admin"
控制台输出(时间戳格式来自上面 dateformat):
text
20:14:02:118 [order=1] 进入 pre,uri=http://localhost:10010/user/1?authorization=admin
20:14:02:119 [order=2] 进入 pre,uri=http://localhost:10010/user/1?authorization=admin
20:14:02:191 [order=2] 响应返回,执行 post
20:14:02:191 [order=1] 响应返回,执行 post
1 进入 → 2 进入 → 2 离开 → 1 离开,括号形的对称结构。把两个 getOrder() 都换成 -1,输出就变成不稳定的形态;把 PostLogFilter 写成不实现 Ordered 只加 @Order(2),它会被甩到链尾,日志顺序也会变------这两组对照比记结论有用得多。
还有个容易看走眼的细节:default-filters 里的 AddRequestHeader 也是 order = 1,和 PreLogFilter 撞在同一档。它不打印日志所以看不出来,但它正是上一节说的"撞档"现场。工程里就尽量别制造这种同档。
@Order 那个坑的直观画法:
#mermaid-svg-NgoHKHOPk515VR8M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NgoHKHOPk515VR8M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NgoHKHOPk515VR8M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NgoHKHOPk515VR8M .error-icon{fill:#552222;}#mermaid-svg-NgoHKHOPk515VR8M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NgoHKHOPk515VR8M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NgoHKHOPk515VR8M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NgoHKHOPk515VR8M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NgoHKHOPk515VR8M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NgoHKHOPk515VR8M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NgoHKHOPk515VR8M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NgoHKHOPk515VR8M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NgoHKHOPk515VR8M .marker.cross{stroke:#333333;}#mermaid-svg-NgoHKHOPk515VR8M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NgoHKHOPk515VR8M p{margin:0;}#mermaid-svg-NgoHKHOPk515VR8M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NgoHKHOPk515VR8M .cluster-label text{fill:#333;}#mermaid-svg-NgoHKHOPk515VR8M .cluster-label span{color:#333;}#mermaid-svg-NgoHKHOPk515VR8M .cluster-label span p{background-color:transparent;}#mermaid-svg-NgoHKHOPk515VR8M .label text,#mermaid-svg-NgoHKHOPk515VR8M span{fill:#333;color:#333;}#mermaid-svg-NgoHKHOPk515VR8M .node rect,#mermaid-svg-NgoHKHOPk515VR8M .node circle,#mermaid-svg-NgoHKHOPk515VR8M .node ellipse,#mermaid-svg-NgoHKHOPk515VR8M .node polygon,#mermaid-svg-NgoHKHOPk515VR8M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NgoHKHOPk515VR8M .rough-node .label text,#mermaid-svg-NgoHKHOPk515VR8M .node .label text,#mermaid-svg-NgoHKHOPk515VR8M .image-shape .label,#mermaid-svg-NgoHKHOPk515VR8M .icon-shape .label{text-anchor:middle;}#mermaid-svg-NgoHKHOPk515VR8M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NgoHKHOPk515VR8M .rough-node .label,#mermaid-svg-NgoHKHOPk515VR8M .node .label,#mermaid-svg-NgoHKHOPk515VR8M .image-shape .label,#mermaid-svg-NgoHKHOPk515VR8M .icon-shape .label{text-align:center;}#mermaid-svg-NgoHKHOPk515VR8M .node.clickable{cursor:pointer;}#mermaid-svg-NgoHKHOPk515VR8M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NgoHKHOPk515VR8M .arrowheadPath{fill:#333333;}#mermaid-svg-NgoHKHOPk515VR8M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NgoHKHOPk515VR8M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NgoHKHOPk515VR8M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NgoHKHOPk515VR8M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NgoHKHOPk515VR8M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NgoHKHOPk515VR8M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NgoHKHOPk515VR8M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NgoHKHOPk515VR8M .cluster text{fill:#333;}#mermaid-svg-NgoHKHOPk515VR8M .cluster span{color:#333;}#mermaid-svg-NgoHKHOPk515VR8M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NgoHKHOPk515VR8M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NgoHKHOPk515VR8M rect.text{fill:none;stroke-width:0;}#mermaid-svg-NgoHKHOPk515VR8M .icon-shape,#mermaid-svg-NgoHKHOPk515VR8M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NgoHKHOPk515VR8M .icon-shape p,#mermaid-svg-NgoHKHOPk515VR8M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NgoHKHOPk515VR8M .icon-shape .label rect,#mermaid-svg-NgoHKHOPk515VR8M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NgoHKHOPk515VR8M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NgoHKHOPk515VR8M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NgoHKHOPk515VR8M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 直接写在下一行
接 then Mono.fromRunnable
filter 方法被调用
chain.filter 返回一个 Mono
后置逻辑怎么写
日志立刻打印
此时响应还没回来
仍在 pre 阶段
日志在 Mono 完成时打印
才是真正的 post 阶段
工程上的 order 分区
order 是 int,能用的范围很大,但实战里按区间分工最省事。
| 类型 | 建议 order | 理由 |
|---|---|---|
| 鉴权 / 限流 | 负数,如 -100 |
越早越好,不通过就直接返回,后续过滤器一次都不用跑 |
| 全链路日志、耗时统计 | 负数但要比鉴权的还小 ,如 -200 |
要包住整条链,post 阶段要最后离开才能算到总耗时 |
| 业务参数改写 | 0 附近或正数 |
排在鉴权之后,此时身份已确认 |
| 响应加工 | 大于 -1 |
post 阶段是逆序,order 比 -1 大才能赶在响应回写前改 |
| 依赖最终目标地址的逻辑 | 大于 10100 |
RouteToRequestUrlFilter、LoadBalancerClientFilter 之后 URI 才解析完 |
"谁包谁"这件事得单独想清楚。想把总耗时算准,日志过滤器必须最先进入、最后离开。按"order 小的在 pre 最先、在 post 最后",它的 order 要取极小值,同时还要小于鉴权过滤器的 order------否则鉴权在它外面,鉴权本身耗掉的时间和直接被拒的请求都不在统计范围内。这是很多人第一次写耗时统计会踩的地方:数字看着挺准,实际少了最外面那一段。
五个坑
getOrder()返回-1却指望它最后执行 。方向搞反了,-1表示最优先。想最后执行用Ordered.LOWEST_PRECEDENCE。- 只写
@Order不实现Ordered。2.2.9 的loadFilters只认instanceof Ordered,拿不到 order 就退化成Integer.MAX_VALUE。写法统一成implements GlobalFilter, Ordered+getOrder()。 - 两个过滤器给同一个 order。同档先后在讲义和源码里结论不一致(见上文),别把自己的业务逻辑押在这个顺序上。
- 以为后置处理也是升序。后置是逆序,"order 小的先进入、后离开"。写成升序思维,包在外层的过滤器会先执行 post,统计和清理逻辑全错位。
chain.filter(exchange)后面直接写业务代码 。这行代码在订阅时就执行了,此时下游响应还没回来,它属于 pre 阶段。要后置必须.then(...)。
API 速览
| 名称 | 全限定名 / 位置 | 用途 |
|---|---|---|
GlobalFilter |
org.springframework.cloud.gateway.filter.GlobalFilter |
自定义全局过滤器接口 |
filter() |
GlobalFilter#filter(ServerWebExchange, GatewayFilterChain) |
过滤器主体,返回 Mono<Void> |
Ordered |
org.springframework.core.Ordered |
提供排序键,getOrder() 优先于 @Order |
@Order |
org.springframework.core.annotation.Order |
注解方式指定顺序 |
OrderedGatewayFilter |
org.springframework.cloud.gateway.filter.OrderedGatewayFilter |
给无 order 的过滤器补一个排序键 |
GatewayFilterAdapter |
FilteringWebHandler 内部类 |
把 GlobalFilter 适配成 GatewayFilter |
Ordered.HIGHEST_PRECEDENCE |
Integer.MIN_VALUE |
最高优先级 |
Ordered.LOWEST_PRECEDENCE |
Integer.MAX_VALUE |
最低优先级,NettyRoutingFilter 用的就是这个 |
RouteDefinitionRouteLocator#getFilters |
org.springframework.cloud.gateway.route |
合并默认过滤器与路由过滤器并排序 |
FilteringWebHandler#handle |
org.springframework.cloud.gateway.handler |
合并全局过滤器并组织最终链条 |
官方文档
- Combined Global Filter and GatewayFilter Ordering(pre/post 与 order 官方口径)
- Default Filters(声明位置从 1 递增、稳定排序)
- AnnotationAwareOrderComparator(Ordered 覆盖注解值)
- FilteringWebHandler 源码 v2.2.9.RELEASE
- RouteDefinitionRouteLocator 源码 v2.2.9.RELEASE
总结
- 请求匹配到路由之后,网关才把
default-filters、本路由filters、全部GlobalFilter合成一条链,靠适配器统一成GatewayFilter再排序。 order升序执行。配置类过滤器的 order 由 Spring 按声明位置从 1 递增,两类分开计数;全局过滤器的 order 自己指定,getOrder()优先于@Order。- 前置升序、后置逆序。
NettyRoutingFilter(Integer.MAX_VALUE)在最内层,负责真正转发;NettyWriteResponseFilter(-1)在外层,负责回写响应。 - 同 order 的先后在讲义和 2.2.9 源码里结论不一致,所以别依赖它------把 order 显式错开,鉴权用负数、日志统计用更小的负数、响应加工放到
-1之后。 - 验证手段很直接:把
org.springframework.cloud.gateway的日志级别调到 debug,看Sorted gatewayFilterFactories那一行,比自己推快得多。