☰
Spring Cloud Gateway过滤器工厂GatewayFilterFactory源码解析与实战
2026/10/16 1:18:31 网站建设 项目流程

在 Spring Cloud Gateway 里,过滤器是整个请求链路中最能体现“网关控制力”的部分。而GatewayFilterFactory这个过滤器工厂接口,则是所有内置过滤器能被 YAML 声明式配置出来的根基。我见过不少讲 Gateway 过滤器的文章,大多停留在“怎么配、配了能干嘛”这个层面,很少有人把工厂这一层真正拆开。这次借“Spring-Cloud-Gateway 源码解析 —— 过滤器 (4.2) 之 GatewayFilterFactory 过滤器工厂”这个主题,把我读源码时的关键理解和踩过的坑完整写出来。文章面向有 Java 基础和 Spring Boot 使用经验、想深入理解 Gateway 内部机制的读者,不需要你提前读过源码,跟着思路走一遍,你会发现过滤器工厂并没有想象中那么神秘。

1. 过滤器工厂在整个过滤器体系中的身位

1.1 先理清一个前提:GatewayFilter 和 GatewayFilterFactory 的关系

很多初学者第一次看到这两个类名会直接懵掉:一个是“过滤器”,一个是“过滤器工厂”,到底谁干活?

先说GatewayFilter。它是真正放在请求处理链路上执行逻辑的对象,接口定义非常简单,只有一个核心方法:

Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

拿到当前请求的ServerWebExchange,以及下一个要执行的过滤器链chain,决定是直接放行、修改请求后放行,还是拦截住不再往下走。它是一个典型的函数式接口,所以我们平时在工厂里写过滤器时经常直接返回一个 lambda。

那GatewayFilterFactory干什么?从名字上就能看出来,它是“生产 GatewayFilter 的工厂”。YAML 里写的过滤器是字符串形式的声明,比如:

filters: - AddRequestHeader=X-Request-Id, 123

网关不可能直接执行这段字符串,它必须把这段声明解析成一个实实在在的过滤器对象,然后在请求时执行。这个“从配置字符串到过滤器实例”的转换过程,就是GatewayFilterFactory的职责。

工厂接口的核心方法同样只有一个:

GatewayFilter apply(C config);

把配置对象C传进去,返回一个GatewayFilter。所以两者的关系一句话总结:GatewayFilter 负责执行,GatewayFilterFactory 负责根据配置生产执行器。

1.2 为什么非要抽象出一层工厂

如果只是需要一个过滤器,直接在代码里new一个不行吗?当然行,但问题是网关的场景是“声明式配置驱动”,并且同一个类型的过滤器要能出现在多条不同路由里、参数完全不同。

举个实际例子:我有两条路由,一条给订单服务用,需要加请求头X-Order-Source,另一条给用户服务用,需要加X-User-Source。如果不用工厂,我就得为这两个参数各写一个过滤器类,以后换参数又得新增,代码量爆炸。但有了AddRequestHeaderGatewayFilterFactory这个工厂,一切都变得简单:

routes: - id: order_route filters: - AddRequestHeader=X-Order-Source, gateway - id: user_route filters: - AddRequestHeader=X-User-Source, gateway

同一个工厂类,根据不同的配置参数,创建出两个配置不同的过滤器实例。这种“工厂 + 配置 → 产品”的模式,在需要把同一类逻辑复用到不同场景时非常好用。

更重要的是,工厂把“配置解析”和“逻辑执行”做了职责分离。配置怎么解析、类型怎么转换、缺省值怎么处理,全部收敛在工厂内部;过滤器本体只专注于一件事:拿到配置后怎么处理请求。这个分层思路,也是 Spring Cloud Gateway 能支撑几十个内置过滤器、同时又能轻松扩展自定义过滤器的根本原因。

1.3 工厂的两条实现路线

打开 Spring Cloud Gateway 源码,符合GatewayFilterFactory规范的实现类大致有两条路线:

第一条是纯粹的“局部过滤器工厂”,也就是基于AbstractGatewayFilterFactory派生出来的,比如AddRequestHeaderGatewayFilterFactory、RewritePathGatewayFilterFactory。它们生产出来的过滤器只作用于绑定了该过滤器的那条路由,离开路由范围就不生效。

第二条是“全局过滤器”,实现GlobalFilter接口,比如NettyRoutingFilter、ForwardPathFilter。全局过滤器不经过工厂,也不跟路由绑定,它们对所有请求生效,统一在FilteringWebHandler里被收集并按顺序执行。

这两条线经常让新手混淆。记住一点:GatewayFilterFactory只负责第一类,也就是局部过滤器。全局过滤器体系我们后面单独说,这一篇聚焦工厂本身。

2. 源码拆解:接口、抽象类、配置绑定三层逻辑

2.1 GatewayFilterFactory 接口:把“配置”变成“过滤器”

先看最顶层的接口定义。在org.springframework.cloud.gateway.filter.factory包下,核心内容大致如下:

public interface GatewayFilterFactory<C> extends ShortcutConfigurable { GatewayFilter apply(C config); Class<C> getConfigClass(); default String name() { return getName(); } default String getName() { return getClass().getSimpleName() .replace(GatewayFilterFactory.class.getSimpleName(), ""); } C newConfig(); }

注意几个关键点:

第一,接口带了泛型C。这个C是配置类型,每个工厂都可以定义自己的配置类。比如AddRequestHeaderGatewayFilterFactory的配置类里有两个字段:name和value,分别表示请求头名称和请求头值。

第二,name()这个默认方法非常巧妙。它通过类名的简单形式去掉GatewayFilterFactory后缀来得到工厂的名字。比如类名AddRequestHeaderGatewayFilterFactory,去掉后就是AddRequestHeader,正好对应 YAML 里写的过滤器名。这个方法的存在意味着:你只要按规范命名类,YAML 里的名字就不用额外注册。很多自定义过滤器不生效的问题,就是这里类名命名不符合规范导致的。

第三,newConfig()负责创建配置对象实例。在抽象类AbstractGatewayFilterFactory里面,这个方法通常基于构造时传入的配置类的Class做反射实例化。

2.2 AbstractGatewayFilterFactory:构造时固化配置类型

AbstractGatewayFilterFactory是绝大多数内置工厂的直接父类。它的实现核心非常简单,就是把你传入的配置类信息保存下来:

public abstract class AbstractGatewayFilterFactory<C> implements GatewayFilterFactory<C> { private final Class<C> configClass; protected AbstractGatewayFilterFactory(Class<C> configClass) { this.configClass = configClass; } @Override public Class<C> getConfigClass() { return configClass; } @Override public C newConfig() { try { return configClass.newInstance(); } catch (Exception e) { throw new IllegalStateException("Can not create config instance", e); } } @Override public String name() { return getName(); } }

所以你会看到所有内置工厂类的构造方法都长这样:

public AddRequestHeaderGatewayFilterFactory() { super(Config.class); }

这里的Config就是每个工厂自己定义的配置类。父类的newConfig()用反射创建出来的是空配置对象,后续负责把 YAML 里的参数值填充进去。

这里有一个需要留意的地方:configClass.newInstance()底层依赖无参构造器。很多人在自定义工厂时给配置类写了有参构造,或者把配置类定义成了非静态内部类,结果运行时报“无法实例化配置类”之类的错误。配置类一定要是public static的内部类或者单独一个类,并且提供无参构造。

2.3 Configurable 与 ShortcutConfigurable:字符串参数是怎么变成配置对象的

工厂有了配置类还不够,还必须解决一件核心事情:YAML 里的字符串参数,怎么塞进强类型的配置对象。

这里就引入了两个关键接口:Configurable和ShortcutConfigurable。

Configurable很简单,它只要求工厂能提供配置类类型和创建配置实例的能力,AbstractGatewayFilterFactory默认就实现了这部分。

真正复杂的是ShortcutConfigurable。这个接口承载了“短路配置”的能力。什么叫短路配置?就是下面这种写法:

filters: - AddRequestHeader=X-Request-Id, 123

与之相对的是展开写法:

filters: - name: AddRequestHeader args: name: X-Request-Id value: 123

短写方式更简洁,但网关需要额外的解析逻辑才能知道:X-Request-Id应该对应配置里的name字段,123应该对应value字段。这个对应关系,就是通过ShortcutConfigurable里的shortcutFieldOrder()方法来声明的。

AddRequestHeaderGatewayFilterFactory里的实现如下:

@Override public List<String> shortcutFieldOrder() { return Arrays.asList("name", "value"); }

它声明了短写参数按顺序映射到配置类的name、value两个属性。所以AddRequestHeader=a, b,最终解析出来的配置就是{name: "a", value: "b"}。

如果你没有实现shortcutFieldOrder(),默认返回空列表,那么即使你写了- 你的过滤器=参数1, 参数2,解析阶段也不会按字段顺序去绑定。这个坑我踩过一次,后面排查很久才发现是忘了声明字段顺序。

2.4 解析规则里的关键细节:shortcutFieldOrder 与类型转换

短写配置的解析过程远比表面看起来复杂。路由定义里存储的过滤器声明是一段字符串,FilterDefinition会先把等号右边的部分按逗号拆分成多个值,存进一个临时Map<String, String>里。

这时的 key 可能是name0、name1这样的临时下标,而不是配置字段本身的名称。真正把这些临时 key 和配置字段对齐的,是后面ShortcutConfigurable.normalize()方法干的活(不同版本里这个流程的实现差异比较大,2.x 用 Spring 的Binder,高版本引入了ConfigurationService,但思路一致)。

整个流程可以概括为四步:

  1. 解析 YAML 里的过滤器字符串,取出过滤器名和参数列表。
  2. 根据工厂名字从容器中找到对应的GatewayFilterFactory。
  3. 调用工厂的newConfig()创建空的配置对象。
  4. 把参数列表按shortcutFieldOrder()声明的顺序,一一代入配置对象,并根据配置字段的类型做类型转换。

类型转换这一步尤其重要。AddRequestHeader的配置字段是 String,直接赋值即可。但像RetryGatewayFilterFactory的配置里包含retries这种 Integer 字段,或者RequestRateLimiterGatewayFilterFactory的配置里包含 Map 类型字段,情况就完全不同了。

对于简单类型,Spring 的ConversionService会把字符串转成对应的 Integer、Boolean 等。对于复杂类型,比如 Map 或 List,如果参数值以{或[开头,网关会尝试按 JSON 来解析。这也是为什么在实际配置中,碰到复杂配置项时官方文档更推荐用展开写法。

3. 以 AddRequestHeaderGatewayFilterFactory 为例,看工厂完整生命周期

3.1 准备工作:从类声明到注册到容器

AddRequestHeaderGatewayFilterFactory是理解工厂机制最好的样本。它的完整实现不长,核心就两部分。

第一部分是构造器和配置类:

public class AddRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AddRequestHeaderGatewayFilterFactory.Config> { public static final String NAME = "AddRequestHeader"; public static final String NAME_KEY = "name"; public static final String VALUE_KEY = "value"; public AddRequestHeaderGatewayFilterFactory() { super(Config.class); } public static class Config { private String name; private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } }

第二部分是apply方法:

@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest().mutate() .headers(headers -> headers.add(config.getName(), config.getValue())) .build(); return chain.filter(exchange.mutate().request(request).build()); }; }

整个类的逻辑清晰到几乎不需要额外注释:配置类定义了两个字段,apply方法拿到配置后,修改请求头,然后继续执行过滤器链。

但你可能没想过,这个工厂是怎么从“源码里的一个类”变成“运行时可用实例”的。

Spring Cloud Gateway 在自动配置类里,会把容器中所有GatewayFilterFactory类型的 Bean 收集成一个Map<String, GatewayFilterFactory>。key 就是前面提到的name()返回的名字。之后在解析路由时,是从这个 Map 里按名字找工厂的。

所以,自定义工厂要生效,只需要一个动作:把它注册成 Spring Bean。用@Component注解挂在类上,或者在配置类里声明@Bean,都行。名字不需要手动注册,name()默认方法会帮你算好。

3.2 apply 方法体内发生的三件事

别看apply方法只有几行,里面其实藏着三个关键设计点。

第一件事是ServerHttpRequest的不可变修改。网关里的request对象不能直接 setHeader,必须通过mutate()创建一个新对象。这里有些人会疑惑:为什么明明已经有getHeaders().add()了,还要mutate().headers()再 add?原因在于响应式的世界里,请求对象一旦生成就假定为不可变,直接修改可能引发下游数据不一致。

第二件事是exchange.mutate().request(request).build()。过滤器的输入参数是ServerWebExchange,你要改它的 request,不能只替换 request 变量,因为后续过滤器拿到的是这个 exchange,不是你自己新创建的 request。正确做法是把新的 request 塞回 exchange 里,再把这个新 exchange 传给chain.filter()。

第三件事是返回chain.filter()的结果。这说明它是一个“放行型”过滤器:修改完头部后不拦截,继续执行后续过滤器和目标服务。如果你写的是鉴权类过滤器,可以在这里返回一个错误响应,从而中断链路。

3.3 YAML 里的两种写法到底怎么走的

理解了工厂内部机制后,再回头看配置声明的两条路径,就非常清楚了。

短写方案:

filters: - AddRequestHeader=X-Trace-Id, abc123

解析过程:FilterDefinition把AddRequestHeader识别为工厂名,把X-Trace-Id和abc123拆成两个参数。接着工厂的shortcutFieldOrder()声明了顺序是name, value,于是第一个参数自动落到config.name,第二个参数落到config.value。

展开方案:

filters: - name: AddRequestHeader args: name: X-Trace-Id value: abc123

解析过程更直接:args是标准的 Map,key 就是配置字段名,不需要经过位置匹配,直接把X-Trace-Id塞给name字段,abc123塞给value。

两种方案最终到达的Config对象完全相同。区别在于短写方案依赖字段声明的顺序,参数多了之后很容易配错;展开方案虽然啰嗦,但可读性和可维护性都更好。我的建议是:超过两个参数或者参数类型不是简单字符串时,优先用展开写法。

4. 手写一个自定义过滤器工厂:请求耗时统计

4.1 需求与设计:不要让截止时间把功能做重

光看不练等于没懂。写代码之前先想清楚需求。假设现在我需要一个过滤器,给每条请求打上时间戳,并且在响应里加上处理耗时,方便排查慢接口。这个功能在网关层做正合适,不侵入业务代码。

设计要点有三个:

第一,需要一个开关。不是所有环境都想开这个功能,所以配置类里要有一个enabled字段,默认值设为true。

第二,需要一个字段控制执行顺序。我们希望在请求一开始就计时,响应回来后再算差值,所以过滤器要在早期执行。可以用OrderedGatewayFilter包一层并指定 order。

第三,工厂要支持短写和展开两种配置方式。短写时只有一个开关参数,shortcutFieldOrder()返回包含enabled的列表即可。

4.2 完整代码与关键注释

直接上完整代码:

@Component public class RequestTimingGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimingGatewayFilterFactory.Config> { public RequestTimingGatewayFilterFactory() { super(Config.class); } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("enabled"); } @Override public GatewayFilter apply(Config config) { return new OrderedGatewayFilter((exchange, chain) -> { if (!config.isEnabled()) { return chain.filter(exchange); } long start = System.currentTimeMillis(); return chain.filter(exchange) .then(Mono.fromRunnable(() -> { long cost = System.currentTimeMillis() - start; exchange.getResponse().getHeaders() .add("X-Request-Cost", String.valueOf(cost)); })); }, 1000); } public static class Config { private boolean enabled = true; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } }

注意几个细节:

OrderedGatewayFilter是网关提供的一个工具类,构造时传入过滤器本身和执行顺序。我把 order 设成了1000,保证它在多数内置过滤器之前执行。

响应耗时写在then(Mono.fromRunnable(...))里,而不是直接在chain.filter(exchange)返回前计算。那是因为耗时要等下游处理完才能算出来,必须等chain.filter返回的Mono完成后再执行。我第一次写的时候直接把耗时加在chain.filter之前,结果每次耗时的值都是 0,这就是没理解响应式执行流的典型错误。

4.3 注册进 Spring 容器并与路由绑定

@Component注解已经让工厂被 Spring 管理了。容器启动后,自动配置会把它的名字解析为RequestTiming,因为我写的类名是RequestTimingGatewayFilterFactory,去掉后缀就是RequestTiming。

在 YAML 里绑定到某条路由:

spring: cloud: gateway: routes: - id: demo_route uri: http://localhost:8081 predicates: - Path=/demo/** filters: - RequestTiming=true

请求/demo路径的接口后,查看响应头,应该能看到X-Request-Cost字段。

这时候可以顺便验证一下开关的作用:把配置改成RequestTiming=false,重启后请求头就没有这个字段了。

如果配置类里没有enabled的 setter,或者字段声明成了private Boolean enabled但 getter 是getEnabled()而不是isEnabled(),Spring 的 Binder 可能会绑定失败或者拿不到值。这种问题不会直接报错,而是表现为“配置好像没生效”,排查起来往往很费时间。

5. 过滤器顺序、装配路径与常见坑

5.1 过滤器是从哪儿装配进请求管线的

前面的内容解决了一个问题:工厂怎么根据配置生产过滤器。接下来要理解生产出来的过滤器是怎么进入请求链路的。

完整的装配路径大概是这样的:

  1. RouteDefinitionRouteLocator读取路由定义,遍历每条路由的filterDefinitions。
  2. 逐个调用对应的GatewayFilterFactory.apply(),得到GatewayFilter列表。
  3. 把这些过滤器拼进路由的Route对象中保存。
  4. 请求进来后,FilteringWebHandler取出路由上的过滤器列表,再合并所有GlobalFilter,统一排序。
  5. 用GatewayFilterChain把它们串成一条责任链,逐层执行。

理解这条路径有几个实际意义。

第一个意义:过滤器工厂不是在每次请求时都调用apply()的,而是在路由定义加载阶段就调用了,加载完成后,请求执行的是工厂生产好的过滤器实例。所以在apply()里做数据库查询、远程调用等重逻辑是非常糟糕的设计,它会拖慢启动和路由刷新过程。

第二个意义:路由过滤器和全局过滤器是两套体系。工厂生产的过滤器只在对应路由生效;全局过滤器不经过工厂,对所有请求生效。如果你写了一个GlobalFilter,又想在某个路由上取消它,普通工厂做不到,得在全局过滤器内部写路由判断。

第三个意义:排序发生在请求阶段,不在装配阶段。所有过滤器在FilteringWebHandler里通过Ordered接口排序后,才进入执行链。

5.2 OrderedGatewayFilter 与排序规则

过滤器排序是实际开发里最容易出问题的地方。

Spring Cloud Gateway 的过滤器排序规则是order值越小,越先执行。两个地方可以控制:

  • 过滤器工厂apply()中,返回new OrderedGatewayFilter(gatewayFilter, order)。
  • 工厂的Config里放一个order字段,让用户通过配置控制,但这种方式要求工厂在 apply 里读取并应用。

内置过滤器的 order 各有不同。比如AddRequestHeaderGatewayFilterFactory的 order 被设成了Integer.MAX_VALUE,也就是尽可能靠后执行;而RetryGatewayFilterFactory的 order 比较靠前,因为重试肯定要包在最外层。

实际项目里我建议统一维护一个常量类,定义各个自定义过滤器的 order 值,避免“拍脑袋写数字”导致顺序失控。踩过最典型的坑是:我写了一个修改请求体的过滤器,order 设置得太大,结果执行时请求体已经被下游消费完了,改啥都不生效。后来把 order 调小,问题立刻消失。

5.3 常见问题速查表

把这段时间积攒的调试经验整理成一张速查表,方便遇到问题直接定位。

症状可能原因解决办法
过滤器完全不执行工厂没注册为 Spring Bean加@Component或@Bean注册
报错找不到过滤器工厂类名尾缀不是GatewayFilterFactory,或大小写不一致按XxxGatewayFilterFactory命名
短写配置参数全部为空没实现shortcutFieldOrder()按顺序返回配置字段名列表
短写参数顺序错乱逗号分隔值与字段顺序不匹配调整参数顺序,或改用展开写法
配置赋值失败/类型转换报错配置类没有无参构造、setter 或 getter 不规范配置类改为 public static,补全 JavaBean 属性
改了配置但行为没变化路由或过滤器缓存未刷新确认是否触发了路由刷新,必要时重启验证
过滤器顺序不对没指定 order 或数值不合理用OrderedGatewayFilter显式指定
启动时配置类实例化失败配置类是内部类且不是静态的改成 public static class

还有两个平时不太容易察觉的细节值得提一下。

一个是apply()方法可能在路由刷新时被反复调用。如果你在里面用了全局状态或者外部资源,要注意资源泄漏和状态污染。另一个是某些版本的 Gateway 里,工厂名字的前缀处理有细微差异,升级大版本后如果过滤器突然失效,可以先查一下这个版本的name()实现有没有变化。

另外,Debug 时有个很实用的小技巧:在RouteDefinitionRouteLocator.loadGatewayFilters这类方法上打断点,能直接看到工厂的加载和apply()的调用过程。我之前排查一个过滤器不生效的问题,就是靠断点发现工厂名被解析成了别的字符串,跟 YAML 里写的对不上。

这篇文章写到这里,核心内容基本都覆盖到了。最后分享一个我个人的体会:读源码最忌讳对着代码一行行死抠,要带着问题去找答案。比如这次分析GatewayFilterFactory,抓住的主线就是“配置字符串如何变成过滤器实例”,顺着这条线,接口设计、抽象类、配置绑定、注册装配、执行顺序这些知识点自然就串起来了。下次你再遇到自定义过滤器这类需求,至少知道从哪个类下手、要注意哪些坑,比单纯搜“怎么配置”靠谱得多。

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

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

立即咨询