From fedbb09ad900ab47dec2d15da90b3ba3ed4a67e6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 23 Nov 2018 17:47:48 +0100 Subject: [PATCH] Improve RouterFunctions reference documentation Changed WebFlux.fn docs to use router function builder. Issue: SPR-17016 --- src/docs/asciidoc/web/webflux-functional.adoc | 239 +++++++++++++----- 1 file changed, 176 insertions(+), 63 deletions(-) diff --git a/src/docs/asciidoc/web/webflux-functional.adoc b/src/docs/asciidoc/web/webflux-functional.adoc index f42e883be6..9fda56fd59 100644 --- a/src/docs/asciidoc/web/webflux-functional.adoc +++ b/src/docs/asciidoc/web/webflux-functional.adoc @@ -1,7 +1,7 @@ [[webflux-fn]] = Functional Endpoints -Spring WebFlux includes a lightweight functional programming model in which functions +Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotation-based programming model but otherwise runs on the same <> foundation. @@ -12,18 +12,20 @@ the same <> foundation. [[webflux-fn-overview]] == Overview -An HTTP request is handled with a `HandlerFunction` that takes `ServerRequest` and -returns `Mono`, both of which are immutable contracts that offer -JDK 8-friendly access to the HTTP request and response. `HandlerFunction` is the equivalent of -a `@RequestMapping` method in the annotation-based programming model. +In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes +`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono`). +Both the request as the response object have immutable contracts that offer JDK 8-friendly +access to the HTTP request and response. +`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the +annotation-based programming model. -Requests are routed to a `HandlerFunction` with a `RouterFunction` that takes -`ServerRequest` and returns `Mono`. When a request is matched to a -particular route, the `HandlerFunction` mapped to the route is used. `RouterFunction` is -the equivalent of a `@RequestMapping` annotation. +Incoming requests are routed to a handler function with a `RouterFunction`: a function that +takes `ServerRequest` and returns a delayed `HandlerFunction` (i.e. `Mono`). +When the router function matches, a handler function is returned; otherwise an empty Mono. +`RouterFunction` is the equivalent of a `@RequestMapping` annotation, but with the major +difference that router functions provide not just data, but also behavior. -`RouterFunctions.route(RequestPredicate, HandlerFunction)` provides a router function -default implementation that can be used with a number of built-in request predicates, +`RouterFunctions.route()` provides a router builder that facilitates the creation of routers, as the following example shows: ==== @@ -37,10 +39,11 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); -RouterFunction route = - route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson) - .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople) - .andRoute(POST("/person"), handler::createPerson); +RouterFunction route = route() + .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET("/person", accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson) + .build(); public class PersonHandler { @@ -77,12 +80,12 @@ Most applications can run through the WebFlux Java configuration, see <>. +access to the HTTP request and response. +Both request and response provide http://www.reactive-streams.org[Reactive Streams] back pressure +against the body streams. +The request body is represented with a Reactor `Flux` or `Mono`. +The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`. +For more on that, see <>. @@ -195,9 +198,11 @@ HandlerFunction helloWorld = ---- ==== -That is convenient, but, in an application, we need multiple functions, and it is useful to group -related handler functions together into a handler (like a `@Controller`). For example, -the following class exposes a reactive `Person` repository: +That is convenient, but in an application we need multiple functions, and multiple inline +lambda's can get messy. +Therefore, it is useful to group related handler functions together into a handler class, which +has a similar role as `@Controller` in an annotation-based application. +For example, the following class exposes a reactive `Person` repository: ==== [source,java,indent=0] @@ -251,12 +256,22 @@ found. If it is not found, we use `switchIfEmpty(Mono)` to return a 404 Not F [[webflux-fn-router-functions]] == `RouterFunction` -`RouterFunction` is used to route requests to a `HandlerFunction`. Typically, you do not -write router functions yourself, but rather use -`RouterFunctions.route(RequestPredicate, HandlerFunction)`. If the predicate applies, the -request is routed to the given `HandlerFunction`. Otherwise, no routing is performed, -and that would translate to a 404 (Not Found) response. +Router functions are used to route the requests to the corresponding `HandlerFunction`. +Typically, you do not write router functions yourself, but rather use a method on the +`RouterFunctions` utility class to create one. +`RouterFunctions.route()` (no parameters) provides you with a fluent builder for creating a router +function, whereas `RouterFunctions.route(RequestPredicate, HandlerFunction)` offers a direct way +to create a router. +Generally, it is recommended to use the `route()` builder, as it provides +convenient short-cuts for typical mapping scenarios without requiring hard-to-discover +static imports. +For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs. + +Besides HTTP method-based mapping, the route builder offers a way to introduce additional +predicates when mapping to requests. +For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a +parameter, though which additional constraints can be expressed. [[webflux-fn-predicates]] @@ -264,15 +279,17 @@ and that would translate to a 404 (Not Found) response. You can write your own `RequestPredicate`, but the `RequestPredicates` utility class offers commonly used implementations, based on the request path, HTTP method, content-type, -and so on. The following example creates a request predicate based on a path: +and so on. +The following example uses a request predicate to create a constraint based on the `Accept` +header: ==== [source,java,indent=0] [subs="verbatim,quotes"] ---- -RouterFunction route = - RouterFunctions.route(RequestPredicates.path("/hello-world"), - request -> Response.ok().body(fromObject("Hello World"))); +RouterFunction route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> Response.ok().body(fromObject("Hello World"))); ---- ==== @@ -281,28 +298,34 @@ You can compose multiple request predicates together by using: * `RequestPredicate.and(RequestPredicate)` -- both must match. * `RequestPredicate.or(RequestPredicate)` -- either can match. -Many of the predicates from `RequestPredicates` are composed. For example, -`RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)` +Many of the predicates from `RequestPredicates` are composed. +For example, `RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)` and `RequestPredicates.path(String)`. - -You can compose multiple router functions into one, such that they are evaluated in order, -and, if the first route does not match, the second is evaluated. You can declare more -specific routes before more general ones. +The example shown above also uses two request predicates, as the builder uses +`RequestPredicates.GET` internally, and composes that with the `accept` predicate. [[webflux-fn-routes]] === Routes -You can compose multiple router functions together by using: +Router functions are evaluated in order: if the first route does not match, the +second is evaluated, and so on. +Therefore, it makes sense to declare more specific routes before general ones. +Note that this behavior is different from the annotation-based programming model, where the +"most specific" controller method is picked automatically. + +When using the router function builder, all defined routes are composed into one +`RouterFunction` that is returned from `build()`. +There are also other ways to compose multiple router functions together: +* `add(RouterFunction)` on the `RouterFunctions.route()` builder * `RouterFunction.and(RouterFunction)` * `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` -- shortcut for `RouterFunction.and()` with nested `RouterFunctions.route()`. -Using composed routes and predicates, we can then declare the following routes, referring -to methods in the `PersonHandler` (shown in <>) through -https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html[method-references]: +The following example shows the composition of four routes: + ==== [source,java,indent=0] @@ -314,14 +337,70 @@ import static org.springframework.web.reactive.function.server.RequestPredicates PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); -RouterFunction personRoute = - route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson) - .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople) - .andRoute(POST("/person"), handler::createPerson); +RouterFunction otherRoute = ... + +RouterFunction route = route() + .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> + .GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> + .POST("/person", handler::createPerson) // <3> + .add(otherRoute) // <4> + .build(); ---- +<1> `GET /person/{id}` with an `Accept` header that matches JSON is routed to +`PersonHandler.getPerson` +<2> `GET /person` with an `Accept` header that matches JSON is routed to +`PersonHandler.listPeople` +<3> `POST /person` with no additional predicates is mapped to +`PersonHandler.createPerson`, and +<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. + ==== +=== Nested Routes + +It is common for a group of router functions to have a shared predicate, for instance a shared +path. +In the example above, the shared predicate would be a path predicate that matches `/person`, +used by three of the routes. +When using annotations, you would remove this duplication by using a type-level `@RequestMapping` + annotation that maps to `/person`. +In WebFlux.fn, path predicates can be shared through the `path` method on the router function builder. +For instance, the last few lines of the example above can be improved in the following way by using nested routes: + +==== +[source,java,indent=0] +[subs="verbatim,quotes"] +---- +RouterFunction route = route() + .path("/person", builder -> builder + .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET("", accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson)) + .build(); +---- +==== + +Note that second parameter of `path` is a consumer that takes the a router builder. + +Though path-based nesting is the most common, you can nest on any kind of predicate by using +the `nest` method on the builder. +The above still contains some duplication in the form of the shared `Accept`-header predicate. +We can further improve by using the `nest` method together with `accept`: + +==== +[source,java,indent=0] +[subs="verbatim,quotes"] +---- +RouterFunction route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET("", handler::listPeople)) + .POST("/person", handler::createPerson)) + .build(); +---- +==== [[webflux-fn-running]] @@ -336,7 +415,7 @@ function to an `HttpHandler` by using one of the following: You can then use the returned `HttpHandler` with a number of server adapters by following <> for server-specific instructions. -A more advanced option is to run with a +A more typical option, also used by Spring Boot, is to run with a <>-based setup through the <>, which uses Spring configuration to declare the components required to process requests. The WebFlux Java configuration declares the following @@ -400,40 +479,74 @@ public class WebConfig implements WebFluxConfigurer { [[webflux-fn-handler-filter-function]] -== `HandlerFilterFunction` +== Filtering Handler Functions -You can filter routes mapped by a router function by calling -`RouterFunction.filter(HandlerFilterFunction)`, where `HandlerFilterFunction` is essentially a -function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`. -The handler function parameter represents the next element in the chain. This is typically the -`HandlerFunction` that is routed to, but it can also be another `FilterFunction` if multiple filters -are applied. +You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing +function builder. With annotations, you can achieve similar functionality by using `@ControllerAdvice`, a `ServletFilter`, or both. +The filter will apply to all routes that are built by the builder. +This means that filters defined in nested routes do not apply to "top-level" routes. +For instance, consider the following example: + +==== +[source,java,indent=0] +[subs="verbatim,quotes"] +---- +RouterFunction route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET("", handler::listPeople) + .before(request -> ServerRequest.from(request) // <1> + .header("X-RequestHeader", "Value") + .build())) + .POST("/person", handler::createPerson)) + .after((request, response) -> logResponse(response)) // <2> + .build(); +---- +<1> The `before` filter that adds a custom request header is only applied to the two GET routes. +<2> The `after` filter that logs the response is applied to all routes, including the nested ones. +==== + +The `filter` method on the router builder takes a `HandlerFilterFunction`: a +function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`. +The handler function parameter represents the next element in the chain. +This is typically the handler that is routed to, but it can also be another +filter if multiple are applied. + Now we can add a simple security filter to our route, assuming that we have a `SecurityManager` that -can determine whether a particular path is allowed. The following example shows how to do so: +can determine whether a particular path is allowed. +The following example shows how to do so: ==== [source,java,indent=0] [subs="verbatim,quotes"] ---- -import static org.springframework.http.HttpStatus.UNAUTHORIZED; SecurityManager securityManager = ... -RouterFunction route = ... -RouterFunction filteredRoute = - route.filter((request, next) -> { +RouterFunction route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET("", handler::listPeople)) + .POST("/person", handler::createPerson)) + .filter((request, next) -> { if (securityManager.allowAccessTo(request.path())) { return next.handle(request); } else { return ServerResponse.status(UNAUTHORIZED).build(); } - }); + }) + .build(); ---- ==== -The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional. We -allow only the handler function to be executed when access is allowed. +The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional. +We allow only the handler function to be executed when access is allowed. + +Besides using the `filter` method on the router function builder, it is possible to apply a +filter to an existing router function via `RouterFunction.filter(HandlerFilterFunction)`. NOTE: CORS support for functional endpoints is provided through a dedicated <>.