1、概览 {#1概览}
上一篇文章《Spring Cloud Gateway 教程》中介绍了 Spring Cloud Gateway 网关框架。本文将带你了解如何在 Spring Cloud Gateway 中自定义 Filter。以及如何在 Filter 中修改请求和响应数据。
2、项目设置 {#2项目设置}
创建一个基本应用,并将其用作 API 网关。
2.1、Maven 配置 {#21maven-配置}
在使用 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 网关配置 {#22api-网关配置}
假设 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 {#3创建全局-filter}
一旦网关 Handler 确定请求与路由相匹配,网关就会将请求传递给过滤器链(Filter Chain)。这些过滤器可在请求发送前或发送后执行逻辑。
先从简单的全局过滤器开始,全局,意味着它将影响每一个请求。
首先,来看看如何在发送代理请求之前执行逻辑(也称为 "pre" Filter)
3.1、"Pre" Filter {#31pre-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 {#32post-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 {#4创建-gatewayfilter}
全局过滤器非常有用,但我们经常需要执行仅适用于某些路由的细粒度自定义网关 Filter 操作。
4.1、定义 GatewayFilterFactory {#41定义-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 {#42通过-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 {#43orderedgatewayfilter}
如果要配置 Filter 在 Filter Chain 中的位置,可以从 AbstractGatewayFilterFactory#apply
方法中返回一个 OrderedGatewayFilter
实例,而不是一个普通的 lambda 表达式:
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
// ...
}, 1);
}
4.4、编程式注册 GatewayFilter {#44编程式注册-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、高级场景 {#5高级场景}
到目前为止,我们所做的只是在网关流程的不同阶段输出日志。
一般来说,我们会通过 Filter 实现更高级的功能。例如:检查或者操作接收到的请求,修改响应,甚至在响应式流(Reactive Stream)中与其他不同的服务调用进行链式操作。
5.1、检查和修改请求 {#51检查和修改请求}
假设一个场景。服务过去是根据 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、修改响应 {#52修改响应}
继续相同的案例场景,现在定义一个 "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、将请求与其他服务链接 {#53将请求与其他服务链接}
在我们假设的场景中,下一步是依靠第三方服务来指示我们应该使用哪种 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、总结 {#6总结}
本文介绍了如何在 Spring Cloud Gateway 中自定义 Filter,以及如何通过 Filter 修改请求、响应,甚至是链接到其他的服务调用。
Ref:https://www.baeldung.com/spring-cloud-custom-gateway-filters