1. 概述
本文将带你深入掌握如何编写自定义的 Spring Cloud Gateway 过滤器。
我们之前在《探索 Spring Cloud Gateway》一文中介绍了该框架及其内置过滤器。但实际项目中,仅靠内置功能远远不够。✅
本文目标是让你真正“榨干”API 网关的能力——通过自定义过滤器实现更灵活、更强大的控制。
我们将从以下几个层面展开:
- ✅ 编写影响所有请求的 全局过滤器(Global Filter)
- ✅ 实现可按需绑定到特定路由的 网关过滤器工厂(GatewayFilterFactory)
- ✅ 进阶实战:修改请求/响应、链式调用其他服务(Reactive 风格)
踩坑提示:Spring Cloud Gateway 基于 WebFlux 和 Project Reactor,如果你对响应式编程不熟,建议先补一补基础,否则后面代码会看得一头雾水。
2. 项目搭建
先快速搭建一个基础的 API 网关应用。
2.1 Maven 依赖配置
使用 Spring Cloud 时,强烈建议通过 dependencyManagement
统一管理版本,避免依赖冲突。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后引入核心依赖(无需指定版本):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
📌 版本选择建议:
- 查看 Maven Central 获取最新 Release Train 版本
- 务必确认与当前 Spring Boot 版本兼容(参考 Spring Cloud 官方文档)
2.2 网关基础配置
假设本地 8081 端口运行着一个服务,访问 /resource
接口会返回一个字符串。
我们的目标是:当请求网关的 /service/resource
时,自动转发到该服务。
配置如下:
spring:
cloud:
gateway:
routes:
- id: service_route
uri: http://localhost:8081
predicates:
- Path=/service/**
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
其中 RewritePath
是内置过滤器,用于重写路径。比如 /service/foo
会被重写为 /foo
再转发。
为了便于调试,开启网关和 Netty 客户端日志:
logging:
level:
org.springframework.cloud.gateway: DEBUG
reactor.netty.http.client: DEBUG
3. 编写全局过滤器(Global Filter)
当请求匹配到某个路由后,会进入一个过滤器链。这些过滤器可分为两类:
- “pre” 过滤器:请求转发前执行
- “post” 过滤器:收到响应后执行
全局过滤器作用于所有请求,实现方式非常简单:✅
实现
GlobalFilter
接口并注册为 Spring Bean
3.1 编写“pre”全局过滤器
@Component
public class LoggingGlobalPreFilter implements GlobalFilter {
final Logger logger = LoggerFactory.getLogger(LoggingGlobalPreFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("Global Pre Filter executed");
return chain.filter(exchange);
}
}
逻辑清晰:打印日志 → 继续执行过滤器链。
3.2 编写“post”全局过滤器
“post”阶段稍微复杂,因为必须等整个链执行完才能执行后续逻辑。⚠️
利用 Reactor 的 then()
操作符即可实现:
@Configuration
public class LoggingGlobalFiltersConfigurations {
final Logger logger = LoggerFactory.getLogger(LoggingGlobalFiltersConfigurations.class);
@Bean
public GlobalFilter postGlobalFilter() {
return (exchange, chain) -> {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
logger.info("Global Post Filter executed");
}));
};
}
}
📌 核心点:
chain.filter(exchange)
返回Mono<Void>
,表示异步执行链.then(...)
在链执行完成后触发回调
调用 /service/resource
后,日志输出如下:
DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping: Route matched: service_route
INFO --- c.b.s.c.f.global.LoggingGlobalPreFilter: Global Pre Filter executed
DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Handler is being applied
INFO --- c.f.g.LoggingGlobalFiltersConfigurations: Global Post Filter executed
顺序完全符合预期。
3.3 合并“pre”和“post”逻辑
一个过滤器完全可以同时处理前后逻辑:
@Component
public class FirstPreLastPostGlobalFilter implements GlobalFilter, Ordered {
final Logger logger = LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("First Pre Global Filter");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
logger.info("Last Post Global Filter");
}));
}
@Override
public int getOrder() {
return -1; // 越小越早执行
}
}
⚠️ 注意 Ordered
接口的作用:
- 数值越小,优先级越高
- “pre” 阶段:按 order 升序执行
- “post” 阶段:按 order 降序执行(即后进先出)
4. 编写 GatewayFilter(局部过滤器)
全局过滤器太“粗”,很多时候我们需要只对特定路由生效的过滤器。
4.1 定义 GatewayFilterFactory
实现自定义 GatewayFilter
的标准姿势是继承 AbstractGatewayFilterFactory
:
@Component
public class LoggingGatewayFilterFactory extends
AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
final Logger logger = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// ...
}
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
// 构造函数、getter/setter...
}
}
📌 关键点:
Config
类用于接收外部配置参数- 必须提供无参构造 + setter,否则属性注入会失败 ❌
过滤器逻辑:
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (config.isPreLogger()) {
logger.info("Pre GatewayFilter logging: " + config.getBaseMessage());
}
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
logger.info("Post GatewayFilter logging: " + config.getBaseMessage());
}
}));
};
}
4.2 通过配置文件注册过滤器
在 application.yml
中使用:
spring:
cloud:
gateway:
routes:
- id: service_route
uri: http://localhost:8081
predicates:
- Path=/service/**
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- name: Logging
args:
baseMessage: My Custom Message
preLogger: true
postLogger: true
或使用简洁语法:
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- Logging=My Custom Message, true, true
要支持简洁语法,需重写 shortcutFieldOrder()
:
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("baseMessage", "preLogger", "postLogger");
}
4.3 控制过滤器执行顺序
如果想指定该过滤器在链中的位置,返回 OrderedGatewayFilter
:
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
// 过滤逻辑
}, 1); // order = 1
}
数值越小,越早执行。
4.4 编程式注册过滤器
也可以通过 Java 配置方式定义路由:
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, LoggingGatewayFilterFactory loggingFactory) {
return builder.routes()
.route("service_route_java_config", r -> r.path("/service/**")
.filters(f ->
f.rewritePath("/service(?<segment>/?.*)", "$\\{segment}")
.filter(loggingFactory.apply(new Config("My Custom Message", true, true))))
.uri("http://localhost:8081"))
.build();
}
这种方式更灵活,适合复杂逻辑或动态路由场景。
5. 进阶实战
光打印日志太小儿科了。真正的网关过滤器应该能:
- ✅ 修改请求头/参数
- ✅ 修改响应内容
- ✅ 链式调用其他服务(非阻塞)
5.1 检查并修改请求
场景:旧版 API 通过 locale
参数传语言,新版改用 Accept-Language
头。我们希望网关做兼容处理:
- 若有
Accept-Language
,保留 - 否则,取
locale
参数值作为语言 - 都没有则用默认值
- 最后删除
locale
参数
实现如下:
(exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
List<Locale.LanguageRange> acceptLanguage = request.getHeaders().getAcceptLanguage();
if (!acceptLanguage.isEmpty()) {
return chain.filter(exchange); // 已有语言头,直接放行
}
// 获取 locale 参数
String queryParamLocale = request.getQueryParams().getFirst("locale");
Locale requestLocale = Optional.ofNullable(queryParamLocale)
.map(Locale::forLanguageTag)
.orElse(config.getDefaultLocale());
// 修改请求头
ServerHttpRequest modifiedRequest = request.mutate()
.headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale)))
.build();
// 重新构建 exchange 并移除 query param
URI newUri = UriComponentsBuilder.fromUri(request.getURI())
.replaceQueryParams(new LinkedMultiValueMap<>())
.build()
.toUri();
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest)
.request(request -> request.uri(newUri))
.build();
return chain.filter(modifiedExchange);
}
📌 踩坑点:
- 请求是不可变对象,必须通过
mutate()
创建副本 - 修改 URI 需要重新构建整个
ServerWebExchange
5.2 修改响应
场景:后端服务返回自定义语言头 Bael-Custom-Language-Header
,我们想将其标准化为 Content-Language
。
(exchange, chain) -> {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst("locale"))
.ifPresent(qp -> {
String contentLanguage = response.getHeaders().getContentLanguage().getLanguage();
response.getHeaders().add("Bael-Custom-Language-Header", contentLanguage);
});
}));
}
✅ 注意:响应对象可直接修改,无需 mutate
。
⚠️ 顺序很重要!该过滤器必须在“pre”阶段修改请求的过滤器之后执行,否则拿不到原始 locale
参数。
5.3 链式调用其他服务(Reactive)
场景:语言选择依赖第三方服务,需先调用语言服务获取推荐语言。
关键:必须非阻塞调用,否则会破坏响应式流。
(exchange, chain) -> {
return WebClient.create()
.get()
.uri(config.getLanguageEndpoint())
.exchange()
.flatMap(response -> {
return response.statusCode().is2xxSuccessful() ?
response.bodyToMono(String.class) :
Mono.just(config.getDefaultLanguage());
})
.map(LanguageRange::parse)
.map(range -> {
// 修改请求头
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.headers(h -> h.setAcceptLanguage(range))
.build();
return exchange.mutate().request(newRequest).build();
})
.flatMap(chain::filter); // 继续执行链
}
✅ 核心思路:
- 使用
WebClient
发起异步调用 - 通过
flatMap
将外部服务响应与网关过滤链串联 - 最终仍返回
Mono<Void>
,保持响应式流完整性
6. 总结
本文系统讲解了 Spring Cloud Gateway 自定义过滤器的开发方式:
类型 | 适用场景 | 注册方式 |
---|---|---|
GlobalFilter |
全局通用逻辑(如日志、鉴权) | 实现接口 + @Component |
GatewayFilterFactory |
可配置、可复用的局部过滤器 | 继承 AbstractGatewayFilterFactory |
✅ 关键要点回顾:
- “pre” 过滤器直接返回
chain.filter()
- “post” 过滤器使用
.then()
回调 - 修改请求必须
mutate()
,响应可直接改 - 链式调用外部服务要用
WebClient + flatMap
- 执行顺序通过
Ordered
接口控制
所有完整示例代码已上传至 GitHub:
👉 https://github.com/eugenp/tutorials/tree/master/spring-cloud-modules/spring-cloud-gateway
测试时请确保目标服务(8081)已启动,并通过 Maven 执行集成测试。