Spring Cloud Gateway Routing Predicate Factories – Spring云网关路由谓语工厂

最后修改: 2020年 2月 1日

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

1. Introduction

1.绪论

In a previous article, we’ve covered what is the Spring Cloud Gateway and how to use the built-in predicates to implement basic routing rules. Sometimes, however, those built-in predicates might not be enough. For instance, our routing logic might require a database lookup for some reason.

在之前的文章中,我们已经介绍了什么是Spring Cloud Gateway以及如何使用内置谓词来实现基本路由规则。但是,有时候,这些内置谓词可能还不够。例如,我们的路由逻辑可能由于某种原因需要进行数据库查询。

For those cases, Spring Cloud Gateway allows us to define custom predicates. Once defined, we can use them as any other predicate, meaning we can define routes using the fluent API and/or the DSL.

对于这些情况,Spring Cloud Gateway允许我们定义自定义谓词。一旦定义,我们可以像其他谓词一样使用它们,这意味着我们可以使用流畅的API和/或DSL定义路由。

2. Anatomy of a Predicate

2.谓词的剖析

In a nutshell, a Predicate in Spring Cloud Gateway is an object that tests if the given request fulfills a given condition. For each route, we can define one or more predicates that, if satisfied, will accept requests for the configured backend after applying any filters.

简而言之,Spring Cloud Gateway中的Predicate是一个测试给定请求是否满足特定条件的对象。对于每个路由,我们可以定义一个或多个谓词,如果满足,在应用任何过滤器后,将接受配置的后端请求。

Before writing our predicate, let’s take a look at the source code of an existing predicate or, more precisely, the code for an existing PredicateFactory. As the name already hints, Spring Cloud Gateway uses the popular Factory Method Pattern as a mechanism to support the creation of Predicate instances in an extensible way.

在编写我们的谓词之前,让我们先看看现有谓词的源代码,或者更准确地说,现有PredicateFactory的代码。正如其名称已经暗示的那样,Spring Cloud Gateway使用流行的Factory Method Pattern作为一种机制,支持以可扩展的方式创建Predicateinstances。

We can pick any one of the built-in predicate factories, which are available in the org.springframework.cloud.gateway.handler.predicate package of the spring-cloud-gateway-core module. We can easily spot the existing ones since their names all end in RoutePredicateFactory. HeaderRouterPredicateFactory is a good example:

我们可以从内置的谓词工厂中选择任何一个,这些工厂在 org.springframework.cloud.gateway.handler.predicate 模块的 spring-cloud-gateway-core 包中可用。我们可以很容易地发现现有的,因为它们的名字都以RoutePredicateFactory结尾。HeaderRouterPredicateFactory就是一个好例子。

public class HeaderRoutePredicateFactory extends 
  AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {

    // ... setup code omitted
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                // ... predicate logic omitted
            }
        };
    }

    @Validated
    public static class Config {
        public Config(boolean isGolden, String customerIdCookie ) {
          // ... constructor details omitted
        }
        // ...getters/setters omitted
    }
}

There are a few key points we can observe in the implementation:

在实施过程中,我们可以观察到几个关键点。

  • It extends the AbstractRoutePredicateFactory<T>, which, in turn, implements the RoutePredicateFactory interface used by the gateway
  • The apply method returns an instance of the actual Predicate – a GatewayPredicate in this case
  • The predicate defines an inner Config class, which is used to store static configuration parameters used by the test logic

If we take a look at other available PredicateFactory, we’ll see that the basic pattern is basically the same:

如果我们看一下其他可用的PredicateFactory,我们会发现基本模式基本上是一样的。

  1. Define a Config class to hold configuration parameters
  2. Extend the AbstractRoutePredicateFactory, using the configuration class as its template parameter
  3. Override the apply method, returning a Predicate that implements the desired test logic

3. Implementing a Custom Predicate Factory

3.实现一个自定义谓词工厂

For our implementation, let’s suppose the following scenario: for a given API, call we have to choose between two possible backends. “Golden” customers, who are our most valued ones, should be routed to a powerful server, with access to more memory, more CPU, and fast disks. Non-golden customers go to a less powerful server, which results in slower response times.

对于我们的实施,让我们假设以下情况:对于一个给定的API,调用我们必须在两个可能的后端之间进行选择。”黄金 “客户,也就是我们最有价值的客户,应该被路由到一个强大的服务器,可以访问更多的内存、更多的CPU和快速的磁盘。非黄金客户会被送到一个性能较差的服务器上,这将导致较慢的响应时间。

To determine whether the request comes from a golden customer, we’ll need to call a service that takes the customerId associated with the request and returns its status. As for the customerId, in our simple scenario, we’ll assume it is available in a cookie.

为了确定该请求是否来自黄金客户,我们需要调用一个服务,该服务接收与该请求相关的customerId并返回其状态。至于customerId,在我们的简单方案中,我们将假设它在cookie中可用。

With all this information, we can now write our custom predicate. We’ll keep the existing naming convention and name our class GoldenCustomerRoutePredicateFactory:

有了这些信息,我们现在可以编写我们的自定义谓词。我们将保持现有的命名惯例,并将我们的类命名为GoldenCustomerRoutePredicateFactory

public class GoldenCustomerRoutePredicateFactory extends 
  AbstractRoutePredicateFactory<GoldenCustomerRoutePredicateFactory.Config> {

    private final GoldenCustomerService goldenCustomerService;
    
    // ... constructor omitted

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {        
        return (ServerWebExchange t) -> {
            List<HttpCookie> cookies = t.getRequest()
              .getCookies()
              .get(config.getCustomerIdCookie());
              
            boolean isGolden; 
            if ( cookies == null || cookies.isEmpty()) {
                isGolden = false;
            } else {                
                String customerId = cookies.get(0).getValue();                
                isGolden = goldenCustomerService.isGoldenCustomer(customerId);
            }              
            return config.isGolden() ? isGolden : !isGolden;           
        };        
    }
    
    @Validated
    public static class Config {        
        boolean isGolden = true;        
        @NotEmpty
        String customerIdCookie = "customerId";
        // ...constructors and mutators omitted   
    }    
}

As we can see, the implementation is quite simple. Our apply method returns a lambda that implements the required logic using the ServerWebExchange passed to it. First, it checks for the presence of the customerId cookie. If it cannot find it, then this is a normal customer. Otherwise, we use the cookie value to call the isGoldenCustomer service method.

正如我们所看到的,实现是非常简单的。我们的apply方法返回一个lambda,使用传递给它的ServerWebExchange实现所需的逻辑。首先,它检查是否存在customerId cookie。如果它找不到,那么这就是一个正常的客户。否则,我们使用cookie的值来调用isGoldenCustomer服务方法。

Next, we combine the client’s type with the configured isGolden parameter to determine the return value. This allows us to use the same predicate to create both routes described before, by just changing the value of the isGolden parameter.

接下来,我们将客户端的类型与配置的isGolden参数相结合,以确定返回值。这使得我们可以使用相同的谓词来创建之前描述的两个路由,只需改变isGolden参数的值即可

4. Registering the Custom Predicate Factory

4.注册自定义谓词工厂

Once we’ve coded our custom predicate factory, we need a way to make Spring Cloud Gateway aware of if. Since we’re using Spring, this is done in the usual way: we declare a bean of type GoldenCustomerRoutePredicateFactory.

一旦我们编码了我们的自定义谓词工厂,我们需要一种方法来使Spring Cloud Gateway意识到这一点。由于我们使用的是Spring,这可以通过通常的方式完成:我们声明一个GoldenCustomerRoutePredicateFactory.类型的bean。

Since our type implements RoutePredicateFactory through is base class, it will be picked by Spring at context initialization time and made available to Spring Cloud Gateway.

由于我们的类型通过基类实现了RoutePredicateFactory ,它将在上下文初始化时被Spring选中并提供给Spring Cloud Gateway。

Here, we’ll create our bean using a @Configuration class:

在这里,我们将使用@Configuration类创建我们的bean。

@Configuration
public class CustomPredicatesConfig {
    @Bean
    public GoldenCustomerRoutePredicateFactory goldenCustomer(
      GoldenCustomerService goldenCustomerService) {
        return new GoldenCustomerRoutePredicateFactory(goldenCustomerService);
    }
}

We assume here we have a suitable GoldenCustomerService implementation available in the Spring’s context. In our case, we have just a dummy implementation that compares the customerId value with a fixed one — not realistic, but useful for demonstration purposes.

我们假设在Spring的上下文中有一个合适的GoldenCustomerService实现。在我们的案例中,我们只有一个假的实现,将customerId值与一个固定的值进行比较–这并不现实,但对于演示来说很有用。

5. Using the Custom Predicate

5.使用自定义谓词

Now that we have our “Golden Customer” predicate implemented and available to Spring Cloud Gateway, we can start using it to define routes. First, we’ll use the fluent API to define a route, then we’ll do it in a declarative way using YAML.

现在我们已经实现了 “黄金客户 “谓词,并可用于Spring Cloud Gateway,我们可以开始使用它来定义路由。首先,我们将使用流畅的API来定义一个路由,然后我们将使用YAML以声明的方式进行定义。

5.1. Defining a Route with the Fluent API

5.1.用Fluent API定义一个路由

Fluent APIs are a popular design choice when we have to programmatically create complex objects. In our case, we define routes in a @Bean that creates a RouteLocator object using a RouteLocatorBuilder and our custom predicate factory:

Fluent API是我们必须以编程方式创建复杂对象时的一种流行设计选择。在我们的案例中,我们在一个 @Bean中定义路由,该对象使用RouteLocatorBuilder和我们的自定义谓词工厂创建RouteLocator对象。

@Bean
public RouteLocator routes(RouteLocatorBuilder builder, GoldenCustomerRoutePredicateFactory gf ) {
    return builder.routes()
      .route("golden_route", r -> r.path("/api/**")
        .uri("https://fastserver")
        .predicate(gf.apply(new Config(true, "customerId"))))
      .route("common_route", r -> r.path("/api/**")
        .uri("https://slowserver")
        .predicate(gf.apply(new Config(false, "customerId"))))                
      .build();
}

Notice how we’ve used two distinct Config configurations in each route. In the first case, the first argument is true, so the predicate also evaluates to true when we have a request from a golden customer. As for the second route, we pass false in the constructor so our predicate will return true for non-golden customers.

请注意我们在每个路由中使用了两个不同的Config配置。在第一种情况下,第一个参数是true,所以当我们有一个来自黄金客户的请求时,谓词也评估为true。至于第二条路线,我们在构造函数中传递false,所以我们的谓词对于非黄金客户将返回true

5.2. Defining a Route in YAML

5.2.在YAML中定义一个路由

We can achieve the same result as before in a declarative way using properties or YAML files. Here, we’ll use YAML, as it’s a bit easier to read:

我们可以使用属性或YAML文件以声明的方式实现与之前相同的结果。在这里,我们将使用YAML,因为它比较容易阅读。

spring:
  cloud:
    gateway:
      routes:
      - id: golden_route
        uri: https://fastserver
        predicates:
        - Path=/api/**
        - GoldenCustomer=true
      - id: common_route
        uri: https://slowserver
        predicates:
        - Path=/api/**
        - name: GoldenCustomer
          args:
            golden: false
            customerIdCookie: customerId

Here we’ve defined the same routes as before, using the two available options to define predicates. The first one, golden_route, uses a compact representation that takes the form Predicate=[param[,param]+]Predicate here is the predicate’s name, which is derived automatically from the factory class name by removing the RoutePredicateFactory suffix. Following the “=” sign, we have parameters used to populate the associated Config instance.

这里我们定义了与之前相同的路线,使用两个可用的选项来定义谓词。第一个选项,golden_route,使用紧凑的表示法,其形式为Predicate=[param[,param]+]。这里的Predicate是谓词的名称,通过去除RoutePredicateFactory后缀,它自动从工厂类的名称中派生出来。在”=”号之后,我们有用于填充相关Config实例的参数。

This compact syntax is fine when our predicate requires just simple values, but this might not always be the case. For those scenarios, we can use the long format, depicted in the second route. In this case, we supply an object with two properties: name and args. name contains the predicate name, and args is used to populate the Config instance. Since this time args is an object, our configuration can be as complex as required.

当我们的谓词只需要简单的值时,这种紧凑的语法很好,但这可能并不总是这样。对于这些情况,我们可以使用长格式,在第二条路线中描述。在这种情况下,我们提供一个有两个属性的对象。nameargsname包含了谓词的名称,args用于填充Configinstance。由于这次的args是一个对象,我们的配置可以根据需要而复杂。

6. Testing

6.测试

Now, let’s check if everything is working as expected using curl to test our gateway. For those tests, we’ve set up our routes just like previously shown, but we’ll use the publicly available httpbin.org service as our dummy backend. This is a quite useful service that we can use to quickly check if our rules are working as expected, available both online and as a docker image that we can use locally.

现在,让我们使用curl来测试我们的网关,看看一切是否按预期工作。对于这些测试,我们已经设置了我们的路由,就像之前显示的那样,但是我们将使用公开可用的httpbin.org服务作为我们的假后端。这是一个相当有用的服务,我们可以用它来快速检查我们的规则是否按预期工作,它既可以在线使用,也可以作为一个docker镜像,我们可以在本地使用。

Our test configuration also includes the standard AddRequestHeader filter. We use it  to add a custom Goldencustomer header to the request with a value that corresponds to the predicate result. We also add a StripPrefix filter, since we want to remove the /api from the request URI before calling the backend.

我们的测试配置还包括标准的AddRequestHeader过滤器。我们用它来向请求添加一个自定义的Goldencustomer头,其值与谓词结果相对应。我们还添加了一个StripPrefix过滤器,因为我们想在调用后端之前从请求URI中移除/api

First, let’s test the “common client” scenario. With our gateway up and running, we use curl to invoke httpbin‘s headers API, which will simply echo all received headers:

首先,让我们测试一下 “普通客户端 “的情况。随着我们的网关启动和运行,我们用curl调用httpbinheaders API,它将简单地回显所有收到的头文件。

$ curl http://localhost:8080/api/headers
{
  "headers": {
    "Accept": "*/*",
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"127.0.0.1:51547\"",
    "Goldencustomer": "false",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.55.1",
    "X-Forwarded-Host": "localhost:8080",
    "X-Forwarded-Prefix": "/api"
  }
}

As expected, we see that the Goldencustomer header was sent with a false value. Let’s try now with a “Golden” customer:

正如预期的那样,我们看到Goldencustomer头的发送值为false。现在让我们用一个 “黄金 “客户试试。

$ curl -b customerId=baeldung http://localhost:8080/api/headers
{
  "headers": {
    "Accept": "*/*",
    "Cookie": "customerId=baeldung",
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"127.0.0.1:51651\"",
    "Goldencustomer": "true",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.55.1",
    "X-Forwarded-Host": "localhost:8080",
    "X-Forwarded-Prefix": "/api"
  }
}

This time, Goldencustomer is true, as we’ve sent a customerId cookie with a value that our dummy service recognizes as valid for a golden customer.

这一次,Goldencustomertrue,因为我们已经发送了一个customerId cookie,其值被我们的假服务识别为对黄金客户有效。

7. Conclusion

7.结语

In this article, we’ve covered how to add custom predicate factories to Spring Cloud Gateway and use them to define routes using arbitrary logic.

在这篇文章中,我们已经介绍了如何向Spring Cloud Gateway添加自定义谓词工厂,并使用它们来定义使用任意逻辑的路由。

As usual, all code is available over on GitHub.

像往常一样,所有的代码都可以在GitHub上找到