Add Kotlin code snippets to WebFlux refdoc

See gh-21778
This commit is contained in:
Sebastien Deleuze 2019-08-28 11:18:26 +02:00
parent 5b4ad8bf36
commit 14558844bc
6 changed files with 2467 additions and 667 deletions

View File

@ -83,24 +83,43 @@ The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`]
annotation enables cross-origin requests on annotated controller methods, as the
following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@RestController
@RequestMapping("/account")
public class AccountController {
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@CrossOrigin
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
}
----
By default, `@CrossOrigin` allows:
@ -119,48 +138,90 @@ should be used only where appropriate.
`@CrossOrigin` is supported at the class level, too, and inherited by all methods.
The following example specifies a certain domain and sets `maxAge` to an hour:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
}
----
You can use `@CrossOrigin` at both the class and the method level,
as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@CrossOrigin(maxAge = 3600) <1>
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin(maxAge = 3600) // <1>
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com") <2>
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@CrossOrigin("https://domain2.com") // <2>
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
----
<1> Using `@CrossOrigin` at the class level.
<2> Using `@CrossOrigin` at the method level.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@CrossOrigin(maxAge = 3600) // <1>
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com") // <2>
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
}
----
<1> Using `@CrossOrigin` at the class level.
<2> Using `@CrossOrigin` at the method level.
@ -191,26 +252,46 @@ should be used only where appropriate.
To enable CORS in the WebFlux Java configuration, you can use the `CorsRegistry` callback,
as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
// Add more mappings...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
}
----
@ -232,25 +313,47 @@ for CORS.
To configure the filter, you can declare a `CorsWebFilter` bean and pass a
`CorsConfigurationSource` to its constructor, as the following example shows:
[source,java,indent=0]
[subs="verbatim"]
[source,java,indent=0,subs="verbatim",role="primary"]
.Java
----
@Bean
CorsWebFilter corsFilter() {
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
return new CorsWebFilter(source);
}
----
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
.Kotlin
----
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
----

View File

@ -28,41 +28,75 @@ difference that router functions provide not just data, but also behavior.
`RouterFunctions.route()` provides a router builder that facilitates the creation of routers,
as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
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);
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();
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 class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter { // <1>
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 {
// ...
}
}
----
<1> 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 an `HttpHandler` and install it
through one of the built-in <<web-reactive.adoc#webflux-httphandler, server adapters>>:
@ -95,50 +129,88 @@ while access to the body is provided through the `body` methods.
The following example extracts the request body to a `Mono<String>`:
[source,java]
[source,java,role="primary"]
.Java
----
Mono<String> string = request.bodyToMono(String.class);
----
[source,kotlin,role="secondary"]
.Kotlin
----
val string = request.awaitBody<String>()
----
The following example extracts the body to a `Flux<Person>`, where `Person` objects are decoded from some
serialized form, such as JSON or XML:
[source,java]
The following example extracts the body to a `Flux<Person>` (or a `Flow<Person>` in Kotlin),
where `Person` objects are decoded from someserialized form, such as JSON or XML:
[source,java,role="primary"]
.Java
----
Flux<Person> people = request.bodyToFlux(Person.class);
----
[source,kotlin,role="secondary"]
.Kotlin
----
val people = request.bodyToFlow<Person>()
----
The preceding examples are shortcuts that use the more general `ServerRequest.body(BodyExtractor)`,
which accepts the `BodyExtractor` functional strategy interface. The utility class
`BodyExtractors` provides access to a number of instances. For example, the preceding examples can
also be written as follows:
[source,java]
[source,java,role="primary"]
.Java
----
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
----
[source,kotlin,role="secondary"]
.Kotlin
----
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitFirst()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
----
The following example shows how to access form data:
[source,java]
[source,java,role="primary"]
.Java
----
Mono<MultiValueMap<String, String> map = request.body(BodyExtractors.toFormData());
Mono<MultiValueMap<String, String> map = request.formData();
----
[source,kotlin,role="secondary"]
.Kotlin
----
val map = request.awaitFormData()
----
The following example shows how to access multipart data as a map:
[source,java]
[source,java,role="primary"]
.Java
----
Mono<MultiValueMap<String, Part> map = request.body(BodyExtractors.toMultipartData());
Mono<MultiValueMap<String, Part> map = request.multipartData();
----
[source,kotlin,role="secondary"]
.Kotlin
----
val map = request.awaitMultipartData()
----
The following example shows how to access multiparts, one at a time, in streaming fashion:
[source,java]
[source,java,role="primary"]
.Java
----
Flux<Part> parts = request.body(BodyExtractors.toParts());
----
[source,kotlin,role="secondary"]
.Kotlin
----
val parts = request.body(BodyExtractors.toParts()).asFlow()
----
@ -150,28 +222,48 @@ a `build` method to create it. You can use the builder to set the response statu
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
content:
[source,java]
[source,java,role="primary"]
.Java
----
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
----
[source,kotlin,role="secondary"]
.Kotlin
----
val person: Mono<Person> = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyWithType<Person>(person)
----
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
[source,java]
[source,java,role="primary"]
.Java
----
URI location = ...
ServerResponse.created(location).build();
----
[source,kotlin,role="secondary"]
.Kotlin
----
val location: URI = ...
ServerResponse.created(location).build()
----
Depending on the codec used, it is possible to pass hint parameters to customize how the
body is serialized or deserialized. For example, to specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON view]:
====
[source,java]
[source,java,role="primary"]
.Java
----
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
----
[source,kotlin,role="secondary"]
.Kotlin
----
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).bodyWithType(...)
----
====
@ -180,11 +272,16 @@ ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.clas
We can write a handler function as a lambda, as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body(fromObject("Hello World"));
request -> ServerResponse.ok().bodyValue("Hello World");
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
----
That is convenient, but in an application we need multiple functions, and multiple inline
@ -193,12 +290,11 @@ Therefore, it is useful to group related handler functions together into a handl
has a similar role as `@Controller` in an annotation-based application.
For example, the following class exposes a reactive `Person` repository:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.ServerResponse.ok;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
@ -221,7 +317,7 @@ public class PersonHandler {
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person)))
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
@ -237,6 +333,37 @@ when the `Person` has been saved).
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
found. If it is not found, we use `switchIfEmpty(Mono<T>)` to return a 404 Not Found response.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { // <1>
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { // <2>
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { // <3>
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
----
<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.
Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type.
<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.
[[webflux-fn-handler-validation]]
@ -246,34 +373,61 @@ A functional endpoint can use Spring's <<core.adoc#validation, validation facili
apply validation to the request body. For example, given a custom Spring
<<core.adoc#validation, Validator>> implementation for a `Person`:
====
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
public class PersonHandler {
public class PersonHandler {
private final Validator validator = new PersonValidator(); // <1>
private final Validator validator = new PersonValidator(); // <1>
// ...
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); <2>
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2>
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()); <3>
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); // <3>
}
}
}
----
<1> Create `Validator` instance.
<2> Apply validation.
<3> Raise exception for a 400 response.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() // <1>
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) // <2>
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()) // <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`.
@ -311,12 +465,21 @@ and so on.
The following example uses a request predicate to create a constraint based on the `Accept`
header:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> Response.ok().body(fromObject("Hello World")));
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World"));
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val route = coRouter {
(GET("/hello-world") and accept(MediaType.TEXT_PLAIN)).invoke {
ServerResponse.ok().bodyAndAwait("Hello World")
}
}
----
You can compose multiple request predicates together by using:
@ -353,8 +516,8 @@ There are also other ways to compose multiple router functions together:
The following example shows the composition of four routes:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@ -379,6 +542,29 @@ RouterFunction<ServerResponse> route = route()
`PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
(GET("/person/{id}") and accept(APPLICATION_JSON)).invoke(handler::getPerson) // <1>
(GET("/person") and accept(APPLICATION_JSON)).invoke(handler::listPeople) // <2>
POST("/person").invoke(handler::createPerson) // <3>
}.and(otherRoute) // <4>
----
<1> `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
@ -392,34 +578,58 @@ When using annotations, you would remove this duplication by using a type-level
In WebFlux.fn, path predicates can be shared through the `path` method on the router function builder.
For instance, the last few lines of the example above can be improved in the following way by using nested routes:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.path("/person", builder -> builder // <1>
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
----
<1> Note that second parameter of `path` is a consumer that takes the a router builder.
Note that second parameter of `path` is a consumer that takes the a router builder.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val route = coRouter {
"/person".nest {
(GET("/{id}") and accept(APPLICATION_JSON)).invoke(handler::getPerson)
(GET("") and accept(APPLICATION_JSON)).invoke(handler::listPeople)
POST("/person").invoke(handler::createPerson)
}
}
----
Though path-based nesting is the most common, you can nest on any kind of predicate by using
the `nest` method on the builder.
The above still contains some duplication in the form of the shared `Accept`-header predicate.
We can further improve by using the `nest` method together with `accept`:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.build();
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val route = coRouter {
"/person".nest{
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
----
@ -457,40 +667,72 @@ starter.
The following example shows a WebFlux Java configuration (see
<<web-reactive.adoc#webflux-dispatcher-handler, DispatcherHandler>> for how to run it):
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure message conversion...
}
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
----
@ -506,8 +748,8 @@ The filter will apply to all routes that are built by the builder.
This means that filters defined in nested routes do not apply to "top-level" routes.
For instance, consider the following example:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
@ -524,6 +766,12 @@ RouterFunction<ServerResponse> route = route()
<1> The `before` filter that adds a custom request header is only applied to the two GET routes.
<2> The `after` filter that logs the response is applied to all routes, including the nested ones.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
// TODO when https://github.com/spring-projects/spring-framework/issues/23526 will be fixed
----
The `filter` method on the router builder takes a `HandlerFilterFunction`: a
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`.
@ -535,27 +783,31 @@ Now we can add a simple security filter to our route, assuming that we have a `S
can determine whether a particular path is allowed.
The following example shows how to do so:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
SecurityManager securityManager = ...
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
// TODO when https://github.com/spring-projects/spring-framework/issues/23526 will be fixed
----
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.

View File

@ -47,8 +47,8 @@ integration for using Spring WebFlux with FreeMarker templates.
The following example shows how to configure FreeMarker as a view technology:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebFlux
@ -56,7 +56,7 @@ The following example shows how to configure FreeMarker as a view technology:
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freemarker();
registry.freeMarker();
}
// Configure FreeMarker...
@ -69,6 +69,25 @@ The following example shows how to configure FreeMarker as a view technology:
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates/freemarker")
}
}
----
Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`,
shown in the preceding example. Given the preceding configuration, if your controller
@ -87,8 +106,8 @@ properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property
a `java.util.Properties` object, and the `freemarkerVariables` property requires a
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebFlux
@ -108,6 +127,22 @@ a `java.util.Properties` object, and the `freemarkerVariables` property requires
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// ...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
}
}
----
See the FreeMarker documentation for details of settings and variables as they apply to
the `Configuration` object.
@ -210,8 +245,8 @@ You can declare a `ScriptTemplateConfigurer` bean to specify the script engine t
the script files to load, what function to call to render templates, and so on.
The following example uses Mustache templates and the Nashorn JavaScript engine:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebFlux
@ -233,6 +268,26 @@ The following example uses Mustache templates and the Nashorn JavaScript engine:
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
----
The `render` function is called with the following parameters:
@ -252,11 +307,11 @@ https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
browser facilities not available in the server-side script engine.
The following example shows how to set a custom render function:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
@EnableWebMvc
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
@ -275,6 +330,26 @@ The following example shows how to set a custom render function:
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
----
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
script engines with templating libraries not designed for concurrency, such as Handlebars or
@ -284,8 +359,7 @@ to https://bugs.openjdk.java.net/browse/JDK-8076099[this bug].
`polyfill.js` defines only the `window` object needed by Handlebars to run properly,
as the following snippet shows:
[source,javascript,indent=0]
[subs="verbatim,quotes"]
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var window = {};
----
@ -296,8 +370,7 @@ This can be done on the script side, as well as any customization you need (mana
template engine configuration for example).
The following example shows how compile a template:
[source,javascript,indent=0]
[subs="verbatim,quotes"]
[source,javascript,indent=0,subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);

View File

@ -38,8 +38,8 @@ You can also use `WebClient.builder()` with further options:
The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs>>:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
@ -51,12 +51,25 @@ The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs
.exchangeStrategies(strategies)
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val strategies = ExchangeStrategies.builder()
.codecs {
// ...
}
.build()
val client = WebClient.builder()
.exchangeStrategies(strategies)
.build()
----
Once built, a `WebClient` instance is immutable. However, you can clone it and build a
modified copy without affecting the original instance, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
@ -68,6 +81,19 @@ modified copy without affecting the original instance, as the following example
// client2 has filterA, filterB, filterC, filterD
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
----
@ -76,8 +102,8 @@ modified copy without affecting the original instance, as the following example
To customize Reactor Netty settings, simple provide a pre-configured `HttpClient`:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
@ -85,6 +111,15 @@ To customize Reactor Netty settings, simple provide a pre-configured `HttpClient
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
----
[[webflux-client-builder-reactor-resources]]
@ -102,26 +137,32 @@ application deployed as a WAR), you can declare a Spring-managed bean of type
Netty global resources are shut down when the Spring `ApplicationContext` is closed,
as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
----
You can also choose not to participate in the global Reactor Netty resources. However,
in this mode, the burden is on you to ensure that all Reactor Netty client and server
instances use shared resources, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setGlobalResources(false); <1>
factory.setUseGlobalResources(false); // <1>
return factory;
}
@ -133,9 +174,33 @@ instances use shared resources, as the following example shows:
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper); <2>
new ReactorClientHttpConnector(resourceFactory(), mapper); // <2>
return WebClient.builder().clientConnector(connector).build(); <3>
return WebClient.builder().clientConnector(connector).build(); // <3>
}
----
<1> Create resources independent of global ones.
<2> Use the `ReactorClientHttpConnector` constructor with resource factory.
<3> Plug the connector into the `WebClient.Builder`.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false // <1>
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Further customizations...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) // <2>
return WebClient.builder().clientConnector(connector).build() // <3>
}
----
<1> Create resources independent of global ones.
@ -148,29 +213,50 @@ instances use shared resources, as the following example shows:
To configure a connection timeout:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}
----
To configure a read and/or write timeout values:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create().tcpConfiguration {
it.doOnConnected { conn -> conn
.addHandlerLast(ReadTimeoutHandler(10))
.addHandlerLast(WriteTimeoutHandler(10))
}
}
----
@ -180,8 +266,8 @@ HttpClient httpClient = HttpClient.create()
The following example shows how to customize Jetty `HttpClient` settings:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
@ -189,6 +275,15 @@ The following example shows how to customize Jetty `HttpClient` settings:
WebClient webClient = WebClient.builder().clientConnector(connector).build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val httpClient = HttpClient()
httpClient.cookieStore = ...
val connector = JettyClientHttpConnector(httpClient)
val webClient = WebClient.builder().clientConnector(connector).build();
----
By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`),
which remain active until the process exits or `stop()` is called.
@ -198,8 +293,8 @@ ensure that the resources are shut down when the Spring `ApplicationContext` is
declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example
shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Bean
public JettyResourceFactory resourceFactory() {
@ -209,12 +304,11 @@ shows:
@Bean
public WebClient webClient() {
Consumer<HttpClient> customizer = client -> {
// Further customizations...
};
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(resourceFactory(), customizer); <1>
new JettyClientHttpConnector(httpClient, resourceFactory()); <1>
return WebClient.builder().clientConnector(connector).build(); <2>
}
@ -222,7 +316,25 @@ shows:
<1> Use the `JettyClientHttpConnector` constructor with resource factory.
<2> Plug the connector into the `WebClient.Builder`.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further customizations...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) // <1>
return WebClient.builder().clientConnector(connector).build() // <2>
}
----
<1> Use the `JettyClientHttpConnector` constructor with resource factory.
<2> Plug the connector into the `WebClient.Builder`.
[[webflux-client-retrieve]]
@ -231,8 +343,8 @@ shows:
The `retrieve()` method is the easiest way to get a response body and decode it.
The following example shows how to do so:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebClient client = WebClient.create("https://example.org");
@ -241,17 +353,35 @@ The following example shows how to do so:
.retrieve()
.bodyToMono(Person.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()
----
You can also get a stream of objects decoded from the response, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()
----
By default, responses with 4xx or 5xx status codes result in an
`WebClientResponseException` or one of its HTTP status specific sub-classes, such as
@ -259,8 +389,8 @@ By default, responses with 4xx or 5xx status codes result in an
You can also use the `onStatus` method to customize the resulting exception,
as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
@ -269,6 +399,16 @@ as the following example shows:
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()
----
When `onStatus` is used, if the response is expected to have content, then the `onStatus`
callback should consume it. If not, the content will be automatically drained to ensure
@ -283,25 +423,41 @@ resources are released.
The `exchange()` method provides more control than the `retrieve` method. The following example is equivalent
to `retrieve()` but also provides access to the `ClientResponse`:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.awaitBody<Person>()
----
At this level, you can also create a full `ResponseEntity`:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.toEntity<Person>()
----
Note that (unlike `retrieve()`), with `exchange()`, there are no automatic error signals for
4xx and 5xx responses. You have to check the status code and decide how to proceed.
@ -319,10 +475,10 @@ is closed and is not placed back in the pool.
== Request Body
The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`,
like `Mono` as the following example shows:
like `Mono` or Kotlin Coroutines `Deferred` as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Mono<Person> personMono = ... ;
@ -333,11 +489,23 @@ like `Mono` as the following example shows:
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyWithType<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
----
You can also have a stream of objects be encoded, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Flux<Person> personFlux = ... ;
@ -348,22 +516,46 @@ You can also have a stream of objects be encoded, as the following example shows
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val people: Flow<Person> = ...
Alternatively, if you have the actual value, you can use the `body` shortcut method,
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyWithType(people)
.retrieve()
.awaitBody<Unit>()
----
Alternatively, if you have the actual value, you can use the `bodyValue` shortcut method,
as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(person)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()
----
@ -374,22 +566,33 @@ To send form data, you can provide a `MultiValueMap<String, String>` as the body
content is automatically set to `application/x-www-form-urlencoded` by the
`FormHttpMessageWriter`. The following example shows how to use `MultiValueMap<String, String>`:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.body(formData)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()
----
You can also supply form data in-line by using `BodyInserters`, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.BodyInserters.*;
@ -399,6 +602,17 @@ You can also supply form data in-line by using `BodyInserters`, as the following
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()
----
@ -410,8 +624,8 @@ either `Object` instances that represent part content or `HttpEntity` instances
headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a
multipart request. The following example shows how to create a `MultiValueMap<String, ?>`:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
@ -421,6 +635,18 @@ multipart request. The following example shows how to create a `MultiValueMap<St
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val builder = MultipartBodyBuilder().apply {
part("fieldPart", "fieldValue")
part("filePart1", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
----
In most cases, you do not have to specify the `Content-Type` for each part. The content
type is determined automatically based on the `HttpMessageWriter` chosen to serialize it
@ -431,8 +657,8 @@ builder `part` methods.
Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is
through the `body` method, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
MultipartBodyBuilder builder = ...;
@ -442,6 +668,17 @@ through the `body` method, as the following example shows:
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()
----
If the `MultiValueMap` contains at least one non-`String` value, which could also
represent regular form data (that is, `application/x-www-form-urlencoded`), you need not
@ -451,8 +688,8 @@ set the `Content-Type` to `multipart/form-data`. This is always the case when us
As an alternative to `MultipartBodyBuilder`, you can also provide multipart content,
inline-style, through the built-in `BodyInserters`, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.BodyInserters.*;
@ -462,7 +699,17 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
.retrieve()
.bodyToMono(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()
----
@ -472,74 +719,115 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
in order to intercept and modify requests, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebClient client = WebClient.builder()
.filter((request, next) -> {
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
return next.exchange(filtered);
})
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = WebClient.builder()
.filter { request, next ->
val filtered = ClientRequest.from(request)
.header("foo", "bar")
.build()
next.exchange(filtered)
}
.build()
----
This can be used for cross-cutting concerns, such as authentication. The following example uses
a filter for basic authentication through a static factory method:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
// static import of ExchangeFilterFunctions.basicAuthentication
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()
----
Filters apply globally to every request. To change a filter's behavior for a specific
request, you can add request attributes to the `ClientRequest` that can then be accessed
by all filters in the chain, as the following example shows:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
})
.build();
}.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.awaitBody<Unit>()
----
You can also replicate an existing `WebClient`, insert new filters, or remove already
registered filters. The following example, inserts a basic authentication filter at
index 0:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
// static import of ExchangeFilterFunctions.basicAuthentication
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
----
@ -548,38 +836,69 @@ WebClient client = webClient.mutate()
`WebClient` can be used in synchronous style by blocking at the end for the result:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}
----
However if multiple calls need to be made, it's more efficient to avoid blocking on each
response individually, and instead wait for the combined result:
[source,java,intent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", personName);
map.put("hobbies", hobbies);
return map;
})
.block();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val data = runBlocking {
val personDeferred = async {
client.get().uri("/person/{id}", personId)
.retrieve().awaitBody<Person>()
}
val hobbiesDeferred = async {
client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
}
----
The above is merely one example. There are lots of other patterns and operators for putting
@ -588,8 +907,10 @@ inter-dependent, without ever blocking until the end.
[NOTE]
====
You should never have to block in a Spring MVC controller. Simply return the resulting
`Flux` or `Mono` from the controller method.
With `Flux` or `Mono`, you should never have to block in a Spring MVC or Spring WebFlux controller.
Simply return the resulting reactive type from the controller method. The same principle apply to
Kotlin Coroutines and Spring WebFlux, just use suspending function or return `Flow` in your
controller method .
====

View File

@ -26,8 +26,8 @@ server-side applications that handle WebSocket messages.
To create a WebSocket server, you can first create a `WebSocketHandler`.
The following example shows how to do so:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
@ -40,14 +40,27 @@ The following example shows how to do so:
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}
----
Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
static class WebConfig {
class WebConfig {
@Bean
public HandlerMapping handlerMapping() {
@ -64,6 +77,24 @@ Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the followi
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
----
@ -104,23 +135,45 @@ receives a cancellation signal.
The most basic implementation of a handler is one that handles the inbound stream. The
following example shows such an implementation:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
class ExampleHandler implements WebSocketHandler {
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() <1>
.doOnNext(message -> {
// ... <2>
})
.concatMap(message -> {
// ... <3>
})
.then(); <4>
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() // <1>
.doOnNext(message -> {
// ... // <2>
})
.concatMap(message -> {
// ... // <3>
})
.then(); // <4>
}
}
----
<1> Access the stream of inbound messages.
<2> Do something with each message.
<3> Perform nested asynchronous operations that use the message content.
<4> Return a `Mono<Void>` that completes when receiving completes.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive() // <1>
.doOnNext {
// ... // <2>
}
.concatMap {
// ... // <3>
}
.then() // <4>
}
}
}
----
<1> Access the stream of inbound messages.
<2> Do something with each message.
@ -135,26 +188,50 @@ released before you have had a chance to read the data. For more background, see
The following implementation combines the inbound and outbound streams:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
class ExampleHandler implements WebSocketHandler {
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
@Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive() <1>
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.map(value -> session.textMessage("Echo " + value)); <2>
Flux<WebSocketMessage> output = session.receive() // <1>
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.map(value -> session.textMessage("Echo " + value)); // <2>
return session.send(output); <3>
return session.send(output); // <3>
}
}
----
<1> Handle the inbound message stream.
<2> Create the outbound message, producing a combined flow.
<3> Return a `Mono<Void>` that does not complete while we continue to receive.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val output = session.receive() // <1>
.doOnNext {
// ...
}
.concatMap {
// ...
}
.map { session.textMessage("Echo $it") } // <2>
return session.send(output) // <3>
}
}
}
----
<1> Handle the inbound message stream.
<2> Create the outbound message, producing a combined flow.
@ -164,29 +241,56 @@ class ExampleHandler implements WebSocketHandler {
Inbound and outbound streams can be independent and be joined only for completion,
as the following example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
class ExampleHandler implements WebSocketHandler {
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
@Override
public Mono<Void> handle(WebSocketSession session) {
Mono<Void> input = session.receive() <1>
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.then();
Mono<Void> input = session.receive() <1>
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.then();
Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
return Mono.zip(input, output).then(); <3>
return Mono.zip(input, output).then(); <3>
}
}
----
<1> Handle inbound message stream.
<2> Send outgoing messages.
<3> Join the streams and return a `Mono<Void>` that completes when either stream ends.
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val input = session.receive() // <1>
.doOnNext {
// ...
}
.concatMap {
// ...
}
.then()
val source: Flux<String> = ...
val output = session.send(source.map(session::textMessage)) // <2>
return Mono.zip(input, output).then() // <3>
}
}
}
----
<1> Handle inbound message stream.
<2> Send outgoing messages.
@ -233,11 +337,11 @@ The `RequestUpgradeStrategy` for each server exposes WebSocket-related configura
options available for the underlying WebSocket engine. The following example sets
WebSocket options when running on Tomcat:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration
static class WebConfig {
class WebConfig {
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
@ -252,6 +356,25 @@ WebSocket options when running on Tomcat:
}
}
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@Configuration
class WebConfig {
@Bean
fun handlerAdapter() =
WebSocketHandlerAdapter(webSocketService())
@Bean
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
----
Check the upgrade strategy for your server to see what options are available. Currently,
only Tomcat and Jetty expose such options.
@ -284,16 +407,28 @@ API to suspend receiving messages for back pressure.
To start a WebSocket session, you can create an instance of the client and use its `execute`
methods:
[source,java,indent=0]
[subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
session.receive()
.doOnNext(::println)
.then()
}
----
Some clients, such as Jetty, implement `Lifecycle` and need to be stopped and started

File diff suppressed because it is too large Load Diff