Skip to content

Commit fb11d3a

Browse files
committed
Initial docs on writing predicates and filters.
Fixes gh-3143
1 parent 563a0ab commit fb11d3a

File tree

1 file changed

+231
-1
lines changed

1 file changed

+231
-1
lines changed
Lines changed: 231 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,234 @@
11
[[writing-custom-predicates-and-filters]]
22
= Writing Custom Predicates and Filters
33

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

Comments
 (0)