|
1 | 1 | [[writing-custom-predicates-and-filters]] |
2 | 2 | = Writing Custom Predicates and Filters |
3 | 3 |
|
4 | | -TODO |
| 4 | +Spring Cloud Gateway Server MVC uses the https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html[Spring WebMvc.fn] API (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/package-summary.html[javadoc]) as the basis for the API Gateway functionality. |
| 5 | + |
| 6 | +Spring Cloud Gateway Server MVC is extensible using these APIs. Users might commonly expect to write custom implementations of https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RequestPredicate.html[`RequestPredicate`] and https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/HandlerFilterFunction.html[`HandlerFilterFunction`] and two variations of `HandlerFilterFunction`, one for "before" filters and another for "after" filters. |
| 7 | + |
| 8 | +== Fundamentals |
| 9 | + |
| 10 | +The most basic interfaces that are a part of the Spring WebMvc.fn API are https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html#webmvc-fn-request[`ServerRequest`] (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/ServerRequest.html[javadoc]) and https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html#webmvc-fn-response[ServerResponse] (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/ServerResponse.html[javadoc]). These provide access to all parts of the HTTP request and response. |
| 11 | + |
| 12 | +NOTE: The Spring WebMvc.fn docs https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html#webmvc-fn-handler-functions[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. |
| 13 | + |
| 14 | +== Implementing a RequestPredicate |
| 15 | + |
| 16 | +The Spring WebMvc.fn https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RouterFunctions.Builder.html[RouterFunctions.Builder] expects a https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html#webmvc-fn-predicates[`RequestPredicate`] (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RequestPredicate.html[javadoc]) to match a given https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html#webmvc-fn-routes[Route]. `RequestPredicate` is a functional interface and can therefor be implemented with lambdas. The method signature to implement is: |
| 17 | + |
| 18 | +[source] |
| 19 | +---- |
| 20 | +boolean test(ServerRequest request) |
| 21 | +---- |
| 22 | + |
| 23 | +=== Example RequestPredicate Implementation |
| 24 | + |
| 25 | +For this example, we will show the implementation of a predicate to test that a particular HTTP headers is part of the HTTP request. |
| 26 | + |
| 27 | +The `RequestPredicate` implementations in Spring WebMvc.fn https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RequestPredicates.html[`RequestPredicates`] and in https://github.com/spring-cloud/spring-cloud-gateway/blob/main/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/predicate/GatewayRequestPredicates.java[GatewayRequestPredicates] are all implemented as `static` methods. We will do the same here. |
| 28 | + |
| 29 | +.SampleRequestPredicates.java |
| 30 | +[source,java] |
| 31 | +---- |
| 32 | +import org.springframework.web.reactive.function.server.RequestPredicate; |
| 33 | +
|
| 34 | +class SampleRequestPredicates { |
| 35 | + public static RequestPredicate headerExists(String header) { |
| 36 | + return request -> request.headers().asHttpHeaders().containsKey(header); |
| 37 | + } |
| 38 | +} |
| 39 | +---- |
| 40 | + |
| 41 | +The implementation is a simple lambda that transforms the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/ServerRequest.Headers.html[ServerRequest.Headers] object to the richer API of https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpHeaders.html[HttpHeaders]. This allows the predicate to test for the presence of the named `header`. |
| 42 | + |
| 43 | +=== How To Use A Custom RequestPredicate |
| 44 | + |
| 45 | +To use our new `headerExists` `RequestPredicate`, we need to plug it in to an appropriate method on the `RouterFunctions.Builder` such as https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RouterFunctions.Builder.html#route(org.springframework.web.servlet.function.RequestPredicate,org.springframework.web.servlet.function.HandlerFunction)[route()]. Of course, the lambda in the `headerExists` method could be written inline in the example below. |
| 46 | + |
| 47 | +.RouteConfiguration.java |
| 48 | +[source,java] |
| 49 | +---- |
| 50 | +import static SampleRequestPredicates.headerExists; |
| 51 | +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; |
| 52 | +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; |
| 53 | +
|
| 54 | +@Configuration |
| 55 | +class RouteConfiguration { |
| 56 | +
|
| 57 | + @Bean |
| 58 | + public RouterFunction<ServerResponse> headerExistsRoute() { |
| 59 | + return route("header_exists_route") |
| 60 | + .route(headerExists("X-Green"), http("https://example.org")) |
| 61 | + .build(); |
| 62 | + } |
| 63 | +} |
| 64 | +---- |
| 65 | + |
| 66 | +The above route will be matched when an HTTP request has a header named `X-Green`. |
| 67 | + |
| 68 | +== Writing Custom HandlerFilterFunction Implementations |
| 69 | + |
| 70 | +The `RouterFunctions.Builder` has three options to add filters: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RouterFunctions.Builder.html#filter(org.springframework.web.servlet.function.HandlerFilterFunction)[filter], https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RouterFunctions.Builder.html#before(java.util.function.Function)[before], and https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/RouterFunctions.Builder.html#after(java.util.function.BiFunction)[after]. The `before` and `after` methods are specializations of the general `filter` method. |
| 71 | + |
| 72 | +=== Implementing a HandlerFilterFunction |
| 73 | + |
| 74 | +The `filter` method takes a https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/HandlerFilterFunction.html[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: |
| 75 | + |
| 76 | +[source] |
| 77 | +---- |
| 78 | +R filter(ServerRequest request, HandlerFunction<T> next) |
| 79 | +---- |
| 80 | + |
| 81 | +This allows access to the `ServerRequest` and after calling `next.handle(request)` access to the `ServerResponse` is available. |
| 82 | + |
| 83 | +==== Example HandlerFilterFunction Implementation |
| 84 | + |
| 85 | +This example will show adding a header to both the request and response. |
| 86 | + |
| 87 | +.SampleHandlerFilterFunctions.java |
| 88 | +[source,java] |
| 89 | +---- |
| 90 | +import org.springframework.web.servlet.function.HandlerFilterFunction; |
| 91 | +import org.springframework.web.servlet.function.ServerRequest; |
| 92 | +import org.springframework.web.servlet.function.ServerResponse; |
| 93 | +
|
| 94 | +class SampleHandlerFilterFunctions { |
| 95 | + public static HandlerFilterFunction<ServerResponse, ServerResponse> instrument(String requestHeader, String responseHeader) { |
| 96 | + return (request, next) -> { |
| 97 | + ServerRequest modified = ServerRequest.from(request).header(requestHeader, generateId()); |
| 98 | + ServerResponse response = next.handle(modified); |
| 99 | + response.headers().add(responseHeader, generateId()); |
| 100 | + return response; |
| 101 | + }; |
| 102 | + } |
| 103 | +} |
| 104 | +---- |
| 105 | + |
| 106 | +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. |
| 107 | + |
| 108 | +==== How To Use Custom HandlerFilterFunction Implementations |
| 109 | + |
| 110 | +.RouteConfiguration.java |
| 111 | +[source,java] |
| 112 | +---- |
| 113 | +import static SampleHandlerFilterFunctions.instrument; |
| 114 | +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; |
| 115 | +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; |
| 116 | +
|
| 117 | +@Configuration |
| 118 | +class RouteConfiguration { |
| 119 | +
|
| 120 | + @Bean |
| 121 | + public RouterFunction<ServerResponse> instrumentRoute() { |
| 122 | + return route("instrument_route") |
| 123 | + .GET("/**", http("https://example.org")) |
| 124 | + .filter(instrument("X-Request-Id", "X-Response-Id")) |
| 125 | + .build(); |
| 126 | + } |
| 127 | +} |
| 128 | +---- |
| 129 | + |
| 130 | +The above route will add a `X-Request-Id` header to the request and a `X-Response-Id` header to the response. |
| 131 | + |
| 132 | +=== Writing Custom Before Filter Implementations |
| 133 | + |
| 134 | +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. |
| 135 | + |
| 136 | +NOTE: Before functions may be adapted to `HandlerFilterFunction` instances via https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/HandlerFilterFunction.html#ofRequestProcessor(java.util.function.Function)[HandlerFilterFunction.ofRequestProcessor()]. |
| 137 | + |
| 138 | +==== Example Before Filter Implementation |
| 139 | + |
| 140 | +In this example we will add a header with a generated value to the request. |
| 141 | + |
| 142 | +.SampleBeforeFilterFunctions.java |
| 143 | +[source,java] |
| 144 | +---- |
| 145 | +import java.util.function.Function; |
| 146 | +import org.springframework.web.servlet.function.ServerRequest; |
| 147 | +
|
| 148 | +class SampleBeforeFilterFunctions { |
| 149 | + public static Function<ServerRequest, ServerRequest> instrument(String header) { |
| 150 | + return request -> ServerRequest.from(request).header(header, generateId());; |
| 151 | + } |
| 152 | +} |
| 153 | +---- |
| 154 | + |
| 155 | +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`. |
| 156 | + |
| 157 | +==== How To Use Custom Before Filter Implementations |
| 158 | + |
| 159 | +.RouteConfiguration.java |
| 160 | +[source,java] |
| 161 | +---- |
| 162 | +import static SampleBeforeFilterFunctions.instrument; |
| 163 | +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; |
| 164 | +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; |
| 165 | +
|
| 166 | +@Configuration |
| 167 | +class RouteConfiguration { |
| 168 | +
|
| 169 | + @Bean |
| 170 | + public RouterFunction<ServerResponse> instrumentRoute() { |
| 171 | + return route("instrument_route") |
| 172 | + .GET("/**", http("https://example.org")) |
| 173 | + .before(instrument("X-Request-Id")) |
| 174 | + .build(); |
| 175 | + } |
| 176 | +} |
| 177 | +---- |
| 178 | + |
| 179 | +The above route will add a `X-Request-Id` header to the request. Note the use of the `before()` method, rather than `filter()`. |
| 180 | + |
| 181 | +=== Writing Custom After Filter Implementations |
| 182 | + |
| 183 | +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. |
| 184 | + |
| 185 | +NOTE: After functions may be adapted to `HandlerFilterFunction` instances via https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/function/HandlerFilterFunction.html#ofResponseProcessor(java.util.function.BiFunction)[HandlerFilterFunction.ofResponseProcessor()]. |
| 186 | + |
| 187 | +==== Example After Filter Implementation |
| 188 | + |
| 189 | +In this example we will add a header with a generated value to the response. |
| 190 | + |
| 191 | +.SampleAfterFilterFunctions.java |
| 192 | +[source,java] |
| 193 | +---- |
| 194 | +import java.util.function.BiFunction; |
| 195 | +import org.springframework.web.servlet.function.ServerRequest; |
| 196 | +import org.springframework.web.servlet.function.ServerResponse; |
| 197 | +
|
| 198 | +class SampleAfterFilterFunctions { |
| 199 | + public static BiFunction<ServerRequest, ServerResponse, ServerResponse> instrument(String header) { |
| 200 | + return (request, response) -> { |
| 201 | + response.headers().add(header, generateId()); |
| 202 | + return response; |
| 203 | + }; |
| 204 | + } |
| 205 | +} |
| 206 | +---- |
| 207 | + |
| 208 | +In this case we simply add the header to the response and return it. |
| 209 | + |
| 210 | +==== How To Use Custom After Filter Implementations |
| 211 | + |
| 212 | +.RouteConfiguration.java |
| 213 | +[source,java] |
| 214 | +---- |
| 215 | +import static SampleAfterFilterFunctions.instrument; |
| 216 | +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; |
| 217 | +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; |
| 218 | +
|
| 219 | +@Configuration |
| 220 | +class RouteConfiguration { |
| 221 | +
|
| 222 | + @Bean |
| 223 | + public RouterFunction<ServerResponse> instrumentRoute() { |
| 224 | + return route("instrument_route") |
| 225 | + .GET("/**", http("https://example.org")) |
| 226 | + .after(instrument("X-Response-Id")) |
| 227 | + .build(); |
| 228 | + } |
| 229 | +} |
| 230 | +---- |
| 231 | + |
| 232 | +The above route will add a `X-Response-Id` header to the response. Note the use of the `after()` method, rather than `filter()`. |
| 233 | + |
| 234 | +// TODO: advanced topics such as attributes, beans and more |
0 commit comments