Spring Cloud Gateway 源码拆解:一个请求从进入到转发经历了什么
引言
"网关登录接口一直 401,JWT 密钥、Redis、白名单全查了一遍,最后发现是过滤器顺序问题——自定义 AuthFilter 和 StripPrefix 的 order 都是 0,谁先执行看运气。" 这个真实排查故事的关键线索是网关启动日志里一行不起眼的 Sorted gatewayFilterFactories:两个过滤器 order 相同,路径有时先被 StripPrefix 改写成 /auth/login,白名单配的 /api/auth/login 就永远匹配不上。
Spring Cloud Gateway(SCG)是 Spring Cloud 全家桶里少数基于 WebFlux/Netty 的全异步组件,很多从 Spring MVC 转过来的开发者读它的源码会不适应:没有 Servlet、没有线程池阻塞、到处是 Mono/Flux。这篇文章按一个请求的真实执行路径,从 DispatcherHandler 一路跟到 NettyRoutingFilter,讲透三件事:路由怎么匹配(优先级)、过滤器怎么排序(执行顺序)、响应式链路怎么串起来(Mono 套 Mono)。读完你会明白:网关里几乎所有"玄学 401/路径错乱/过滤器不生效"问题,本质都是这三件事没理清。
一、全景:请求生命周期总览
Netty(Reactor Netty,事件循环线程)
│ HTTP 请求解码成 ServerWebExchange(请求/响应/属性的响应式容器)
↓
DispatcherHandler.handle(exchange) ← WebFlux 总入口
│
├─① HandlerMapping 列表 → RoutePredicateHandlerMapping.getHandler()
│ 遍历所有 Route,Predicate 匹配 → 找到第一个命中的路由
│ 把 Route 塞进 exchange 属性
↓
├─② HandlerAdapter → SimpleHandlerAdapter 支持 FilteringWebHandler
↓
FilteringWebHandler.handle(exchange)
│ 把 GlobalFilter 全部取出 + 当前 Route 的 GatewayFilter
│ 按 order 排序 → 组装成 DefaultGatewayFilterChain(责任链)
↓
过滤器链依次执行:
-100 ReactiveLoadBalancerClientFilter(负载均衡,解析 service 名)
-1 NettyWriteResponseFilter(把下游响应写回客户端的"回程"起点)
0 RouteToRequestUrlFilter(把 lb://service 拼写成真实 URL)
1 自定义 AuthFilter(鉴权,生产建议放这里)
2 AddRequestHeader / StripPrefix 等
…
3 NettyRoutingFilter(order=Integer.MAX_VALUE,最后执行,真正发请求)
↓
下游服务响应 → 沿过滤器链反向回传(after 阶段/post 逻辑)→ 客户端
记住两个锚点:匹配阶段只找路由不转发;转发永远发生在 NettyRoutingFilter(order 最大,链尾)。所有鉴权、限流、改路径的过滤器都在它之前。
二、DispatcherHandler:WebFlux 的总调度
2.1 它不是 Servlet 的 DispatcherServlet
// spring-webflux 核心类,代码做了简化
public class DispatcherHandler implements WebHandler {
private final List<HandlerMapping> handlerMappings; // 有序
private final List<HandlerAdapter> handlerAdapters;
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange)) // ① 找处理器
.next() // 第一个非空的
.switchIfEmpty(DispatcherHandler.this.createNotFoundError(exchange))
.flatMap(handler -> invokeHandler(exchange, handler)) // ② 执行处理器
.flatMap(result -> handleResult(exchange, result)); // ③ 处理结果
}
}
三个关键点:
- handlerMappings 是列表:SCG 启动时自动装配的
RoutePredicateHandlerMapping是其中之一,和静态资源、actuator 等 Mapping 共存,按顺序匹配,第一个给出 handler 的胜出。 - 返回值全是 Mono:没有任何阻塞调用。
concatMap顺序尝试各 Mapping,next()取第一个非空——这就是响应式的"短路"。 - 没匹配上走 createNotFoundError:对应网关返回 404 的场景(路由表根本没这条路径)。
2.2 ServerWebExchange:贯穿全链路的上下文
public interface ServerWebExchange {
ServerHttpRequest getRequest();
ServerHttpResponse getResponse();
Map<String, Object> getAttributes(); // 过滤器之间传递数据靠它
// ...
}
网关是异步的,没有 ThreadLocal 可用(一个请求可能在多个 Netty 线程间跳)。过滤器之间传数据(用户信息、开始时间、路由对象)全部塞 exchange.getAttributes()——这也是为什么你在自定义过滤器里拿到当前路由要用:
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
三、RoutePredicateHandlerMapping:路由怎么匹配
3.1 核心源码
// spring-cloud-gateway-server,简化
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
private final RouteLocator routeLocator;
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
return lookupRoute(exchange)
.flatMap(route -> {
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
return Mono.just(this.webHandler); // 永远返回 FilteringWebHandler
})
.switchIfEmpty(Mono.empty());
}
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
.concatMap(route -> Mono.just(route)
.filterWhen(r -> {
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return r.getPredicate().apply(exchange); // 执行 Predicate
})
.doOnError(e -> logger.error(...))
.onErrorResume(e -> Mono.empty()))
.next(); // ← 第一个 Predicate 返回 true 的路由胜出
}
}
注意 routeLocator.getRoutes() 返回 Flux<Route>——路由是有序的流,按顺序逐个试 Predicate,第一个命中的立即 next() 胜出,后面的路由不看了。
3.2 路由的顺序从哪来
| 路由来源 | 排序规则 |
|---|---|
Java DSL routes.route(...) | 按定义顺序(可用 .order(n) 显式指定) |
| YAML 配置 | 按列表顺序,spring.cloud.gateway.routes 数组从上到下 |
| 服务发现自动路由(lb://) | DiscoveryClientRouteDefinitionLocator,order 默认很大(靠后) |
| 显式 order | order 值小的优先 |
spring:
cloud:
gateway:
routes:
- id: auth-route
uri: lb://auth-service
predicates:
- Path=/api/auth/** # ← 更具体的路由放前面
- id: user-route
uri: lb://user-service
predicates:
- Path=/api/** # ← 通配范围大的放后面,否则会截胡
3.3 Predicate 组合:AND 语义
一条路由可以配多个 Predicate,它们之间是逻辑与:
// 等价于:Path 且 Header 且时间在 0~12 点
predicates:
- Path=/api/order/**
- Header=X-Caller, app
- Between=2026-09-12T00:00:00+08:00, 2026-09-12T12:00:00+08:00
3.4 两个常见排查点
- 404 但服务明明在线:路由 Predicate 没匹配。开
logging.level.org.springframework.cloud.gateway=DEBUG,日志会打印每条路由的匹配情况;同时确认 Path 匹配是匹配原始请求路径(StripPrefix 还没执行,见第五章的顺序坑)。 - 路由被"截胡":
/api/auth/**配在了/api/**后面,请求全被通配路由接走。原则:具体路由在前、通配路由在后。
四、FilteringWebHandler:过滤器链的组装
4.1 核心源码:两类过滤器合并排序
public class FilteringWebHandler implements WebHandler {
private final List<GatewayFilter> globalFilters;
public FilteringWebHandler(List<GlobalFilter> globalFilters) {
// GlobalFilter 启动时收集,包装成 GatewayFilter
this.globalFilters = loadFilters(globalFilters);
}
@Override
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); // ← 按 @Order/getOrder 排序
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
}
4.2 GlobalFilter vs GatewayFilter
| 类型 | 作用范围 | 定义方式 | 典型 |
|---|---|---|---|
| GlobalFilter | 所有路由生效 | Spring Bean,实现 GlobalFilter + getOrder() | NettyRoutingFilter、负载均衡、鉴权 |
| GatewayFilter | 只对配置它的路由生效 | YAML filters / RouteFilter 工厂 | StripPrefix、AddRequestHeader、限流 |
本质上它们在同一条链里——启动时 GlobalFilter 被 GatewayFilterAdapter 包成 GatewayFilter,然后两类放一起统一按 order 排序。所以"全局过滤器一定在路由过滤器前面"是误解:谁的 order 小谁在前。
4.3 内置过滤器 order 速查表(必须背下来)
| 过滤器 | order | 干什么 |
|---|---|---|
| RemoveCachedBodyFilter | -2147483648(最小) | 清理缓存 body |
| AdaptCachedBodyGlobalFilter | -2147482648 | 缓存请求体(让 body 可重复读) |
| NettyWriteResponseFilter | -1 | 把下游响应写回(回程过滤器的注册点) |
| RouteToRequestUrlFilter | 0 | 用路由信息拼出真实目标 URL |
| ReactiveLoadBalancerClientFilter | 10150 | lb:// 服务名解析为实例 |
| NettyRoutingFilter | Integer.MAX_VALUE(链尾) | 真正用 Netty 发 HTTP 请求 |
| ForwardRoutingFilter | Integer.MAX_VALUE | forward:// 本地转发 |
| Route 级 GatewayFilter(StripPrefix 等) | 大多在 0~10 | 改写路径/头/参数 |
启动时打开 DEBUG 日志可以直接看到排序结果:
Sorted gatewayFilterFactories: [...]——排查顺序问题先看这行日志。
五、DefaultGatewayFilterChain:责任链与执行顺序
5.1 链式调用源码
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final int index;
private final List<GatewayFilter> filters;
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
if (this.index < this.filters.size()) {
GatewayFilter filter = this.filters.get(this.index);
DefaultGatewayFilterChain next =
new DefaultGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, next); // 当前过滤器执行,并把"下一个链"传给它
}
return Mono.empty();
}
}
和 Servlet 责任链神似,但有一个本质区别:filter.filter(exchange, next) 返回的是 Mono,链的"前进"是声明式组装而不是立即执行。一个典型过滤器长这样:
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (!verify(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 短路:不调 chain.filter()
}
exchange.getAttributes().put("userId", parseUserId(token));
return chain.filter(exchange); // 放行:组装下一个过滤器
}
@Override
public int getOrder() { return 1; }
}
5.2 pre / post 两阶段:Mono 的 defer 效应
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// post 逻辑:下游响应回来之后才执行
log.info("响应状态: {}", exchange.getResponse().getStatusCode());
}));
执行时序(同步视角的等价描述):
pre-AuthFilter ──→ pre-StripPrefix ──→ … ──→ NettyRoutingFilter(发请求)
│ 等下游 I/O
post-AuthFilter ←── post-StripPrefix ←─ … ←─────── 响应返回
(.then 里的逻辑,顺序反向)
关键认知:chain.filter() 之前的代码是 pre(请求出去前),.then()/doOnSuccess() 里的是 post(响应回来后)。因为整条链是"拼装 Mono 图 + 订阅时才执行",post 逻辑的实际执行顺序与 pre 相反——这和 Spring MVC 拦截器的 preHandle/postHandle 模型完全同构,只是用 Mono 算子表达。
5.3 实战坑:AuthFilter 与 StripPrefix 同为 order=0
这是开篇那个登录 401 事故的根因。排序时 order 相同的过滤器相对顺序不稳定(取决于 Bean 注册/列表合并顺序):
时序 A(有时):AuthFilter 先执行
原始路径 /api/auth/login → 白名单 /api/auth/** 命中 → 放行 → StripPrefix 改写成 /auth/login ✅
时序 B(重启后可能变):StripPrefix 先执行
路径被改成 /auth/login → AuthFilter 白名单配的是 /api/auth/** → 不命中 → 401 ❌
修复(任选其一,推荐第一种):
// ① 让鉴权明确在路径改写之前
@Override
public int getOrder() { return -100; } // 早于所有路径改写类过滤器
② 白名单同时兼容改写前后两套路径:/api/auth/** 和 /auth/**
③ 把白名单判断建立在"原始路径"上:
exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR)
通用规律:路径匹配/鉴权类逻辑要在 StripPrefix、RewritePath、SetPath 等"路径改写类"过滤器之前,order 必须拉开不要都写 0。 路径在过滤器链里是会变的,每个过滤器看到的 path 可能不同——这是网关排障的第一直觉。
六、转发三件套:URL 拼装 → 负载均衡 → Netty 发请求
6.1 RouteToRequestUrlFilter(order=0):拼接目标 URL
// 核心逻辑简化
URI routeUri = route.getUri(); // lb://user-service
String scheme = routeUri.getScheme();
if ("lb".equalsIgnoreCase(scheme)) {
// 把 lb:// 替换成 http://,host 保留服务名,交给负载均衡过滤器解析
URI requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.host(routeUri.getHost())
.scheme("http")
.build(true).toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
6.2 ReactiveLoadBalancerClientFilter(order=10150):服务名换实例
从 LoadBalancer 选一个实例(轮询/随机/权重),把 URL 中的 user-service 替换成 10.0.3.12:8080,还附带处理同一实例的连接复用。配合 Nacos 等注册中心,实例列表来自服务发现(参见 Nacos vs Eureka 那篇)。
6.3 NettyRoutingFilter(order=Integer.MAX_VALUE):真正的转发
链尾的它才是真正发 HTTP 请求的地方,也是全链路的订阅触发点:
public class NettyRoutingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
// 用响应式 HttpClient 发请求(非阻塞,事件循环)
return this.httpClient.request(requestMethod, url, req -> {
// 复制请求头、应用过滤器改写后的 body
return req.headers(headers -> addHeaders(...))
.send(request.getBody());
})
.doOnNext(res -> {
// 把下游响应状态/响应头放回 exchange
exchange.getResponse().setStatusCode(res.status());
// 响应体不在这写回!交给 NettyWriteResponseFilter
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
})
.then(chain.filter(exchange)); // 回到链上,由 -1 号过滤器写回响应
}
@Override
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}
要点:
- 全异步非阻塞:
httpClient.request(...)返回Mono<HttpClientResponse>,发出请求的 Netty 线程不等待,响应就绪后回调——这是 SCG 单机能扛上万 QPS 而 Tomcat 版 Zuul 扛不住的根本原因。 - 它在链尾:前面所有过滤器改的头、路径、body 在此刻定型发出,所以在它之后的 pre 逻辑没有任何意义。
- 响应回写是另一个过滤器:
NettyWriteResponseFilter(order=-1)在请求阶段就注册好了响应的回写管道,拿到CLIENT_RESPONSE_ATTR后把下游响应体流式地写给客户端——请求和响应在这里"会师"。
七、串起响应式链路:一个请求的 Mono 图
把前面的类串起来,一次转发在订阅发生前构建的逻辑图大致是:
onErrorResume(全局异常 → 500 JSON)
└─ filterChain.filter(exchange)
├─ AdaptCachedBody(-2147482648)
├─ AuthGlobalFilter(-100) pre: 校验 token,短路 setComplete(401)
├─ NettyWriteResponse(-1) pre: 装配响应回写
├─ RouteToRequestUrl(0) pre: 拼 URL
├─ StripPrefix(1,路由级) pre: 改路径
├─ LoadBalancer(10150) pre: 选实例
└─ NettyRoutingFilter(MAX) pre: 发请求 → 订阅点
post: 响应沿链反向回流
三条响应式心智模型:
- 组装 ≠ 执行:
handle()一路返回新 Mono,只是在描述"要做什么",直到 Netty 层订阅才真正执行。 - 短路靠"不调 chain.filter()":鉴权失败直接返回
response.setComplete(),链上后续过滤器和下游服务一个都不会执行。 - 背压天然支持:下游响应体是
Flux<DataBuffer>,客户端读得慢就自然给网关和下游施压背压,整条链路没有"把大响应读进内存再发"的环节——所以不要在过滤器里无脑DataBufferUtils.join(body)把流式 body 聚合成大对象,会破坏内存优势。
八、自定义过滤器实战:鉴权 + 计时 + 限流
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtVerifier jwtVerifier;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 白名单(原始路径判断,此时 StripPrefix 还没执行,order=-100 保证)
if (path.startsWith("/api/auth/") || path.equals("/actuator/health")) {
return chain.filter(exchange);
}
String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || !jwtVerifier.valid(token)) {
return unauthorized(exchange, "缺少或无效的令牌");
}
// 用户身份传给下游:改写请求头(响应式地 mutate)
String userId = jwtVerifier.userId(token);
ServerHttpRequest mutated = request.mutate()
.header("X-User-Id", userId)
.build();
long start = System.currentTimeMillis();
return chain.filter(exchange.mutate().request(mutated).build())
.doFinally(signalType ->
log.info("[GW] {} {} userId={} cost={}ms",
request.getMethod(), path, userId,
System.currentTimeMillis() - start));
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
ServerHttpResponse resp = exchange.getResponse();
resp.setStatusCode(HttpStatus.UNAUTHORIZED);
resp.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = resp.bufferFactory()
.wrap(("{\"code\":401,\"message\":\"" + msg + "\"}").getBytes(UTF_8));
return resp.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() { return -100; } // 早于 StripPrefix(1)和路由级过滤器
}
这个例子同时用上了三个知识点:order=-100 保证看到原始路径(呼应 401 事故)、短路返回不调 chain.filter()、doFinally 做 post 计时(无论成功失败都执行)。
九、常见问题
9.1 为什么我的 GlobalFilter 对某个路由不生效?
检查三件事:① 是否注册成了 Spring Bean(@Component 漏了);② 是否被该路由的 Predicate 覆盖——GlobalFilter 只在"有路由匹配成功"后才会进 FilteringWebHandler,404 的请求走不到任何 GlobalFilter;③ order 是否在 NettyRoutingFilter 之后——getOrder 返回 Integer.MAX_VALUE 之后的逻辑不参与请求阶段。
9.2 多个过滤器 order 相同会怎样?
排序稳定但相对顺序依赖 Bean 注入列表的顺序,重启/加新过滤器后可能变化——开篇 401 事故就是这么来的。铁律:自定义过滤器显式给 order,且与内置过滤器拉开间隔,鉴权类放 -100 或更小,路径改写类保持默认(0~10),自定义业务改写放 100~1000,不要挤在 0。
9.3 过滤器里怎么读请求 body?为什么读完下游就收不到了?
Reactor 的请求体是一次性 Flux,读了就没了。需要读 body(如验签、日志)时用 ServerWebExchangeUtils.cacheRequestBody(exchange, ...) 或 ModifyRequestBodyGatewayFilterFactory,底层由 AdaptCachedBodyGlobalFilter 缓存后可重复读。生产中对大 body 的验签要评估内存,只缓冲必要范围。
9.4 怎么在过滤器里拿到请求开始时间和路由信息?
开始时间自己在 pre 阶段 exchange.getAttributes().put("startTime", ...);路由信息用 exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);真实目标 URL(负载均衡后)用 GATEWAY_REQUEST_URL_ATTR;原始 URL(路径改写前的集合)用 GATEWAY_ORIGINAL_REQUEST_URL_ATTR。这些属性名都是 ServerWebExchangeUtils 里的常量。
9.5 NettyRoutingFilter 之后还能改响应吗?
pre 阶段不能影响已发出的请求,但响应处理可以:NettyWriteResponseFilter(order=-1)在请求前就注册了回写管道,自定义 post 逻辑(.then()/doOnNext 操作 CLIENT_RESPONSE_ATTR)可以改响应状态和头;要改响应体用 ModifyResponseBodyGatewayFilterFactory,本质是在回写管道上插一层 map。顺序上你的 post 逻辑虽然写在自己的过滤器里,但执行时机在下游响应返回之后。
9.6 全异步链路里怎么做阻塞操作(如 JDBC 查权限)?
绝不能在过滤器线程里直接调阻塞 JDBC——会卡住 Netty 事件循环。正确姿势:Mono.fromCallable(() -> jdbcAuth(token)).subscribeOn(Schedulers.boundedElastic()),把阻塞调用调度到弹性线程池,过滤器链仍然是非阻塞的 Mono。更好的选择是权限数据走 Redis(用 Lettuce 的响应式 API),全程不离开事件循环。
十、总结
请求链路速查卡
Netty 收请求 → ServerWebExchange
→ DispatcherHandler.handle(handlerMappings 顺序匹配,第一个非空胜出)
→ RoutePredicateHandlerMapping
· routeLocator.getRoutes() 按顺序试 Predicate,第一个命中即止
· 路由顺序:YAML 上下顺序 / 显式 order;具体在前通配在后
→ FilteringWebHandler
· GlobalFilter(所有路由)+ GatewayFilter(路由级)合并统一排序
· DefaultGatewayFilterChain 责任链:不调 chain.filter() 即短路
→ -2147482648 缓存body
→ -100 鉴权(自定义,务必早于路径改写)
→ -1 NettyWriteResponse(装配回写)
→ 0 RouteToRequestUrl(拼 URL)
→ 1 StripPrefix 等(路径在此被改写!)
→ 10150 LoadBalancer(lb:// 服务名→实例IP)
→ MAX NettyRoutingFilter(Netty 非阻塞发请求,链尾订阅点)
→ 响应沿链反向回流到客户端
一句话
Spring Cloud Gateway 的源码主干只有三跳:DispatcherHandler 按顺序试 HandlerMapping,RoutePredicateHandlerMapping 用路由的 Predicate 顺序匹配、第一个命中的路由胜出(所以具体路由必须排在通配路由前面);FilteringWebHandler 把 GlobalFilter 和路由级 GatewayFilter 合并成一个列表按 order 统一排序,组成 DefaultGatewayFilterChain 责任链——所有"过滤器玄学"的答案都在 order 里:鉴权 order=-100 早于 StripPrefix(1)才能看到原始路径,order 相同相对顺序不稳定就是登录接口时好时坏 401 的根因,NettyRoutingFilter 固定在 Integer.MAX_VALUE 的链尾,它之前的 pre 逻辑才有效,它用 Reactor Netty 非阻塞发出请求,响应再由 order=-1 的 NettyWriteResponseFilter 沿链反向回写。读懂 WebFlux 只需记住三个心智模型:Mono 是组装后订阅才执行、短路就是不调 chain.filter()、整条链路靠 exchange 属性传上下文而不是 ThreadLocal——理解了"匹配按顺序、过滤器按 order、全程非阻塞"这三件事,网关 90% 的问题都能自己推导出来。
给团队的建议
| 项 | 建议 |
|---|---|
| 排障 | 先开 gateway DEBUG 日志:看路由匹配结果和 Sorted gatewayFilterFactories |
| 路由 | 具体路径在前、通配在后;必要时显式 order |
| 过滤器 | 自定义 order 拉开间隔(鉴权 -100,业务 100+),不要挤 0 |
| 路径 | 鉴权/白名单在 StripPrefix/RewritePath 之前判断,用原始路径 |
| 响应式 | 阻塞调用包 subscribeOn(boundedElastic),绝不阻塞事件循环 |
| body | 读取后用 cacheRequestBody/ModifyRequestBody,保证下游仍可收 |
| 测试 | 针对过滤器顺序写集成测试(白名单路径 + StripPrefix 组合) |
互动话题:你们在网关里遇到过过滤器顺序导致的诡异问题吗?鉴权逻辑放网关还是放各个服务内部?评论区聊聊。
参考资料
- Spring Cloud Gateway 官方文档
- Spring Cloud Gateway 源码(GitHub)
- WebFlux DispatcherHandler 源码解析
- Project Reactor 参考文档(Mono/Flux)
- Gateway 过滤器顺序官方说明(Global Filters)
标题:Spring Cloud Gateway 源码拆解:一个请求从进入到转发经历了什么
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/19/1789226385866.html
公众号:服务端技术精选
- 引言
- 一、全景:请求生命周期总览
- 二、DispatcherHandler:WebFlux 的总调度
- 2.1 它不是 Servlet 的 DispatcherServlet
- 2.2 ServerWebExchange:贯穿全链路的上下文
- 三、RoutePredicateHandlerMapping:路由怎么匹配
- 3.1 核心源码
- 3.2 路由的顺序从哪来
- 3.3 Predicate 组合:AND 语义
- 3.4 两个常见排查点
- 四、FilteringWebHandler:过滤器链的组装
- 4.1 核心源码:两类过滤器合并排序
- 4.2 GlobalFilter vs GatewayFilter
- 4.3 内置过滤器 order 速查表(必须背下来)
- 五、DefaultGatewayFilterChain:责任链与执行顺序
- 5.1 链式调用源码
- 5.2 pre / post 两阶段:Mono 的 defer 效应
- 5.3 实战坑:AuthFilter 与 StripPrefix 同为 order=0
- 六、转发三件套:URL 拼装 → 负载均衡 → Netty 发请求
- 6.1 RouteToRequestUrlFilter(order=0):拼接目标 URL
- 6.2 ReactiveLoadBalancerClientFilter(order=10150):服务名换实例
- 6.3 NettyRoutingFilter(order=Integer.MAX_VALUE):真正的转发
- 七、串起响应式链路:一个请求的 Mono 图
- 八、自定义过滤器实战:鉴权 + 计时 + 限流
- 九、常见问题
- 9.1 为什么我的 GlobalFilter 对某个路由不生效?
- 9.2 多个过滤器 order 相同会怎样?
- 9.3 过滤器里怎么读请求 body?为什么读完下游就收不到了?
- 9.4 怎么在过滤器里拿到请求开始时间和路由信息?
- 9.5 NettyRoutingFilter 之后还能改响应吗?
- 9.6 全异步链路里怎么做阻塞操作(如 JDBC 查权限)?
- 十、总结
- 请求链路速查卡
- 一句话
- 给团队的建议
- 参考资料
评论