Migrate Structure
This commit is contained in:
881
framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc
Normal file
881
framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc
Normal file
@@ -0,0 +1,881 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
|
||||
|
||||
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 <<web#mvc-servlet>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-overview]]
|
||||
== Overview
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-overview, See equivalent in the Reactive stack>>#
|
||||
|
||||
In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
|
||||
`ServerRequest` and returns a `ServerResponse`.
|
||||
Both the request and 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<HandlerFunction>`).
|
||||
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<ServerResponse> route = route() // <1>
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST("/person", handler::createPerson)
|
||||
.build();
|
||||
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
// ...
|
||||
|
||||
public ServerResponse listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using `route()`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository)
|
||||
|
||||
val route = router { // <1>
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/person/{id}", handler::getPerson)
|
||||
GET("/person", handler::listPeople)
|
||||
}
|
||||
POST("/person", handler::createPerson)
|
||||
}
|
||||
|
||||
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
// ...
|
||||
|
||||
fun listPeople(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun getPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using the router DSL.
|
||||
|
||||
|
||||
If you register the `RouterFunction` as a bean, for instance by exposing it in a
|
||||
`@Configuration` class, it will be auto-detected by the servlet, as explained in <<webmvc-fn-running>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-functions]]
|
||||
== HandlerFunction
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-handler-functions, See equivalent in the Reactive stack>>#
|
||||
|
||||
`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<String>()
|
||||
----
|
||||
|
||||
|
||||
The following example extracts the body to a `List<Person>`,
|
||||
where `Person` objects are decoded from a serialized form, such as JSON or XML:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val people = request.body<Person>()
|
||||
----
|
||||
|
||||
The following example shows how to access parameters:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
MultiValueMap<String, String> 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()
|
||||
----
|
||||
|
||||
You can also use an asynchronous result as the body, in the form of a `CompletableFuture`,
|
||||
`Publisher`, or any other type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person = webClient.get().retrieve().awaitBody<Person>()
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
|
||||
----
|
||||
|
||||
If not just the body, but also the status or headers are based on an asynchronous type,
|
||||
you can use the static `async` method on `ServerResponse`, which
|
||||
accepts `CompletableFuture<ServerResponse>`, `Publisher<ServerResponse>`, or
|
||||
any other asynchronous type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
|
||||
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
|
||||
ServerResponse.async(asyncResponse);
|
||||
----
|
||||
|
||||
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the
|
||||
static `sse` method on `ServerResponse`. The builder provided by that method
|
||||
allows you to send Strings, or other objects as JSON. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public RouterFunction<ServerResponse> sse() {
|
||||
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
|
||||
// Save the sseBuilder object somewhere..
|
||||
}));
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world");
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
Person person = ...
|
||||
sseBuilder.send(person);
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person);
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
fun sse(): RouterFunction<ServerResponse> = router {
|
||||
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
|
||||
// Save the sseBuilder object somewhere..
|
||||
}
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world")
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
val person = ...
|
||||
sseBuilder.send(person)
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person)
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[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<ServerResponse> helloWorld =
|
||||
request -> ServerResponse.ok().body("Hello World");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val helloWorld: (ServerRequest) -> ServerResponse =
|
||||
{ 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<Person> 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<Person> = repository.allPeople()
|
||||
return ok().contentType(APPLICATION_JSON).body(people);
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse { // <2>
|
||||
val person = request.body<Person>()
|
||||
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 <<core.adoc#validation, validation facilities>> to
|
||||
apply validation to the request body. For example, given a custom Spring
|
||||
<<core.adoc#validation, Validator>> 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>
|
||||
|
||||
// ...
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
val person = request.body<Person>()
|
||||
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 <<core.adoc#validation-beanvalidation, Spring Validation>>.
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-router-functions]]
|
||||
== `RouterFunction`
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-router-functions, See equivalent in the Reactive stack>>#
|
||||
|
||||
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, through 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<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
|
||||
request -> ServerResponse.ok().body("Hello World")).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
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.
|
||||
This is also important when registering router functions as Spring beans, as will
|
||||
be described later.
|
||||
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<ServerResponse> otherRoute = ...
|
||||
|
||||
RouterFunction<ServerResponse> 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> pass:q[`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
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository);
|
||||
|
||||
val otherRoute = router { }
|
||||
|
||||
val route = router {
|
||||
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> pass:q[`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<ServerResponse> route = route()
|
||||
.path("/person", builder -> builder // <1>
|
||||
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST(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
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest { // <1>
|
||||
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Using `nest` DSL.
|
||||
|
||||
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<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webmvc-fn-running]]
|
||||
== Running a Server
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-running, See equivalent in the Reactive stack>>#
|
||||
|
||||
You typically run router functions in a <<web.adoc#mvc-servlet, `DispatcherHandler`>>-based setup through the
|
||||
<<web.adoc#mvc-config>>, 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, <<core.adoc#beans-factory-ordered, orders them>>, 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<HttpMessageConverter<?>> 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
|
||||
@EnableMvc
|
||||
class WebConfig : WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
fun routerFunctionA(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun routerFunctionB(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
|
||||
// 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]#<<web-reactive.adoc#webflux-fn-handler-filter-function, See equivalent in the Reactive stack>>#
|
||||
|
||||
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<ServerResponse> 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(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
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET(handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
}
|
||||
}
|
||||
POST(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<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(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
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val securityManager: SecurityManager = ...
|
||||
|
||||
val route = router {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(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 only let the handler function be run 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
|
||||
<<webmvc-cors.adoc#mvc-cors-filter, `CorsFilter`>>.
|
||||
Reference in New Issue
Block a user