Writing Custom Spring Cloud Gateway Filters – 编写自定义Spring Cloud Gateway过滤器

最后修改: 2019年 11月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn how to write custom Spring Cloud Gateway filters.

在本教程中,我们将学习如何编写自定义Spring Cloud Gateway过滤器。

We introduced this framework in our previous post, Exploring the New Spring Cloud Gateway, where we had a look at many built-in filters.

我们在上一篇文章探索新的Spring Cloud Gateway中介绍了这一框架,在这篇文章中我们看了许多内置的过滤器。

On this occasion we’ll go deeper, we’ll write custom filters to get the most out of our API Gateway.

这次我们将更深入,我们将编写自定义过滤器,以最大限度地利用我们的API网关。

First, we’ll see how we can create global filters that will affect every single request handled by the gateway. Then, we’ll write gateway filter factories, that can be applied granularly to particular routes and requests.

首先,我们将看到我们如何创建全局过滤器,以影响网关处理的每一个请求。然后,我们将编写网关过滤器工厂,它可以细化地应用于特定的路由和请求。

Finally, we’ll work on more advanced scenarios, learning how to modify the request or the response, and even how to chain the request with calls to other services, in a reactive fashion.

最后,我们将在更高级的场景下工作,学习如何修改请求或响应,甚至如何以反应式的方式将请求与对其他服务的调用联系起来。

2. Project Setup

2.项目设置

We’ll start by setting up a basic application that we’ll be using as our API Gateway.

我们将首先设置一个基本的应用程序,作为我们的API网关使用。

2.1. Maven Configuration

2.1.Maven配置

When working with Spring Cloud libraries, it’s always a good choice to set up a dependency management configuration to handle the dependencies for us:

在使用Spring Cloud库时,设置一个依赖性管理配置来为我们处理依赖关系总是一个不错的选择。

<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>

Now we can add our Spring Cloud libraries without specifying the actual version we’re using:

现在我们可以添加我们的Spring Cloud库,而不必指定我们使用的实际版本。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

The latest Spring Cloud Release Train version can be found using the Maven Central search engine. Of course, we should always check that the version is compatible with the Spring Boot version we’re using in the Spring Cloud documentation.

最新的Spring Cloud Release Train版本可以通过Maven Central搜索引擎找到。当然,我们应该始终检查该版本是否与我们在Spring Cloud文档中使用的Spring Boot版本兼容。

2.2. API Gateway Configuration

2.2.API网关配置

We’ll assume there is a second application running locally in port 8081, that exposes a resource (for simplicity’s sake, just a simple String) when hitting /resource.

我们假设有第二个应用程序在本地运行,端口为8081,当点击/resource时,它暴露了一个资源(为了简单起见,只是一个简单的String)。

With this in mind, we’ll configure our gateway to proxy requests to this service. In a nutshell, when we send a request to the gateway with a /service prefix in the URI path, we’ll be forwarding the call to this service.

考虑到这一点,我们将配置我们的网关来代理请求到这个服务。简而言之,当我们向网关发送一个URI路径中带有/service 前缀的请求时,我们将把调用转发给这个服务。

So, when we call /service/resource in our gateway, we should receive the String response.

因此,当我们在网关中调用/service/resource 时,我们应该收到String 响应。

To achieve this, we’ll configure this route using application properties:

为了实现这一目标,我们将使用应用程序属性配置该路由。

spring:
  cloud:
    gateway:
      routes:
      - id: service_route
        uri: http://localhost:8081
        predicates:
        - Path=/service/**
        filters:
        - RewritePath=/service(?<segment>/?.*), $\{segment}

And additionally, to be able to trace the gateway process properly, we’ll enable some logs as well:

此外,为了能够正确地追踪网关的过程,我们也将启用一些日志。

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

3. Creating Global Filters

3.创建全局过滤器

Once the gateway handler determines that a request matches a route, the framework passes the request through a filter chain. These filters may execute logic before the request is sent, or afterward.

一旦网关处理程序确定一个请求与一个路由相匹配,该框架就会将该请求通过一个过滤器链。这些过滤器可以在发送请求之前或之后执行逻辑。

In this section, we’ll start by writing simple global filters. That means, that it’ll affect every single request.

在本节中,我们将从编写简单的全局过滤器开始。这意味着,它将影响每一个请求。

First, we’ll see how we can execute the logic before the proxy request is sent (also known as a “pre” filter)

首先,我们将看到如何在代理请求发送前执行逻辑(也称为 “预 “过滤器)。

3.1. Writing Global “Pre” Filter Logic

3.1.编写全局 “预 “过滤器逻辑

As we said, we’ll create simple filters at this point, since the main objective here is only to see that the filter is actually getting executed at the correct moment; just logging a simple message will do the trick.

正如我们所说的,我们将在这时创建简单的过滤器,因为这里的主要目的只是看过滤器是否真的在正确的时刻被执行;只要记录一个简单的信息就可以了。

All we have to do to create a custom global filter is to implement the Spring Cloud Gateway GlobalFilter interface, and add it to the context as a bean:

我们创建自定义全局过滤器所要做的就是实现Spring Cloud Gateway的GlobalFilter接口,并将其作为一个bean添加到上下文中:

@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);
    }
}

We can easily see what’s going on here; once this filter is invoked, we’ll log a message, and continue with the execution of the filter chain.

我们可以很容易地看到这里发生了什么;一旦这个过滤器被调用,我们将记录一条信息,并继续执行过滤器链。

Let’s now define a “post” filter, which can be a little bit trickier if we’re not familiarized with the Reactive programming model and the Spring Webflux API.

现在我们来定义一个 “post “过滤器,如果我们不熟悉Reactive编程模型和Spring Webflux API,这可能会有点棘手。

3.2. Writing Global “Post” Filter Logic

3.2.编写全局 “后置 “过滤器逻辑

One other thing to notice about the global filter we just defined is that the GlobalFilter interface defines only one method. Thus, it can be expressed as a lambda expression, allowing us to define filters conveniently.

关于我们刚刚定义的全局过滤器,还有一点需要注意的是,GlobalFilter接口只定义了一个方法。因此,它可以被表达为lambda表达式,使我们可以方便地定义过滤器。

For example, we can define our “post” filter in a configuration class:

例如,我们可以在一个配置类中定义我们的 “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");
              }));
        };
    }
}

Simply put, here we’re running a new Mono instance after the chain completed its execution.

简单地说,这里我们是在链子执行完毕后运行一个新的Mono实例。

Let’s try it out now by calling the /service/resource URL in our gateway service, and checking out the log console:

现在让我们通过调用网关服务中的/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

As we can see, the filters are effectively executed before and after the gateway forwards the request to the service.

正如我们所看到的,过滤器在网关转发请求到服务之前和之后被有效执行。

Naturally, we can combine “pre” and “post” logic in a single filter:

当然,我们可以在一个过滤器中结合 “前 “和 “后 “的逻辑。

@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;
    }
}

Note we can also implement the Ordered interface if we care about the placement of the filter in the chain.

注意,如果我们关心过滤器在链中的位置,我们也可以实现Ordered接口。

Due to the nature of the filter chain, a filter with lower precedence (a lower order in the chain) will execute its “pre” logic in an earlier stage, but it’s “post” implementation will get invoked later:

由于过滤器链的性质,优先级较低的过滤器(在链中顺序较低)将在较早阶段执行其 “前 “逻辑,但它的 “后 “执行将在稍后被调用:

4. Creating GatewayFilters

4.创建GatewayFilters

Global filters are quite useful, but we often need to execute fine-grained custom Gateway filter operations that apply to only some routes.

全局过滤器相当有用,但我们经常需要执行细粒度的自定义Gateway过滤器操作,只适用于某些路由。

4.1. Defining the GatewayFilterFactory

4.1.定义GatewayFilterFactory

In order to implement a GatewayFilter, we’ll have to implement the GatewayFilterFactory interface. Spring Cloud Gateway also provides an abstract class to simplify the process, the AbstractGatewayFilterFactory class:

为了实现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 {
        // ...
    }
}

Here we’ve defined the basic structure of our GatewayFilterFactory. We’ll use a Config class to customize our filter when we initialize it.

这里我们定义了我们的GatewayFilterFactory的基本结构。当我们初始化过滤器时,我们将使用一个Config类来定制它。

In this case, for example, we can define three basic fields in our configuration:

例如,在这种情况下,我们可以在配置中定义三个基本字段。

public static class Config {
    private String baseMessage;
    private boolean preLogger;
    private boolean postLogger;

    // contructors, getters and setters...
}

Simply put, these fields are:

简单地说,这些领域是。

  1. a custom message that will be included in the log entry
  2. a flag indicating if the filter should log before forwarding the request
  3. a flag indicating if the filter should log after receiving the response from the proxied service

And now we can use these configurations to retrieve a GatewayFilter instance, which again, can be represented with a lambda function:

现在我们可以使用这些配置来检索一个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. Registering the GatewayFilter with Properties

4.2.注册带有属性的GatewayFilter

We can now easily register our filter to the route we defined previously in the application properties:

现在我们可以很容易地将我们的过滤器注册到我们之前在应用程序属性中定义的路由。

...
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- name: Logging
  args:
    baseMessage: My Custom Message
    preLogger: true
    postLogger: true

We simply have to indicate the configuration arguments. An important point here is that we need a no-argument constructor and setters configured in our LoggingGatewayFilterFactory.Config class for this approach to work properly.

我们只需指出配置参数。这里很重要的一点是,我们需要在我们的LoggingGatewayFilterFactory.Config类中配置一个无参数的构造函数和设置器,以便这种方法能够正常工作。

If we want to configure the filter using the compact notation instead, then we can do:

如果我们想用紧凑的符号来代替配置过滤器,那么我们可以这样做。

filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- Logging=My Custom Message, true, true

We’ll need to tweak our factory a little bit more. In short, we have to override the shortcutFieldOrder method, to indicate the order and how many arguments the shortcut property will use:

我们需要对我们的工厂再做一些调整。简而言之,我们必须覆盖shortcutFieldOrder方法,以指示快捷方式属性的顺序和使用多少个参数。

@Override
public List<String> shortcutFieldOrder() {
    return Arrays.asList("baseMessage",
      "preLogger",
      "postLogger");
}

4.3. Ordering the GatewayFilter

4.3.对GatewayFilter的排序

If we want to configure the position of the filter in the filter chain, we can retrieve an OrderedGatewayFilter instance from the AbstractGatewayFilterFactory#apply method instead of a plain lambda expression:

如果我们想配置过滤器在过滤器链中的位置,我们可以从AbstractGatewayFilterFactory#apply 方法中检索一个OrderedGatewayFilter instance,而不是一个普通的lambda expression。

@Override
public GatewayFilter apply(Config config) {
    return new OrderedGatewayFilter((exchange, chain) -> {
        // ...
    }, 1);
}

4.4. Registering the GatewayFilter Programmatically

4.4.以编程方式注册GatewayFilter

Furthermore, we can register our filter programmatically, too. Let’s redefine the route we’ve been using, this time by setting up a RouteLocator bean:

此外,我们也可以通过编程来注册我们的过滤器。让我们重新定义我们一直在使用的路由,这次是通过设置一个RouteLocatorbean。

@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. Advanced Scenarios

5.高级场景

So far, all we’ve been doing is logging a message at different stages of the gateway process.

到目前为止,我们所做的只是在网关过程的不同阶段记录一条信息。

Usually, we need our filters to provide more advanced functionality. For instance, we may need to check or manipulate the request we received, modify the response we’re retrieving, or even chain the reactive stream with calls to other different services.

通常情况下,我们需要我们的过滤器提供更高级的功能。例如,我们可能需要检查或操作我们收到的请求,修改我们正在检索的响应,甚至用调用其他不同的服务来链动反应式流。

Next, we’ll see examples of these different scenarios.

接下来,我们将看到这些不同情景的例子。

5.1. Checking and Modifying the Request

5.1.检查和修改请求

Let’s imagine a hypothetical scenario. Our service used to serve its content based on a locale query parameter. Then, we changed the API to use the Accept-Language header instead, but some clients are still using the query parameter.

让我们想象一个假设的场景。我们的服务曾经基于locale查询参数来提供其内容。然后,我们改变了API,使用Accept-Language header来代替,但一些客户仍然在使用查询参数。

Thus, we want to configure the gateway to normalize following this logic:

因此,我们要配置网关,使其按照这个逻辑进行规范化。

  1. if we receive the Accept-Language header, we want to keep that
  2. otherwise, use the locale query parameter value
  3. if that’s not present either, use a default locale
  4. finally, we want to remove the locale query param

Note: To keep things simple here, we’ll focus only on the filter logic; to have a look at the whole implementation we’ll find a link to the codebase at the end of the tutorial.

注意:为了保持简单,我们将只关注过滤器的逻辑;要看整个实现,我们将在教程的最后找到一个代码库的链接。

Let’s configure our gateway filter as a “pre” filter then:

让我们把我们的网关过滤器配置成一个 “预 “过滤器。

(exchange, chain) -> {
    if (exchange.getRequest()
      .getHeaders()
      .getAcceptLanguage()
      .isEmpty()) {
        // populate the Accept-Language header...
    }

    // remove the query param...
    return chain.filter(exchange);
};

Here we’re taking care of the first aspect of the logic. We can see that inspecting the ServerHttpRequest object is really simple. At this point, we accessed only its headers, but as we’ll see next, we can obtain other attributes just as easily:

在这里,我们要处理的是逻辑的第一个方面。我们可以看到,检查ServerHttpRequest对象真的很简单。在这一点上,我们只访问了它的头文件,但正如我们接下来要看到的,我们可以同样容易地获得其他属性。

String queryParamLocale = exchange.getRequest()
  .getQueryParams()
  .getFirst("locale");

Locale requestLocale = Optional.ofNullable(queryParamLocale)
  .map(l -> Locale.forLanguageTag(l))
  .orElse(config.getDefaultLocale());

Now we’ve covered the next two points of the behavior. But we haven’t modified the request, yet. For this, we’ll have to make use of the mutate capability.

现在我们已经涵盖了行为的后两点。但是我们还没有修改请求。为此,我们必须使用mutate capability。

With this, the framework will be creating a Decorator of the entity, maintaining the original object unchanged.

有了这个,框架将创建一个实体的装饰器,保持原始对象不变。

Modifying the headers is simple because we can obtain a reference to the HttpHeaders map object:

修改头文件很简单,因为我们可以获得对HttpHeadersmap对象的引用。

exchange.getRequest()
  .mutate()
  .headers(h -> h.setAcceptLanguageAsLocales(
    Collections.singletonList(requestLocale)))

But, on the other hand, modifying the URI is not a trivial task.

但是,另一方面,修改URI也不是一件简单的事情。

We’ll have to obtain a new ServerWebExchange instance from the original exchange object, modifying the original ServerHttpRequest instance:

我们必须从原来的exchange对象中获得一个新的ServerWebExchange实例,修改原来的ServerHttpRequest实例。

ServerWebExchange modifiedExchange = exchange.mutate()
  // Here we'll modify the original request:
  .request(originalRequest -> originalRequest)
  .build();

return chain.filter(modifiedExchange);

Now it’s time to update the original request URI by removing the query params:

现在是时候通过删除查询参数来更新原始请求URI了。

originalRequest -> originalRequest.uri(
  UriComponentsBuilder.fromUri(exchange.getRequest()
    .getURI())
  .replaceQueryParams(new LinkedMultiValueMap<String, String>())
  .build()
  .toUri())

There we go, we can try it out now. In the codebase, we added log entries before calling the next chain filter to see exactly what is getting sent in the request.

好了,我们现在可以试试了。在代码库中,我们在调用下一个链式过滤器之前添加了日志条目,以查看请求中到底发送了什么。

5.2. Modifying the Response

5.2.修改响应

Proceeding with the same case scenario, we’ll define a “post” filter now. Our imaginary service used to retrieve a custom header to indicate the language it finally chose instead of using the conventional Content-Language header.

按照同样的情况进行,我们现在要定义一个 “post “过滤器。我们想象中的服务用来检索一个自定义的头,以表明它最终选择的语言,而不是使用传统的Content-Language头。

Hence, we want our new filter to add this response header, but only if the request contains the locale header we introduced in the previous section.

因此,我们希望我们的新过滤器能够添加这个响应头,但只有当请求包含我们在上一节中介绍的locale头时才会添加。

(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);
                });
        }));
}

We can obtain a reference to the response object easily, and we don’t need to create a copy of it to modify it, as with the request.

我们可以很容易地获得对响应对象的引用,而且我们不需要像对待请求那样创建一个副本来修改它。

This is a good example of the importance of the order of the filters in the chain; if we configure the execution of this filter after the one we created in the previous section, then the exchange object here will contain a reference to a ServerHttpRequest that will never have any query param.

这是一个很好的例子,说明了过滤器在链中的顺序的重要性;如果我们把这个过滤器的执行配置在我们在上一节创建的过滤器之后,那么这里的exchange对象将包含对ServerHttpRequest的引用,它将永远不会有任何查询参数。

It doesn’t even matter that this is effectively triggered after the execution of all the “pre” filters because we still have a reference to the original request, thanks to the mutate logic.

这甚至并不重要,因为我们仍然有对原始请求的引用,这要归功于mutate逻辑,这实际上是在执行了所有的 “pre “过滤器之后触发的。

5.3. Chaining Requests to Other Services

5.3.将请求串联到其他服务

The next step in our hypothetical scenario is relying on a third service to indicate which Accept-Language header we should use.

在我们的假设情景中,下一步是依靠第三个服务来指示我们应该使用哪种Accept-Language 头。

Thus, we’ll create a new filter which makes a call to this service, and uses its response body as the request header for the proxied service API.

因此,我们将创建一个新的过滤器,对这个服务进行调用,并使用其响应体作为代理服务API的请求头。

In a reactive environment, this means chaining requests to avoid blocking the async execution.

在反应式环境中,这意味着连锁请求以避免阻断异步执行。

In our filter, we’ll start by making the request to the language service:

在我们的过滤器中,我们将首先向语言服务发出请求。

(exchange, chain) -> {
    return WebClient.create().get()
      .uri(config.getLanguageEndpoint())
      .exchange()
      // ...
}

Notice we’re returning this fluent operation, because, as we said, we’ll chain the output of the call with our proxied request.

注意我们要返回这个流畅的操作,因为正如我们所说的,我们将把调用的输出与我们的代理请求连在一起。

The next step will be to extract the language – either from the response body or from the configuration if the response was not successful – and parse it:

下一步将是提取语言–从响应体中提取,或者从配置中提取(如果响应不成功)–并对其进行解析。

// ...
.flatMap(response -> {
    return (response.statusCode()
      .is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage());
}).map(LanguageRange::parse)
// ...

Finally, we’ll set the LanguageRange value as the request header as we did before, and continue the filter chain:

最后,我们将像之前那样把LanguageRange值设置为请求头,并继续过滤链。

.map(range -> {
    exchange.getRequest()
      .mutate()
      .headers(h -> h.setAcceptLanguage(range))
      .build();

    return exchange;
}).flatMap(chain::filter);

That’s it, now the interaction will be carried out in a non-blocking manner.

就这样,现在交互将以非阻塞的方式进行。

6. Conclusion

6.结论

Now that we’ve learned how to write custom Spring Cloud Gateway filters and seen how to manipulate the request and response entities, we’re ready to make the most of this framework.

现在我们已经学会了如何编写自定义Spring Cloud Gateway过滤器,并看到了如何操作请求和响应实体,我们已经准备好充分利用这个框架了。

As always, all the complete examples can be found in over on GitHub. Please remember that in order to test it, we need to run integration and live tests through Maven.

一如既往,所有完整的例子都可以在GitHub上找到。请记住,为了测试,我们需要通过Maven运行集成和实时测试。