Spring Web MVC contains WebMvc.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 runs on the same DispatcherServlet.
Brief Description
In WebMvc.fn,
an HTTP request is handled using a HandlerFunction
: a function that accepts ServerRequest
and returns ServerResponse
. Both the request and response objects have immutable contracts that provide
JDK 8-friendly access to the HTTP request and response. HandlerFunction
is the equivalent of a method
body marked with the @RequestMapping
annotation, in an annotation-based programming model.
Incoming requests are routed to a handler function using RouterFunction
: a function that accepts a
ServerRequest
and returns an optional HandlerFunction
(i.e. Optional<HandlerFunction>
).
If the router function matches, the handler function is returned, otherwise an empty Optional is returned. RouterFunction
is the equivalent of the @RequestMapping
annotation, but with the significant difference that router
functions transfer 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.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.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 ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router {
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 {
// ...
}
}
- Create router using the router DSL.
If registered RouterFunction
as a bean, for example, by opening it in a class with the @Configuration
annotation, it will be automatically discovered by the servlet.
HandlerFunction
ServerRequest
and ServerResponse
are immutable interfaces that provide access to the HTTP
request and response, including headers, body, method, and JDK 8-friendly status code.
ServerRequest
ServerRequest
provides access to the HTTP method, URI, headers and parameters of the request, and
access to the body is provided through the body
methods.
The following example extracts the
request body into a String
:
String string = request.body (String.class);
val string = request.body<String>()
The following example retrieves the body into a List<Person>
, where the Person
objects are decoded from the serialized form, such as JSON or XML format:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
The following example shows how to access the parameters:
MultiValueMap<String, String> params = request.params();
val map = request.params()
ServerResponse
ServerResponse
provides access to the HTTP response, and since it is
immutable, you can use the build method to its creation. You can use the build tool to configure the response
status, add response headers, or pass the response body. The following example generates a 200 (OK) response with
JSON formatted content:
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON ).body(person);
val person: Person = ...
ServerResponse.ok( ).contentType(MediaType.APPLICATION_JSON).body(person)
The following example shows how to create a 201 (CREATED) response with the header Location
and
without body:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
You can also use an asynchronous result as a body in the form of CompletableFuture
, Publisher
or any other type supported by ReactiveAdapterRegistry
. For example:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
If not only the body, but also the status or headers are based on an asynchronous type, you can use the static
async
method on ServerResponse
, which takes
CompletableFuture<ServerResponse>
, Publisher<ServerResponse>
, or any other
asynchronous type supported by ReactiveAdapterRegistry
. For example:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
Events sent by the server can be sent using
the static sse
method for ServerResponse
. The build facility provided by this method
allows you to send strings or other objects in JSON format. For example:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere...
}));
}
// In some other thread sending the string
sseBuilder.send("Hello world");
// Or an object that will be converted to JSON
Person person = ...
sseBuilder.send(person);
// Set up the event using other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and at some point it will be ready
sseBuilder.complete();
fun sse( ): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Save the sseBuilder object somewhere..
}
}
// In some other thread sending the string
sseBuilder.send("Hello world")
// Or an object that will be converted to JSON
val person = ...
sseBuilder.send (person)
// Set up the event using other methods
sseBuilder.id("42")
.event("sse event")
.data(person)
// and at some point it will be ready
sseBuilder.complete()
Handler classes
We can write a handler function as a lambda expression, as shown in the following example:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest ) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
This is convenient, but in the application we need several functions, and several built-in Lambda expressions
can get confusing. 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 ServerResponse listPeople(ServerRequest request) {
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception {
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) {
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();
}
}
}
listPeople
is a handler function that returns allPerson
objects found in the repository in JSON format.createPerson
is a handler function that saves a newPerson
object code> contained in the body of the request.getPerson
is a handler function that returns a single person 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, we return a 404 Not Found response.
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request : ServerRequest): ServerResponse {
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
} fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse {
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
listPeople
is a handler function that returns allPerson
objects found in the repository, in JSON format.createPerson
is a handler function that saves a newPerson
object contained in the request body.getPerson
is a handler function that returns a single person 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, we return a 404 Not Found response.
Validation
The function endpoint can use Spring's validators to apply validation to the body of the
request. For example, here is a custom Spring implementation of Validator for Person
:
public class PersonHandler {
private final Validator validator = new PersonValidator() ;
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person);
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());
}
}
}
- Create an instance of
Validator
. - Apply validation.
- Throw an exception for the 400 response.
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() /
/ ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>() validate(person)
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())
}
}
}
- Create an instance of
Validator
. - Apply validation.
- Throw an exception for response 400.
Handlers can also use the standard bean validation API (JSR-303) by 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().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("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 combine multiple router functions
into one:
add(RouterFunction)
to the build toolRouterFunctions.route()
RouterFunction.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.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)
.GET ("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
GET /person/{id}
with headerAccept
that matches the JSON format is routed toPersonHandler.getPerson
GET /person
with the headerAccept
that matches the JSON format is routed 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
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)
GET("/person", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler: :createPerson)
}.and(otherRoute)
GET /person/{id}
with headerAccept
that matches the JSON format is routed toPersonHandler.getPerson
GET /person
with the headerAccept
that conforms to JSON format is routed 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.
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 WebMvc.fn,
path predicates can be shared using the path
method in the router function builder. 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
path
parameter is the destination that the router builder accepts.
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON ), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
Although the attachment is on path-based 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();
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)
}
}
}
Starting the server
Typically router functions in a configuration based on DispatcherHandler
are launched through an MVC
configuration, which leverages the Spring configuration to declare the components needed to handle requests. The
Java MVC configuration declares the following infrastructure components to support functional endpoints:
RouterFunctionMapping
: Discovers one or moreRouterFunction<?>
beans in the Spring configuration, orders them, combines them usingRouterFunction.andOther
and routes requests to the resulting composite functionRouterFunction
.HandlerFunctionAdapter
: A simple adapter that allows theDispatcherHandler
to call theHandlerFunction
that was mapped to the request.
The previous components allow functional endpoints to fit into the DispatcherServlet
request
lifecycle, and also (potentially) work side by side with annotated controllers, if declared. This is also the
way to enable functional endpoints in the Spring Boot Web launcher.
The following example shows the WebFlux Java configuration:
@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...
}
}
@Configuration
@EnableMvcclass 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...
}
}
Filter handler functions
You can filter handler functions using the before
, after
, or filter
methods in the routing function builder. 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 the "top level" routes. For example, consider the
following example:
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();
- The
before
filter adding custom request header, applies only to two GET routes. - The
after
filter that records the response applies to all routes, including nested ones.
import org.springframework.web.servlet.function.router
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 two GET routes. - The
after
filter, which logs the response, applies to all routes, including nested ones.
The filter
method in the router build tool 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 a different filter if more than one is applied.
We can now add a simple security filter to our route, assuming we have a SecurityManager
, which can
determine whether a certain 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();
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 previous example demonstrated that calling next.handle(ServerRequest)
is optional. We allow the
function to execute -handler only when access is allowed.
In addition to using the filter
method in the router function builder, you can apply a filter to an
existing router function via RouterFunction.filter( HandlerFilterFunction)
.
CorsFilter
.