Java框架 SpringCloud 快速入门: 过滤器链执行顺序

概述

网关里同时开着路由级 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 合并全局过滤器并组织最终链条

官方文档

总结

  • 请求匹配到路由之后,网关才把 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 那一行,比自己推快得多。
相关推荐
砚底藏山河1 小时前
量化实战:回测数据底座快照与版本管理进阶
java·大数据·python·金融·maven
Cc.Y1 小时前
Java零基础入门:封装与继承 —— 从“裸奔“到“穿衣服“,从“重复造轮子“到“站在巨人肩膀上“
java·开发语言
Escalating_xu1 小时前
【C 语言】深入理解指针(4):回调函数、qsort、void * 与泛型排序的模拟实现
java·c语言·开发语言
Shaoshing2 小时前
雪花算法
java
w_zero_one2 小时前
链表(4)
java·数据结构·算法
见叶之秋2 小时前
C++ 泛型世界的两块拼图:容器适配器与仿函数
java·开发语言
MayBaymax3 小时前
MQ 基础概念与架构
java·中间件·架构·java-rocketmq
Dovis(誓平步青云)3 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
yuniko-n3 小时前
【Java】字符串相关坑点
java·开发语言·python