Spring WebFlux contains WebFlux.fn, a lightweight functional programming model in which functions are used to route and process requests, and contracts are designed to be immutable. It is an alternative to the annotation-based programming model, but otherwise works on the same basis from Reactive Core.
Brief Description
In WebFlux.fn, an HTTP request is processed using HandlerFunction: A function that accepts a ServerRequest and returns a deferred ServerResponse (i.e. Mono<ServerResponse>). Both the request and response objects have immutable contracts that provide JDK 8-compliant access to the HTTP request and response. HandlerFunction is the equivalent of a method body with the @RequestMapping in the annotation-based programming model.
Incoming requests are routed to a handler function using RouterFunction: a function that accepts a ServerRequest and returns a deferred HandlerFunction (i.e. Mono<HandlerFunction>). If the router function matches, a handler function is returned; otherwise, an empty Mono function is returned. RouterFunction is the equivalent of the @RequestMapping annotation, but with the significant difference that router functions provide not only data, but also operating logic.
RouterFunctions.route() provides a router builder that makes it easy to create routers, as shown in the following example:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> 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<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter {
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 {
// ...
}
}
- Create router using Coroutines router DSL, a Reactive alternative is also available via
router { }.
One way to run a RouterFunction is to turn it into a HttpHandler and install via one of the built-in server adapters:
RouterFunctions.toHttpHandler(RouterFunction)RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
Most applications can be launched through the WebFlux Java configuration.
HandlerFunction
ServerRequest and ServerResponse are immutable interfaces that provide JDK 8 compliant access to HTTP request and response. Both request and response provide callback Reactive Streams for body streams. The request body is represented using Flux or Mono from Reactor. The response body is presented using any Publisher from Reactive Streams, including Flux and Mono.
ServerRequest
ServerRequest provides access to the HTTP method, URI, request headers and parameters, and access to the body is provided through the body methods.
In The following example extracts the request body into a Mono<String>:
Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()
In the following example, the body is retrieved in Flux<Person> (or Flow<Person> in Kotlin), where Person objects are decoded from some serialized form, such as JSON or XML:
Flux<Person> people = request.bodyToFlux(Person.class);
val people = request.bodyToFlow<Person>()
The previous examples are shortcuts using the more generic ServerRequest.body (BodyExtractor), which accepts the BodyExtractor functional strategy interface. The BodyExtractors helper class provides access to multiple instances. For example, the previous examples could be written as follows:
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
The following example shows how to access form data:
Mono<MultiValueMap<String, String>> map = request.formData();
val map = request.awaitFormData()
The following example shows how to access multipart data as a Map:
Mono<MultiValueMap<String, Part>> map = request.multipartData();
val map = request.awaitMultipartData()
The following example shows how to access multiple components, one at a time, in streaming mode :
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse
ServerResponse provides access to the HTTP response, and since it is immutable, you can use the build method to create it. You can use the build tool to set the response status, add response headers, or provide the response body. The following example generates a 200 (OK) response with JSON formatted content:
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
The following example shows how to create a 201 (CREATED) response with a Location header and no body:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
Depending on the codec used, you can pass hint parameters to customize how the body is serialized or deserialized. For example, to define a Jackson JSON-based view:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java) .body(...)
Handler classes
You can write a handler function as a lambda expression, as shown in the following example:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
This is convenient , but we need multiple functions in an application, and multiple inline lambda expressions can lead to confusion. Therefore, it is useful to group related handler functions into a handler class, which plays the same role as the @Controller annotation in an annotation-based application. For example, the following class represents a reactive Person store:
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 Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
listPeopleis a handler function that returns allPersonfound in the store, in JSON format.createPersonis a handler function that saves a newPersonobject contained in the body of the request. Note thatPersonRepository.savePerson(Person)returnsMono<Void>: an emptyMonothat generates a termination signal if Person has been read from request and saved. Therefore, thebuild(Publisher<Void>)method is used to send a response when a completion signal is received (that is, whenPersonis saved).getPersonis a handler function that returns a single Person object identified by the path variableid. We retrieve thisPersonobject from storage and generate a JSON response if it is found. If it is not found,switchIfEmpty(Mono<T>)is used to return a 404 Not Found response.
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse {
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse {
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
listPeopleis a handler function that returns allPersonobjects found in the store in JSON format.createPersonis a handler function that stores a newPersonobject contained in the request body. Note thatPersonRepository.savePerson(Person)returnsMono<Void>: an emptyMonothat generates a termination signal if Person has been read from request and saved. Therefore, thebuild(Publisher<Void>)method is used to send a response when a completion signal is received (that is, whenPersonis saved).getPersonis a handler function that returns a single Person object identified by the path variableid. We retrieve thisPersonobject from storage and generate a JSON response if it is found. If it is not found,switchIfEmpty(Mono<T>)is used to return a 404 Not Found response.
Validation
A function endpoint can use Spring validators to apply validation to the request body. For example, given a custom implementation of Validator from Spring for the Person object:
public class PersonHandler {
private final Validator validator = new PersonValidator();
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
- Create an instance of
Validator. - Apply validation.
- Throw an exception when the response is 400.
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator()
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person)
repository.savePerson(person)
return ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString())
}
}
}
- Create an instance of
Validator. - Apply validation.
- Generate an exception when the response is 400.
Handlers can also use standard bean validation API (JSR-303), creating and injecting a global Validator instance based on a LocalValidatorFactoryBean.
RouterFunction
Router functions are used to route requests to the corresponding HandlerFunction. As a rule, router functions are not written independently, but to create them, a method is used for the auxiliary class RouterFunctions. RouterFunctions.route() (no parameters) provides a convenient builder for creating a router function, while RouterFunctions.route(RequestPredicate, HandlerFunction) provides a direct way to create a router .
As a matter of fact, it is recommended to use the route() builder because it provides convenient shortcuts for common display scenarios without requiring the import of static elements that are difficult to detect. For example, the router function builder offers a GET(String, HandlerFunction) method to create a Map for GET requests; and POST(String, HandlerFunction) - for POST requests.
In addition to mapping based on HTTP methods, the route builder offers a way to introduce additional predicates when mapping to requests. For each HTTP method there is an overload that takes RequestPredicate as a parameter through which additional restrictions can be expressed.
Predicates
You can write your own native RequestPredicate, but a helper class RequestPredicates provides commonly used implementations based on 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:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
You can string multiple request predicates together using:
RequestPredicate.and(RequestPredicate)- both must match.RequestPredicate.or(RequestPredicate)- any of them can match.
Many predicates from RequestPredicates are compound. For example, RequestPredicates.GET(String) consists of RequestPredicates.method(HttpMethod) and RequestPredicates.path(String). The example above also uses two request predicates because the builder uses RequestPredicates.GET internally and combines it with the accept predicate.
Routes
Router functions are evaluated in an orderly manner: if the first route does not match, then the second is evaluated, and so on. Therefore, it makes sense to declare more specific routes before generic ones. This is also important when registering router functions as Spring beans, as discussed later. Note that this operating logic differs from the annotation-based programming model, where the "most specific" controller method is selected automatically.
When using the router function builder, all defined routes are assembled into a single RouterFunction, which is returned from build(). There are other ways to build multiple router functions into one:
add(RouterFunction)on theRouterFunctions.route()builderRouterFunction.and(RouterFunction)RouterFunction.andRoute(RequestPredicate, HandlerFunction)- shorthand forRouterFunction.and()with nestedRouterFunctions.route().
The following example shows a layout of four routes:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
GET /person/{id}with anAcceptheader that matches JSON is routed toPersonHandler.getPersonGET /personwith aAcceptheader that conforms to JSON format is sent toPersonHandler.listPeoplePOST /personwithout additional predicates is mapped toPersonHandler.createPerson, andotherRouteis a router function that is created elsewhere and added to the constructed route.
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("/person", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}.and(otherRoute)
GET /person/{id}with anAcceptheader that matches JSON is routed toPersonHandler.getPersonGET /personwith aAcceptheader that matches the format JSON, sent toPersonHandler.listPeoplePOST /personwithout additional predicates is mapped toPersonHandler.createPerson, andotherRouteis a router function that is created in another place and added to the constructed route.
Nested routes
Typically, a group of router functions have a common predicate, such as a common path. In the example above, the common predicate would be the path predicate that matches /person, used by the three routes. When using annotations, you can eliminate this duplication by using the @RequestMapping annotation at the type level, which maps to /person. In WebFlux.fn, path predicates can be shared using the path method in the router function constructor. For example, the last few lines of the example above could be expanded to look like this, using nested routes:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
- Note that the second parameter is
pathis the recipient that the router builder receives.
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
Although path-based nesting is the most common, you can nest a predicate of any type using the nest method in the build tool. The above still contains some duplication in the form of the common Accept-header predicate. We will continue to expand the code using the nest method along with accept:
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();
val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
POST(handler::createPerson)
}
}
}
Starting the server
How to start router function in HTTP server? A simple option is to convert the router function to HttpHandler using one of the following:
RouterFunctions.toHttpHandler(RouterFunction)RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
You can then use the returned HttpHandler with different server adapters, following the instructions regarding HttpHandler for a specific server.
A more typical option, also used in Spring Boot, is to work with DispatcherHandler-based configuration via WebFlux configuration, which uses Spring configuration to declare the components needed to handle requests . The WebFlux Java configuration declares the following infrastructure components to support functional endpoints:
RouterFunctionMapping: Discovers one or moreRouterFunction<?>in the Spring configuration, orders them, combines them usingRouterFunction.andOtherand routes requests to the resulting compositeRouterFunction.HandlerFunctionAdapter: A simple adapter that allows theDispatcherHandlerto call theHandlerFunctionthat was mapped to the request.ServerResponseResultHandler: Processes the result of a call toHandlerFunctionby calling the writeTo method on ServerResponse.
The previous components allow functional endpoints points to “fit” into the request processing lifecycle of DispatcherServlet, and also (potentially) work with annotated controllers, if declared, simultaneously. This is also a way to activate functional endpoints using a starter from Spring Boot for WebFlux.
The following example shows the WebFlux Java configuration:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure converting messages...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure the view resolution for HTML visualization...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure converting messages...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure the view resolution for HTML visualization...
}
}
Filtering Handler Functions
You can filter handler functions using the before, after or filter in the forty routing functions tool. Annotations can achieve similar functionality by using @ControllerAdvice, ServletFilter, or both. The filter will be applied to all routes built by the build tool. This means that filters defined in nested routes are not applied to "higher level" routes. For example, consider the following:
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)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response))
.build();
beforefilter that adds a custom title request, applies only to two routes of the GET method.- The
afterfilter, which records the response, applies to all routes, including nested ones.
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before {
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST(handler::createPerson)
after { _, response ->
logResponse(response)
}
}
}
- The
beforefilter, which adds a custom request header, applies only to the two GET method routes. - The
afterfilter, which logs the response, applies to all routes, including nested.
The filter method in the router constructor accepts a HandlerFilterFunction: a function that accepts a ServerRequest and HandlerFunction and returns ServerResponse. The handler function parameter is the next element in the chain. Typically this is the handler to which the request is routed, but it could be another filter if there is more than one applied.
We can now add a simple security filter to our route, assuming we have a SecurityManager , which can determine whether a particular path is valid. The following example shows how to do this:
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();
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 previous example shows that calling next.handle(ServerRequest) is optional. We allow the handler function to run only when access is allowed.
In addition to using the filter method in the router function constructor, you can apply a filter to an existing router function via RouterFunction.filter(HandlerFilterFunction).
CorsWebFilter.
GO TO FULL VERSION