Writing Custom Predicates and Filters
Spring Cloud Gateway Server MVC uses the Spring WebMvc.fn API (javadoc) as the basis for the API Gateway functionality.
Spring Cloud Gateway Server MVC is extensible using these APIs. Users might commonly expect to write custom implementations of RequestPredicate
and HandlerFilterFunction
and two variations of HandlerFilterFunction
, one for "before" filters and another for "after" filters.
Fundamentals
The most basic interfaces that are a part of the Spring WebMvc.fn API are ServerRequest
(javadoc) and ServerResponse (javadoc). These provide access to all parts of the HTTP request and response.
The Spring WebMvc.fn docs declare that "`ServerRequest` and ServerResponse are immutable interfaces. In some cases, Spring Cloud Gateway Server MVC has to provide alternate implementations so that some things can be mutable to satisfy the proxy requirements of an API gateway.
|
Implementing a RequestPredicate
The Spring WebMvc.fn RouterFunctions.Builder expects a RequestPredicate
(javadoc) to match a given Route. RequestPredicate
is a functional interface and can therefor be implemented with lambdas. The method signature to implement is:
boolean test(ServerRequest request)
Example RequestPredicate Implementation
For this example, we will show the implementation of a predicate to test that a particular HTTP headers is part of the HTTP request.
The RequestPredicate
implementations in Spring WebMvc.fn RequestPredicates
and in GatewayRequestPredicates are all implemented as static
methods. We will do the same here.
import org.springframework.web.reactive.function.server.RequestPredicate;
class SampleRequestPredicates {
public static RequestPredicate headerExists(String header) {
return request -> request.headers().asHttpHeaders().containsKey(header);
}
}
The implementation is a simple lambda that transforms the ServerRequest.Headers object to the richer API of HttpHeaders. This allows the predicate to test for the presence of the named header
.
How To Use A Custom RequestPredicate
To use our new headerExists
RequestPredicate
, we need to plug it in to an appropriate method on the RouterFunctions.Builder
such as route(). Of course, the lambda in the headerExists
method could be written inline in the example below.
import static SampleRequestPredicates.headerExists;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> headerExistsRoute() {
return route("header_exists_route")
.route(headerExists("X-Green"), http("https://example.org"))
.build();
}
}
The above route will be matched when an HTTP request has a header named X-Green
.
Writing Custom HandlerFilterFunction Implementations
The RouterFunctions.Builder
has three options to add filters: filter, before, and after. The before
and after
methods are specializations of the general filter
method.
Implementing a HandlerFilterFunction
The filter
method takes a HandlerFilterFunction as a parameter. HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse>
is a functional interface and can therefor be implemented with lambdas. The method signature to implement is:
R filter(ServerRequest request, HandlerFunction<T> next)
This allows access to the ServerRequest
and after calling next.handle(request)
access to the ServerResponse
is available.
Example HandlerFilterFunction Implementation
This example will show adding a header to both the request and response.
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
class SampleHandlerFilterFunctions {
public static HandlerFilterFunction<ServerResponse, ServerResponse> instrument(String requestHeader, String responseHeader) {
return (request, next) -> {
ServerRequest modified = ServerRequest.from(request).header(requestHeader, generateId()).build();
ServerResponse response = next.handle(modified);
response.headers().add(responseHeader, generateId());
return response;
};
}
}
First, a new ServerRequest
is created from the existing request. This allows us to add the header using the header()
method. Then we call next.handle()
passing in the modified ServerRequest
. Then using the returned ServerResponse
we add the header to the response.
How To Use Custom HandlerFilterFunction Implementations
import static SampleHandlerFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> instrumentRoute() {
return route("instrument_route")
.GET("/**", http("https://example.org"))
.filter(instrument("X-Request-Id", "X-Response-Id"))
.build();
}
}
The above route will add a X-Request-Id
header to the request and a X-Response-Id
header to the response.
Writing Custom Before Filter Implementations
The before
method takes a Function<ServerRequest, ServerRequest>
as a parameter. This allows for creating a new ServerRequest
with updated data to be returned from the function.
Before functions may be adapted to HandlerFilterFunction instances via HandlerFilterFunction.ofRequestProcessor().
|
Example Before Filter Implementation
In this example we will add a header with a generated value to the request.
import java.util.function.Function;
import org.springframework.web.servlet.function.ServerRequest;
class SampleBeforeFilterFunctions {
public static Function<ServerRequest, ServerRequest> instrument(String header) {
return request -> ServerRequest.from(request).header(header, generateId()).build();
}
}
A new ServerRequest
is created from the existing request. This allows us to add the header using the header()
method. This implementation is simpler than the HandlerFilterFunction
because we only deal with the ServerRequest
.
How To Use Custom Before Filter Implementations
import static SampleBeforeFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> instrumentRoute() {
return route("instrument_route")
.GET("/**", http("https://example.org"))
.before(instrument("X-Request-Id"))
.build();
}
}
The above route will add a X-Request-Id
header to the request. Note the use of the before()
method, rather than filter()
.
Writing Custom After Filter Implementations
The after
method takes a BiFunction<ServerRequest,ServerResponse,ServerResponse>
. This allows access to both the ServerRequest
and the ServerResponse
and the ability to return a new ServerResponse
with updated information.
After functions may be adapted to HandlerFilterFunction instances via HandlerFilterFunction.ofResponseProcessor().
|
Example After Filter Implementation
In this example we will add a header with a generated value to the response.
import java.util.function.BiFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
class SampleAfterFilterFunctions {
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> instrument(String header) {
return (request, response) -> {
response.headers().add(header, generateId());
return response;
};
}
}
In this case we simply add the header to the response and return it.
How To Use Custom After Filter Implementations
import static SampleAfterFilterFunctions.instrument;
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
@Configuration
class RouteConfiguration {
@Bean
public RouterFunction<ServerResponse> instrumentRoute() {
return route("instrument_route")
.GET("/**", http("https://example.org"))
.after(instrument("X-Response-Id"))
.build();
}
}
The above route will add a X-Response-Id
header to the response. Note the use of the after()
method, rather than filter()
.
How To Register Custom Predicates and Filters for Configuration
To use custom Predicates and Filters in external configuration you need to create a special Supplier class and register it in META-INF/spring.factories
.
Registering Custom Predicates
To register custom predicates you need to implement PredicateSupplier
. The PredicateDiscoverer
looks for static methods that return RequestPredicates
to register.
SampleFilterSupplier.java
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier;
@Configuration
class SamplePredicateSupplier implements PredicateSupplier {
@Override
public Collection<Method> get() {
return Arrays.asList(SampleRequestPredicates.class.getMethods());
}
}
You then need to add the class in META-INF/spring.factories
.
org.springframework.cloud.gateway.server.mvc.predicate.PredicateSupplier=\
com.example.SamplePredicateSupplier
Registering Custom Filters
The SimpleFilterSupplier
allows for easily registering custom filters. The FilterDiscoverer
looks for static methods that return HandlerFilterFunction
to register. If you need more flexibility than SimpleFilterSupplier
you can implement FilterSupplier
directly.
import org.springframework.cloud.gateway.server.mvc.filter.SimpleFilterSupplier;
@Configuration
class SampleFilterSupplier extends SimpleFilterSupplier {
public SampleFilterSupplier() {
super(SampleAfterFilterFunctions.class);
}
}
You then need to add the class in META-INF/spring.factories
.
org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier=\
com.example.SampleFilterSupplier