自定义 Spring Cloud Gateway 过滤器(Filter)
1、概览
上一篇文章《Spring Cloud Gateway 教程》中介绍了 Spring Cloud Gateway 网关框架。本文将带你了解如何在 Spring Cloud Gateway 中自定义 Filter。以及如何在 Filter 中修改请求和响应数据。
2、项目设置
创建一个基本应用,并将其用作 API 网关。
2.1、Maven 配置
在使用 Spring Cloud 时,往往通过 <dependencyManagement>
来管理组件的版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
添加 Spring Cloud Gateway,无需指定使用的实际版本:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
最新的 Spring Cloud 版本可通过 Maven Central 中找到。当然,需要注意使用的 Spring Cloud 版本需要与 Spring Boot 版本兼容。
2.2、API 网关配置
假设 API 服务在本地 8081 端口运行,在 /resource
端点暴露了一个简单的字符串资源。
接下来,配置网关,把请求代理到该服务。简而言之,当请求网关的 URI 路径中带有 /service
前缀的请求时,网关将把请求转发给该服务。
也就是说,当在网关中调用 /service/resource
时,应该会收到字符串响应。
在 application.yaml
中配置此路由:
spring:
cloud:
gateway:
routes:
- id: service_route
uri: http://localhost:8081
predicates:
- Path=/service/**
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
此外,为了追踪网关的处理过程,还要启用一些日志:
logging:
level:
org.springframework.cloud.gateway: DEBUG
reactor.netty.http.client: DEBUG
3、创建全局 Filter
一旦网关 Handler 确定请求与路由相匹配,网关就会将请求传递给过滤器链(Filter Chain)。这些过滤器可在请求发送前或发送后执行逻辑。
先从简单的全局过滤器开始,全局,意味着它将影响每一个请求。
首先,来看看如何在发送代理请求之前执行逻辑(也称为 “pre” Filter)
3.1、“Pre” Filter
要创建自定义全局过滤器,只需实现 Spring Cloud Gateway GlobalFilter
接口,并将其作为 Bean 添加到上下文中:
出于演示目的,只是简单地在 Filter
中输出一条日志信息。
@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);
}
}
如上,当这个过滤器被执行的时候就会输出日志记录,然后继续执行过滤器链。
如果你不熟悉响应式编程模型和 Spring Webflux API,这可能会有点不好理解。
3.2、“Post” Filter
还有一点需要注意,那就是 GlobalFilter
接口只定义了一个方法。因此,它可以用 lambda 来表示。
例如,可以在配置类中定义 “Post” 过滤器:
@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");
}));
};
}
}
简单来说,上述 Filter 在过滤器链完成执行后运行了一个新的 Mono
实例。
现在,尝试在网关服务中调用 /service/resource
URL 并查看控制台输出的日志:
DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping:
Route matched: service_route
DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping:
Mapping [Exchange: GET http://localhost/service/resource]
to Route{id='service_route', uri=http://localhost:8081, order=0, predicate=Paths: [/service/**],
match trailing slash: true, gatewayFilters=[[[RewritePath /service(?<segment>/?.*) = '${segment}'], order = 1]]}
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: {uri=http://localhost:8081/resource, method=GET}
DEBUG --- r.n.http.client.HttpClientOperations:
[id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081]
Received response (auto-read:false) : [Content-Type=text/html;charset=UTF-8, Content-Length=16]
INFO --- c.f.g.LoggingGlobalFiltersConfigurations:
Global Post Filter executed
DEBUG --- r.n.http.client.HttpClientOperations:
[id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Received last HTTP packet
如你所见,在网关将请求转发给服务之前和之后,对应的过滤器都执行了。
可以将 “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
接口,来控制 Filter 在 Filter Chain 中的位置。
由于过滤器链(Filter Chain)的性质,优先级较低(在链中的顺序较低)的过滤器将在较早阶段执行其 “pre” 逻辑,但其 “post” 执行将在较后阶段调用:
4、创建 GatewayFilter
全局过滤器非常有用,但我们经常需要执行仅适用于某些路由的细粒度自定义网关 Filter 操作。
4.1、定义 GatewayFilterFactory
要实现 GatewayFilter
,必须实现 GatewayFilterFactory
接口。Spring Cloud Gateway 还提供了一个抽象类来简化这一过程,即 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 {
// ...
}
}
如上,定义了 GatewayFilterFactory
的基本结构。在初始化过滤器时,使用 Config
类来自定义过滤器。
例如,可以在配置中定义三个基本字段:
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
//构造函数、Get、Set
}
简单地说,这些字段是:
baseMessage
:日志条目中包含的自定义信息preLogger
:表示过滤器是否应在转发请求前记录日志postLogger
:表示过滤器是否应在收到代理服务的响应后记录日志
现在,可以使用这些配置来获取一个 GatewayFilter
实例,这同样可以用 lambda 函数来表示:
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// Pre-processing
if (config.isPreLogger()) {
logger.info("Pre GatewayFilter logging: "
+ config.getBaseMessage());
}
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// Post-processing
if (config.isPostLogger()) {
logger.info("Post GatewayFilter logging: "
+ config.getBaseMessage());
}
}));
};
}
4.2、通过 Properties 注册 GatewayFilter
将 Filter 注册到之前在 application.yaml
中定义的路由上:
...
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- name: Logging
args:
baseMessage: My Custom Message
preLogger: true
postLogger: true
只需指定配置参数即可。这里有一点很重要,必须需要在 LoggingGatewayFilterFactory.Config
类中配置无参数构造函数和 Setter 方法,这样这种方法才能正常工作。
也可以使用更紧凑的方式来配置 Filter:
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- Logging=My Custom Message, true, true
这需要对 Factory 再做一些调整。简而言之,必须重写 shortcutFieldOrder
方法,以指定紧凑方式属性的顺序和参数数量:
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("baseMessage",
"preLogger",
"postLogger");
}
4.3、OrderedGatewayFilter
如果要配置 Filter 在 Filter Chain 中的位置,可以从 AbstractGatewayFilterFactory#apply
方法中返回一个 OrderedGatewayFilter
实例,而不是一个普通的 lambda 表达式:
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
// ...
}, 1);
}
4.4、编程式注册 GatewayFilter
还可以通过编程式注册 Filter。
重新定义前面的路由,这次设置一个 RouteLocator
Bean:
@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、高级场景
到目前为止,我们所做的只是在网关流程的不同阶段输出日志。
一般来说,我们会通过 Filter 实现更高级的功能。例如:检查或者操作接收到的请求,修改响应,甚至在响应式流(Reactive Stream)中与其他不同的服务调用进行链式操作。
5.1、检查和修改请求
假设一个场景。服务过去是根据 locale 查询参数来提供内容的。后来,更改了 API,改用 Accept-Language Header,但有些客户端仍在使用查询参数。
因此,我们希望在网关屏蔽这个差异:
- 如果接收到
Accept-Language
标头,则保留它 - 否则,使用
locale
查询参数值 - 如果查询参数也不存在,则使用默认的
locale
- 最后,删除
locale
查询参数
这里只关注 Filter 中的实现逻辑,其他的内容你可以在文末的 Github 仓库中找到。
把网关 Filter 配置为 “pre” Filter:
(exchange, chain) -> {
if (exchange.getRequest()
.getHeaders()
.getAcceptLanguage()
.isEmpty()) {
// 填充 Accept-Language header 。。。
}
// 删除查询参数
return chain.filter(exchange);
};
如上,通过 ServerHttpRequest
对象访问 Header。
还可以用同样的方式访问其他的属性。
String queryParamLocale = exchange.getRequest()
.getQueryParams()
.getFirst("locale");
Locale requestLocale = Optional.ofNullable(queryParamLocale)
.map(l -> Locale.forLanguageTag(l))
.orElse(config.getDefaultLocale());
现在,使用 mutate()
方法来修改请求,框架会为实体创建一个 Decorator
(装饰器),同时保持原始对象不变。
修改 Header 很简单,因为可以获取 HttpHeaders
Map 对象的引用:
exchange.getRequest()
.mutate()
.headers(h -> h.setAcceptLanguageAsLocales(
Collections.singletonList(requestLocale)))
修改 URI 也一样,必须从原始 exchange
对象中获取一个新的 ServerWebExchange
实例,并修改原始 ServerHttpRequest
实例:
ServerWebExchange modifiedExchange = exchange.mutate()
// 在这里修改原始请求:
.request(originalRequest -> originalRequest)
.build();
return chain.filter(modifiedExchange);
现在,删除查询参数更新原始请求 URI:
originalRequest -> originalRequest.uri(
UriComponentsBuilder.fromUri(exchange.getRequest()
.getURI())
.replaceQueryParams(new LinkedMultiValueMap<String, String>())
.build()
.toUri())
5.2、修改响应
继续相同的案例场景,现在定义一个 “Post” Filter。假设服务会检索一个自定义 Header,以指示它最终选择的语言,而不是使用传统的 Content-Language Header。
因此,我们希望 Filter 添加这个响应 Header,但前提是请求包含上一节中介绍的 locale Header。
(exchange, chain) -> {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
Optional.ofNullable(exchange.getRequest()
.getQueryParams()
.getFirst("locale"))
.ifPresent(qp -> {
String responseContentLanguage = response.getHeaders()
.getContentLanguage()
.getLanguage();
response.getHeaders()
.add("Bael-Custom-Language-Header", responseContentLanguage);
});
}));
}
如上,这可以轻松获取 response
对象的引用,而且不需要像 request
那样创建一个副本来修改它。
这是一个很好的例子,说明了链中 Filter 顺序的重要性;如果在上一节创建的 Filter 之后配置执行该 Filter,那么此处的 exchange
对象将包含对 ServerHttpRequest
的引用,而该引用将永远不会有任何查询参数。
在执行了所有 “Pre” Filter 后,再触发这个 Filter 也没有关系,因为仍然可以引用原始请求,这要归功于 mutate()
逻辑。
5.3、将请求与其他服务链接
在我们假设的场景中,下一步是依靠第三方服务来指示我们应该使用哪种 Accept-Language Header。
因此,创建一个新的 Filter 来调用该服务,并将其响应体用作代理服务 API 的请求 Header。
在响应式环境中,这意味着通过链式请求来避免阻塞异步执行。
在 Filter 中,首先向 Language Service 发起请求:
(exchange, chain) -> {
return WebClient.create().get()
.uri(config.getLanguageEndpoint())
.exchange()
// ...
}
注意,这返回了 Fluent 风格的操作,用于链式地将调用的输出与代理请求连接在一起。
下一步是提取 Language(从响应体中提取,如果响应不成功,则从配置中提取)并进行解析:
// ...
.flatMap(response -> {
return (response.statusCode()
.is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage());
}).map(LanguageRange::parse)
// ...
最后,像之前一样将 LanguageRange 值设置为请求 Header,然后继续过滤器链(Filter Chain):
.map(range -> {
exchange.getRequest()
.mutate()
.headers(h -> h.setAcceptLanguage(range))
.build();
return exchange;
}).flatMap(chain::filter);
交互将以非阻塞方式进行。
6、总结
本文介绍了如何在 Spring Cloud Gateway 中自定义 Filter,以及如何通过 Filter 修改请求、响应,甚至是链接到其他的服务调用。
Ref:https://www.baeldung.com/spring-cloud-custom-gateway-filters