Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

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.

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.

2. Anatomy of a Predicate

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.

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.

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:

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:

  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

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.

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.

With all this information, we can now write our custom predicate. We'll keep the existing naming convention and name our class 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.

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.

4. Registering the Custom Predicate Factory

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.

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.

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

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

5. Using the Custom Predicate

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.

5.1. Defining a Route with the 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:

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

5.2. Defining a Route in 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:

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.

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.

6. Testing

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.

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.

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

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

7. Conclusion

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.

As usual, all code is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!