From 652bbdad2aa9dde7fb1444ab358dd467a1035dd2 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 18 Sep 2019 15:57:00 +0200 Subject: [PATCH] Add reference documentation for WebMvc.fn This commit introduces reference documentation for WebMvc.fn, the functional web framework. Closes gh-23657 --- src/docs/asciidoc/web/webflux-functional.adoc | 6 + src/docs/asciidoc/web/webmvc-functional.adoc | 776 ++++++++++++++++++ src/docs/asciidoc/web/webmvc.adoc | 1 + 3 files changed, 783 insertions(+) create mode 100644 src/docs/asciidoc/web/webmvc-functional.adoc diff --git a/src/docs/asciidoc/web/webflux-functional.adoc b/src/docs/asciidoc/web/webflux-functional.adoc index 46cdb61263..7ff4621301 100644 --- a/src/docs/asciidoc/web/webflux-functional.adoc +++ b/src/docs/asciidoc/web/webflux-functional.adoc @@ -1,5 +1,6 @@ [[webflux-fn]] = Functional Endpoints +[.small]#<># 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. @@ -11,6 +12,7 @@ the same <> foundation. [[webflux-fn-overview]] == Overview +[.small]#<># In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes `ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono`). @@ -110,6 +112,7 @@ Most applications can run through the WebFlux Java configuration, see <># `ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly access to the HTTP request and response. @@ -437,6 +440,7 @@ See <>. [[webflux-fn-router-functions]] == `RouterFunction` +[.small]#<># 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 @@ -635,6 +639,7 @@ We can further improve by using the `nest` method together with `accept`: [[webflux-fn-running]] == Running a Server +[.small]#<># How do you run a router function in an HTTP server? A simple option is to convert a router function to an `HttpHandler` by using one of the following: @@ -740,6 +745,7 @@ The following example shows a WebFlux Java configuration (see [[webflux-fn-handler-filter-function]] == Filtering Handler Functions +[.small]#<># You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing function builder. diff --git a/src/docs/asciidoc/web/webmvc-functional.adoc b/src/docs/asciidoc/web/webmvc-functional.adoc new file mode 100644 index 0000000000..bab53033ef --- /dev/null +++ b/src/docs/asciidoc/web/webmvc-functional.adoc @@ -0,0 +1,776 @@ +[[webmvc-fn]] += Functional Endpoints +[.small]#<># + +Spring Web MVC includes WebMvc.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 <>. + + + + +[[webmvc-fn-overview]] +== Overview +[.small]#<># + +In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes +`ServerRequest` and returns a `ServerResponse`. +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. + +Incoming requests are routed to a handler function with a `RouterFunction`: a function that +takes `ServerRequest` and returns an optional `HandlerFunction` (i.e. `Optional`). +When the router function matches, a handler function is returned; otherwise an empty Optional. +`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()` provides a router builder that facilitates the creation of routers, +as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + import static org.springframework.http.MediaType.APPLICATION_JSON; + import static org.springframework.web.servlet.function.RequestPredicates.*; + import static org.springframework.web.servlet.function.RouterFunctions.route; + + PersonRepository repository = ... + PersonHandler handler = new PersonHandler(repository); + + 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 { + + // ... + + public Mono listPeople(ServerRequest request) { + // ... + } + + public Mono createPerson(ServerRequest request) { + // ... + } + + public Mono getPerson(ServerRequest request) { + // ... + } + } +---- + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val repository: PersonRepository = ... + val handler = PersonHandler(repository) + + val route = coRouter { // <1> + accept(APPLICATION_JSON).nest { + GET("/person/{id}", handler::getPerson) + GET("/person", handler::listPeople) + } + POST("/person", handler::createPerson) + } + + + class PersonHandler(private val repository: PersonRepository) { + + // ... + + suspend fun listPeople(request: ServerRequest): ServerResponse { + // ... + } + + suspend fun createPerson(request: ServerRequest): ServerResponse { + // ... + } + + suspend fun getPerson(request: ServerRequest): ServerResponse { + // ... + } + } +---- +<1> Create router using Coroutines router DSL, a Reactive alternative is also available via `router { }`. + + +If you register the `RouterFunction` as a bean, for instance by exposing it in a +@Configuration class, it will auto-detected by the servlet, as explained in <>. + + + + +[[webmvc-fn-handler-functions]] +== HandlerFunction +[.small]#<># + +`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly +access to the HTTP request and response, including headers, body, method, and status code. + + +[[webmvc-fn-request]] +=== `ServerRequest` + +`ServerRequest` provides access to the HTTP method, URI, headers, and query parameters, +while access to the body is provided through the `body` methods. + +The following example extracts the request body to a `String`: + +[source,java,role="primary"] +.Java +---- +String string = request.body(String.class); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val string = request.body() +---- + + +The following example extracts the body to a `List`, +where `Person` objects are decoded from a serialized form, such as JSON or XML: + +[source,java,role="primary"] +.Java +---- +List people = request.body(new ParameterizedTypeReference>() {}); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val people = request.body() +---- + +The following example shows how to access parameters: + +[source,java,role="primary"] +.Java +---- +MultiValueMap params = request.params(); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val map = request.params() +---- + + +[[webmvc-fn-response]] +=== `ServerResponse` + +`ServerResponse` provides access to the HTTP response and, since it is immutable, you can use +a `build` method to create it. You can use the builder to set the response status, to add response +headers, or to provide a body. The following example creates a 200 (OK) response with JSON +content: + +[source,java,role="primary"] +.Java +---- +Person person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val person: Person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person) +---- + +The following example shows how to build a 201 (CREATED) response with a `Location` header and no body: + +[source,java,role="primary"] +.Java +---- +URI location = ... +ServerResponse.created(location).build(); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val location: URI = ... +ServerResponse.created(location).build() +---- + + +[[webmvc-fn-handler-classes]] +=== Handler Classes + +We can write a handler function as a lambda, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- +HandlerFunction helloWorld = + request -> ServerResponse.ok().body("Hello World"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- +val helloWorld = HandlerFunction { ServerResponse.ok().body("Hello World") } +---- + +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,subs="verbatim,quotes",role="primary"] +.Java +---- +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +public class PersonHandler { + + private final PersonRepository repository; + + public PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + public ServerResponse listPeople(ServerRequest request) { // <1> + List people = repository.allPeople(); + return ok().contentType(APPLICATION_JSON).body(people); + } + + public ServerResponse createPerson(ServerRequest request) throws Exception { // <2> + Person person = request.body(Person.class); + repository.savePerson(person); + return ok().build(); + } + + public ServerResponse getPerson(ServerRequest request) { // <3> + int personId = Integer.parseInt(request.pathVariable("id")); + Person person = repository.getPerson(personId); + if (person != null) { + return ok().contentType(APPLICATION_JSON).body(person)) + } + else { + return ServerResponse.notFound().build(); + } + } + +} +---- +<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as +JSON. +<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. +<3> `getPerson` is a handler function that returns a single person, identified by the `id` path +variable. We retrieve that `Person` from the repository and create a JSON response, if it is +found. If it is not found, we return a 404 Not Found response. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class PersonHandler(private val repository: PersonRepository) { + + fun listPeople(request: ServerRequest): ServerResponse { // <1> + val people: List = repository.allPeople() + return ok().contentType(APPLICATION_JSON).body(people); + } + + fun createPerson(request: ServerRequest): ServerResponse { // <2> + val person = request.body() + repository.savePerson(person) + return ok().build() + } + + fun getPerson(request: ServerRequest): ServerResponse { // <3> + val personId = request.pathVariable("id").toInt() + return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) } + ?: ServerResponse.notFound().build() + + } + } +---- +<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as +JSON. +<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. +<3> `getPerson` is a handler function that returns a single person, identified by the `id` path +variable. We retrieve that `Person` from the repository and create a JSON response, if it is +found. If it is not found, we return a 404 Not Found response. + + +[[webmvc-fn-handler-validation]] +=== Validation + +A functional endpoint can use Spring's <> to +apply validation to the request body. For example, given a custom Spring +<> implementation for a `Person`: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + public class PersonHandler { + + private final Validator validator = new PersonValidator(); // <1> + + // ... + + public ServerResponse createPerson(ServerRequest request) { + Person person = request.body(Person.class); + validate(person); // <2> + repository.savePerson(person); + return ok().build(); + } + + private void validate(Person person) { + Errors errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + throw new ServerWebInputException(errors.toString()); // <3> + } + } + } +---- +<1> Create `Validator` instance. +<2> Apply validation. +<3> Raise exception for a 400 response. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class PersonHandler(private val repository: PersonRepository) { + + private val validator = PersonValidator() // <1> + + // ... + + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.body() + validate(person) // <2> + repository.savePerson(person) + return ok().build() + } + + private fun validate(person: Person) { + val errors: Errors = BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + throw ServerWebInputException(errors.toString()) // <3> + } + } + } +---- +<1> Create `Validator` instance. +<2> Apply validation. +<3> Raise exception for a 400 response. + +Handlers can also use the standard bean validation API (JSR-303) by creating and injecting +a global `Validator` instance based on `LocalValidatorFactoryBean`. +See <>. + + + +[[webmvc-fn-router-functions]] +== `RouterFunction` +[.small]#<># + +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. + + +[[webmvc-fn-predicates]] +=== Predicates + +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 uses a request predicate to create a constraint based on the `Accept` +header: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().body("Hello World")); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + GET("/hello-world", accept(TEXT_PLAIN)) { + ServerResponse.ok().body("Hello World") + } + } +---- + +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)` +and `RequestPredicates.path(String)`. +The example shown above also uses two request predicates, as the builder uses +`RequestPredicates.GET` internally, and composes that with the `accept` predicate. + + + +[[webmvc-fn-routes]] +=== Routes + +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()`. + +The following example shows the composition of four routes: + + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.servlet.function.RequestPredicates.*; + +PersonRepository repository = ... +PersonHandler handler = new PersonHandler(repository); + +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. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.http.MediaType.APPLICATION_JSON + + val repository: PersonRepository = ... + val handler = PersonHandler(repository); + + val otherRoute: RouterFunction = coRouter { } + + val route = coRouter { + GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> + GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> + POST("/person", handler::createPerson) // <3> + }.and(otherRoute) // <4> +---- +<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 WebMvc.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",role="primary"] +.Java +---- +RouterFunction route = route() + .path("/person", builder -> builder // <1> + .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET("", accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson)) + .build(); +---- +<1> Note that second parameter of `path` is a consumer that takes the router builder. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + "/person".nest { + GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + GET("", accept(APPLICATION_JSON), handler::listPeople) + POST("/person", handler::createPerson) + } + } +---- + +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",role="primary"] +.Java +---- + 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(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + "/person".nest { + accept(APPLICATION_JSON).nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + POST("/person", handler::createPerson) + } + } + } +---- + + +[[webmvc-fn-running]] +== Running a Server +[.small]#<># + +You typically run router functions in a <>-based setup through the +<>, which uses Spring configuration to declare the +components required to process requests. The MVC Java configuration declares the following +infrastructure components to support functional endpoints: + +* `RouterFunctionMapping`: Detects one or more `RouterFunction` beans in the Spring +configuration, combines them through `RouterFunction.andOther`, and routes requests to the +resulting composed `RouterFunction`. +* `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke +a `HandlerFunction` that was mapped to a request. + +The preceding components let functional endpoints fit within the `DispatcherServlet` request +processing lifecycle and also (potentially) run side by side with annotated controllers, if +any are declared. It is also how functional endpoints are enabled by the Spring Boot Web +starter. + +The following example shows a WebFlux Java configuration: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + @Configuration + @EnableMvc + public class WebConfig implements WebMvcConfigurer { + + @Bean + public RouterFunction routerFunctionA() { + // ... + } + + @Bean + public RouterFunction routerFunctionB() { + // ... + } + + // ... + + @Override + public void configureMessageConverters(List> converters) { + // configure message conversion... + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + // configure CORS... + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + // configure view resolution for HTML rendering... + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebMvcConfigurer { + + @Bean + fun routerFunctionA(): RouterFunction<*> { + // ... + } + + @Bean + fun routerFunctionB(): RouterFunction<*> { + // ... + } + + // ... + + override fun configureMessageConverters(converters: List>) { + // configure message conversion... + } + + override fun addCorsMappings(registry: CorsRegistry) { + // configure CORS... + } + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + // configure view resolution for HTML rendering... + } + } +---- + + + + +[[webmvc-fn-handler-filter-function]] +== Filtering Handler Functions +[.small]#<># + +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",role="primary"] +.Java +---- + 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. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = router { + "/person".nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + before { // <1> + ServerRequest.from(it) + .header("X-RequestHeader", "Value").build() + } + POST("/person", handler::createPerson) + after { _, response -> // <2> + logResponse(response) + } + } + } +---- +<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: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + SecurityManager securityManager = ... + + 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(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val securityManager: SecurityManager = ... + + val route = router { + ("/person" and accept(APPLICATION_JSON)).nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + POST("/person", handler::createPerson) + filter { request, next -> + if (securityManager.allowAccessTo(request.path())) { + next(request) + } + else { + status(UNAUTHORIZED).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. + +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 +<>. diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 5516346ab5..eb0c45bdeb 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -3904,6 +3904,7 @@ performance if used extensively. See the {api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] javadoc for more details. +include::webmvc-functional.adoc[leveloffset=+1]