1056 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
			
		
		
	
	
			1056 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
| [[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 xref:web/webmvc/mvc-servlet.adoc[DispatcherServlet].
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| [[webmvc-fn-overview]]
 | |
| == Overview
 | |
| [.small]#xref:web/webflux-functional.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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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()`.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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 xref:web/webmvc-functional.adoc#webmvc-fn-running[Running a Server].
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| [[webmvc-fn-handler-functions]]
 | |
| == HandlerFunction
 | |
| [.small]#xref:web/webflux-functional.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`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| String string = request.body(String.class);
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin]
 | |
| ----
 | |
| val people = request.body<Person>()
 | |
| ----
 | |
| ======
 | |
| 
 | |
| The following example shows how to access parameters:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| MultiValueMap<String, String> params = request.params();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| Person person = ...
 | |
| ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| URI location = ...
 | |
| ServerResponse.created(location).build();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java]
 | |
| ----
 | |
| Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
 | |
| ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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:
 | |
| 
 | |
| --
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| HandlerFunction<ServerResponse> helloWorld =
 | |
|   request -> ServerResponse.ok().body("Hello World");
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 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:
 | |
| 
 | |
| --
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 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.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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 xref:web/webmvc/mvc-config/validation.adoc[validation facilities] to
 | |
| apply validation to the request body. For example, given a custom Spring
 | |
| xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Person`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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 xref:core/validation/beanvalidation.adoc[Spring Validation].
 | |
| 
 | |
| 
 | |
| 
 | |
| [[webmvc-fn-router-functions]]
 | |
| == `RouterFunction`
 | |
| [.small]#xref:web/webflux-functional.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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	RouterFunction<ServerResponse> route = RouterFunctions.route()
 | |
| 		.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
 | |
| 			request -> ServerResponse.ok().body("Hello World")).build();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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:
 | |
| 
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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]]
 | |
| === 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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 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.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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`:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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-serving-resources]]
 | |
| == Serving Resources
 | |
| 
 | |
| WebMvc.fn provides built-in support for serving resources.
 | |
| 
 | |
| NOTE: In addition to the capabilities described below, it is possible to implement even more flexible resource handling thanks to
 | |
| {spring-framework-api}++/web/servlet/function/RouterFunctions.html#resources(java.util.function.Function)++[`RouterFunctions#resource(java.util.function.Function)`].
 | |
| 
 | |
| [[webmvc-fn-resource]]
 | |
| === Redirecting to a resource
 | |
| 
 | |
| It is possible to redirect requests matching a specified predicate to a resource. This can be useful, for example,
 | |
| for handling redirects in Single Page Applications.
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
|     ClassPathResource index = new ClassPathResource("static/index.html");
 | |
| 	List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
 | |
| 	RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
 | |
| 	RouterFunction<ServerResponse> redirectToIndex = route()
 | |
| 		.resource(spaPredicate, index)
 | |
| 		.build();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
|     val redirectToIndex = router {
 | |
| 		val index = ClassPathResource("static/index.html")
 | |
| 		val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
 | |
| 		val spaPredicate = !(path("/api/**") or path("/error") or
 | |
| 			pathExtension(extensions::contains))
 | |
| 		resource(spaPredicate, index)
 | |
| 	}
 | |
| ----
 | |
| ======
 | |
| 
 | |
| [[webmvc-fn-resources]]
 | |
| === Serving resources from a root location
 | |
| 
 | |
| It is also possible to route requests that match a given pattern to resources relative to a given root location.
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
|     Resource location = new FileUrlResource("public-resources/");
 | |
|     RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
|     val location = FileUrlResource("public-resources/")
 | |
|     val resources = router { resources("/resources/**", location) }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| 
 | |
| [[webmvc-fn-running]]
 | |
| == Running a Server
 | |
| [.small]#xref:web/webflux-functional.adoc#webflux-fn-running[See equivalent in the Reactive stack]#
 | |
| 
 | |
| You typically run router functions in a xref:web/webmvc/mvc-servlet.adoc[`DispatcherHandler`]-based setup through the
 | |
| xref:web/webmvc/mvc-config.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, xref:core/beans/annotation-config/autowired.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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	@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...
 | |
| 		}
 | |
| 	}
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	@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]#xref:web/webflux-functional.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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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.
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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:
 | |
| 
 | |
| [tabs]
 | |
| ======
 | |
| Java::
 | |
| +
 | |
| [source,java,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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();
 | |
| ----
 | |
| 
 | |
| Kotlin::
 | |
| +
 | |
| [source,kotlin,indent=0,subs="verbatim,quotes"]
 | |
| ----
 | |
| 	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
 | |
| xref:web/webmvc-cors.adoc#mvc-cors-filter[`CorsFilter`].
 |