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());
}
}
listPeople
is a handler function that returns allPerson
found in the store, in JSON format.createPerson
is a handler function that saves a newPerson
object contained in the body of the request. Note thatPersonRepository.savePerson(Person)
returnsMono<Void>
: an emptyMono
that 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, whenPerson
is saved).getPerson
is a handler function that returns a single Person object identified by the path variableid
. We retrieve thisPerson
object 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()
}
}
listPeople
is a handler function that returns allPerson
objects found in the store in JSON format.createPerson
is a handler function that stores a newPerson
object contained in the request body. Note thatPersonRepository.savePerson(Person)
returnsMono<Void>
: an emptyMono
that 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, whenPerson
is saved).getPerson
is a handler function that returns a single Person object identified by the path variableid
. We retrieve thisPerson
object 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 anAccept
header that matches JSON is routed toPersonHandler.getPerson
GET /person
with aAccept
header that conforms to JSON format is sent toPersonHandler.listPeople
POST /person
without additional predicates is mapped toPersonHandler.createPerson
, andotherRoute
is 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 anAccept
header that matches JSON is routed toPersonHandler.getPerson
GET /person
with aAccept
header that matches the format JSON, sent toPersonHandler.listPeople
POST /person
without additional predicates is mapped toPersonHandler.createPerson
, andotherRoute
is 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
path
is 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.andOther
and routes requests to the resulting compositeRouterFunction
.HandlerFunctionAdapter
: A simple adapter that allows theDispatcherHandler
to call theHandlerFunction
that was mapped to the request.ServerResponseResultHandler
: Processes the result of a call toHandlerFunction
by 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();
before
filter that adds a custom title request, applies only to two routes of the GET method.- The
after
filter, 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
before
filter, which adds a custom request header, applies only to the two GET method routes. - The
after
filter, 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
.