51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

自定义 Spring Cloud Gateway 过滤器(Filter)

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" 执行将在较后阶段调用:

Spring Cloud Gateway Filter执行顺序

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
}

简单地说,这些字段是:

  1. baseMessage:日志条目中包含的自定义信息
  2. preLogger:表示过滤器是否应在转发请求前记录日志
  3. 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,但有些客户端仍在使用查询参数。

因此,我们希望在网关屏蔽这个差异:

  1. 如果接收到 Accept-Language 标头,则保留它
  2. 否则,使用 locale 查询参数值
  3. 如果查询参数也不存在,则使用默认的 locale
  4. 最后,删除 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

赞(3)
未经允许不得转载:工具盒子 » 自定义 Spring Cloud Gateway 过滤器(Filter)