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 annotation enables cross-origin requests on annotated controller methods, as the
following example shows: following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@RestController @RestController
@RequestMapping("/account") @RequestMapping("/account")
public class AccountController { public class AccountController {
@CrossOrigin @CrossOrigin
@GetMapping("/{id}") @GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) { public Mono<Account> retrieve(@PathVariable Long id) {
// ... // ...
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long 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: 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. `@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: The following example specifies a certain domain and sets `maxAge` to an hour:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) @CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController @RestController
@RequestMapping("/account") @RequestMapping("/account")
public class AccountController { public class AccountController {
@GetMapping("/{id}") @GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) { public Mono<Account> retrieve(@PathVariable Long id) {
// ... // ...
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long 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, You can use `@CrossOrigin` at both the class and the method level,
as the following example shows: as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@CrossOrigin(maxAge = 3600) <1> @CrossOrigin(maxAge = 3600) // <1>
@RestController @RestController
@RequestMapping("/account") @RequestMapping("/account")
public class AccountController { public class AccountController {
@CrossOrigin("https://domain2.com") <2> @CrossOrigin("https://domain2.com") // <2>
@GetMapping("/{id}") @GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) { public Mono<Account> retrieve(@PathVariable Long id) {
// ... // ...
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long 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. <1> Using `@CrossOrigin` at the class level.
<2> Using `@CrossOrigin` at the method 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, To enable CORS in the WebFlux Java configuration, you can use the `CorsRegistry` callback,
as the following example shows: as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebFlux @EnableWebFlux
public class WebConfig implements WebFluxConfigurer { public class WebConfig implements WebFluxConfigurer {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com") .allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE") .allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3") .allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2") .exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600); .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 To configure the filter, you can declare a `CorsWebFilter` bean and pass a
`CorsConfigurationSource` to its constructor, as the following example shows: `CorsConfigurationSource` to its constructor, as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim",role="primary"]
[subs="verbatim"] .Java
---- ----
@Bean @Bean
CorsWebFilter corsFilter() { CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration(); CorsConfiguration config = new CorsConfiguration();
// Possibly... // Possibly...
// config.applyPermitDefaultValues() // config.applyPermitDefaultValues()
config.setAllowCredentials(true); config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com"); config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*"); config.addAllowedHeader("*");
config.addAllowedMethod("*"); config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); 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, `RouterFunctions.route()` provides a router builder that facilitates the creation of routers,
as the following example shows: as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.http.MediaType.APPLICATION_JSON; 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.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route; import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ... PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository); PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route() RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson) .POST("/person", handler::createPerson)
.build(); .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 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>>: 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>`: 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); 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); 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)`, The preceding examples are shortcuts that use the more general `ServerRequest.body(BodyExtractor)`,
which accepts the `BodyExtractor` functional strategy interface. The utility class which accepts the `BodyExtractor` functional strategy interface. The utility class
`BodyExtractors` provides access to a number of instances. For example, the preceding examples can `BodyExtractors` provides access to a number of instances. For example, the preceding examples can
also be written as follows: also be written as follows:
[source,java] [source,java,role="primary"]
.Java
---- ----
Mono<String> string = request.body(BodyExtractors.toMono(String.class)); Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.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: 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: 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: 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()); 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 headers, or to provide a body. The following example creates a 200 (OK) response with JSON
content: content:
[source,java] [source,java,role="primary"]
.Java
---- ----
Mono<Person> person = ... Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); 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: 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 = ... URI location = ...
ServerResponse.created(location).build(); 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 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]: 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(...); 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: We can write a handler function as a lambda, as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
HandlerFunction<ServerResponse> helloWorld = 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 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. has a similar role as `@Controller` in an annotation-based application.
For example, the following class exposes a reactive `Person` repository: For example, the following class exposes a reactive `Person` repository:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.ServerResponse.ok; import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
public class PersonHandler { public class PersonHandler {
@ -221,7 +317,7 @@ public class PersonHandler {
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3> public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
int personId = Integer.valueOf(request.pathVariable("id")); int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId) 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()); .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 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. 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]] [[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 apply validation to the request body. For example, given a custom Spring
<<core.adoc#validation, Validator>> implementation for a `Person`: <<core.adoc#validation, Validator>> implementation for a `Person`:
==== [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[source,java,indent=0] .Java
[subs="verbatim,quotes"]
---- ----
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) { public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); <2> Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2>
return ok().build(repository.savePerson(person)); return ok().build(repository.savePerson(person));
} }
private void validate(Person person) { private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person"); Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors); validator.validate(person, errors);
if (errors.hasErrors) { if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); <3> 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. <1> Create `Validator` instance.
<2> Apply validation. <2> Apply validation.
<3> Raise exception for a 400 response. <3> Raise exception for a 400 response.
====
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
a global `Validator` instance based on `LocalValidatorFactoryBean`. 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` The following example uses a request predicate to create a constraint based on the `Accept`
header: header:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
RouterFunction<ServerResponse> route = RouterFunctions.route() RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN), .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> Response.ok().body(fromObject("Hello World"))); 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: 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: The following example shows the composition of four routes:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.http.MediaType.APPLICATION_JSON; 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.RequestPredicates.*;
@ -379,6 +542,29 @@ RouterFunction<ServerResponse> route = route()
`PersonHandler.createPerson`, and `PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. <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 === 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. 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: 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] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
RouterFunction<ServerResponse> route = route() RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder .path("/person", builder -> builder // <1>
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople) .GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)) .POST("/person", handler::createPerson))
.build(); .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 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 `nest` method on the builder.
The above still contains some duplication in the form of the shared `Accept`-header predicate. 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`: We can further improve by using the `nest` method together with `accept`:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
RouterFunction<ServerResponse> route = route() RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1 .path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2 .nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson) .GET("/{id}", handler::getPerson)
.GET("", handler::listPeople)) .GET("", handler::listPeople))
.POST("/person", handler::createPerson)) .POST("/person", handler::createPerson))
.build(); .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 The following example shows a WebFlux Java configuration (see
<<web-reactive.adoc#webflux-dispatcher-handler, DispatcherHandler>> for how to run it): <<web-reactive.adoc#webflux-dispatcher-handler, DispatcherHandler>> for how to run it):
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebFlux @EnableWebFlux
public class WebConfig implements WebFluxConfigurer { public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionA() {
// ... // ...
}
@Bean @Override
public RouterFunction<?> routerFunctionB() { 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 override fun addCorsMappings(registry: CorsRegistry) {
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { // configure CORS...
// configure message conversion... }
}
@Override override fun configureViewResolvers(registry: ViewResolverRegistry) {
public void addCorsMappings(CorsRegistry registry) { // configure view resolution for HTML rendering...
// configure CORS... }
} }
@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. This means that filters defined in nested routes do not apply to "top-level" routes.
For instance, consider the following example: For instance, consider the following example:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
RouterFunction<ServerResponse> route = route() RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1 .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. <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. <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 The `filter` method on the router builder takes a `HandlerFilterFunction`: a
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`. 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. can determine whether a particular path is allowed.
The following example shows how to do so: The following example shows how to do so:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
SecurityManager securityManager = ...
SecurityManager securityManager = ... RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
RouterFunction<ServerResponse> route = route() .nest(accept(APPLICATION_JSON), b2 -> b2
.path("/person", b1 -> b1 .GET("/{id}", handler::getPerson)
.nest(accept(APPLICATION_JSON), b2 -> b2 .GET("", handler::listPeople))
.GET("/{id}", handler::getPerson) .POST("/person", handler::createPerson))
.GET("", handler::listPeople)) .filter((request, next) -> {
.POST("/person", handler::createPerson)) if (securityManager.allowAccessTo(request.path())) {
.filter((request, next) -> { return next.handle(request);
if (securityManager.allowAccessTo(request.path())) { }
return next.handle(request); else {
} return ServerResponse.status(UNAUTHORIZED).build();
else { }
return ServerResponse.status(UNAUTHORIZED).build(); })
} .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. 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: The following example shows how to configure FreeMarker as a view technology:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebFlux @EnableWebFlux
@ -56,7 +56,7 @@ The following example shows how to configure FreeMarker as a view technology:
@Override @Override
public void configureViewResolvers(ViewResolverRegistry registry) { public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freemarker(); registry.freeMarker();
} }
// Configure 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`, 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 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 a `java.util.Properties` object, and the `freemarkerVariables` property requires a
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`: `java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebFlux @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 See the FreeMarker documentation for details of settings and variables as they apply to
the `Configuration` object. 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 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: The following example uses Mustache templates and the Nashorn JavaScript engine:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebFlux @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: 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. browser facilities not available in the server-side script engine.
The following example shows how to set a custom render function: The following example shows how to set a custom render function:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
@EnableWebMvc @EnableWebFlux
public class WebConfig implements WebFluxConfigurer { public class WebConfig implements WebFluxConfigurer {
@Override @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 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 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, `polyfill.js` defines only the `window` object needed by Handlebars to run properly,
as the following snippet shows: as the following snippet shows:
[source,javascript,indent=0] [source,javascript,indent=0,subs="verbatim,quotes"]
[subs="verbatim,quotes"]
---- ----
var window = {}; 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). template engine configuration for example).
The following example shows how compile a template: The following example shows how compile a template:
[source,javascript,indent=0] [source,javascript,indent=0,subs="verbatim,quotes"]
[subs="verbatim,quotes"]
---- ----
function render(template, model) { function render(template, model) {
var compiledTemplate = Handlebars.compile(template); 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>>: The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs>>:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
ExchangeStrategies strategies = ExchangeStrategies.builder() ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> { .codecs(configurer -> {
@ -51,12 +51,25 @@ The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs
.exchangeStrategies(strategies) .exchangeStrategies(strategies)
.build(); .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 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: modified copy without affecting the original instance, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
WebClient client1 = WebClient.builder() WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build(); .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 // 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`: To customize Reactor Netty settings, simple provide a pre-configured `HttpClient`:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); 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)) .clientConnector(new ReactorClientHttpConnector(httpClient))
.build(); .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]] [[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, Netty global resources are shut down when the Spring `ApplicationContext` is closed,
as the following example shows: as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Bean @Bean
public ReactorResourceFactory reactorResourceFactory() { public ReactorResourceFactory reactorResourceFactory() {
return new 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, 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 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: instances use shared resources, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Bean @Bean
public ReactorResourceFactory resourceFactory() { public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory(); ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setGlobalResources(false); <1> factory.setUseGlobalResources(false); // <1>
return factory; return factory;
} }
@ -133,9 +174,33 @@ instances use shared resources, as the following example shows:
}; };
ClientHttpConnector connector = 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. <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: To configure a connection timeout:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create() HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client -> .tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)); 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: To configure a read and/or write timeout values:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create() HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client -> .tcpConfiguration(client ->
client.doOnConnected(conn -> conn client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10)) .addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(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: The following example shows how to customize Jetty `HttpClient` settings:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
HttpClient httpClient = new HttpClient(); HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...); httpClient.setCookieStore(...);
@ -189,6 +275,15 @@ The following example shows how to customize Jetty `HttpClient` settings:
WebClient webClient = WebClient.builder().clientConnector(connector).build(); 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`), By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`),
which remain active until the process exits or `stop()` is called. 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 declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example
shows: shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Bean @Bean
public JettyResourceFactory resourceFactory() { public JettyResourceFactory resourceFactory() {
@ -209,12 +304,11 @@ shows:
@Bean @Bean
public WebClient webClient() { public WebClient webClient() {
Consumer<HttpClient> customizer = client -> { HttpClient httpClient = new HttpClient();
// Further customizations... // Further customizations...
};
ClientHttpConnector connector = ClientHttpConnector connector =
new JettyClientHttpConnector(resourceFactory(), customizer); <1> new JettyClientHttpConnector(httpClient, resourceFactory()); <1>
return WebClient.builder().clientConnector(connector).build(); <2> return WebClient.builder().clientConnector(connector).build(); <2>
} }
@ -222,7 +316,25 @@ shows:
<1> Use the `JettyClientHttpConnector` constructor with resource factory. <1> Use the `JettyClientHttpConnector` constructor with resource factory.
<2> Plug the connector into the `WebClient.Builder`. <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]] [[webflux-client-retrieve]]
@ -231,8 +343,8 @@ shows:
The `retrieve()` method is the easiest way to get a response body and decode it. The `retrieve()` method is the easiest way to get a response body and decode it.
The following example shows how to do so: The following example shows how to do so:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
WebClient client = WebClient.create("https://example.org"); WebClient client = WebClient.create("https://example.org");
@ -241,17 +353,35 @@ The following example shows how to do so:
.retrieve() .retrieve()
.bodyToMono(Person.class); .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: You can also get a stream of objects decoded from the response, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Flux<Quote> result = client.get() Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve() .retrieve()
.bodyToFlux(Quote.class); .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 By default, responses with 4xx or 5xx status codes result in an
`WebClientResponseException` or one of its HTTP status specific sub-classes, such as `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, You can also use the `onStatus` method to customize the resulting exception,
as the following example shows: as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Mono<Person> result = client.get() Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
@ -269,6 +399,16 @@ as the following example shows:
.onStatus(HttpStatus::is5xxServerError, response -> ...) .onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class); .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` 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 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 The `exchange()` method provides more control than the `retrieve` method. The following example is equivalent
to `retrieve()` but also provides access to the `ClientResponse`: to `retrieve()` but also provides access to the `ClientResponse`:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Mono<Person> result = client.get() Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange() .exchange()
.flatMap(response -> response.bodyToMono(Person.class)); .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`: At this level, you can also create a full `ResponseEntity`:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Mono<ResponseEntity<Person>> result = client.get() Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange() .exchange()
.flatMap(response -> response.toEntity(Person.class)); .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 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. 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 == Request Body
The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, 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] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Mono<Person> personMono = ... ; Mono<Person> personMono = ... ;
@ -333,11 +489,23 @@ like `Mono` as the following example shows:
.retrieve() .retrieve()
.bodyToMono(Void.class); .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: You can also have a stream of objects be encoded, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Flux<Person> personFlux = ... ; Flux<Person> personFlux = ... ;
@ -348,22 +516,46 @@ You can also have a stream of objects be encoded, as the following example shows
.retrieve() .retrieve()
.bodyToMono(Void.class); .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: as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Person person = ... ; Person person = ... ;
Mono<Void> result = client.post() Mono<Void> result = client.post()
.uri("/persons/{id}", id) .uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(person) .bodyValue(person)
.retrieve() .retrieve()
.bodyToMono(Void.class); .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 content is automatically set to `application/x-www-form-urlencoded` by the
`FormHttpMessageWriter`. The following example shows how to use `MultiValueMap<String, String>`: `FormHttpMessageWriter`. The following example shows how to use `MultiValueMap<String, String>`:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
MultiValueMap<String, String> formData = ... ; MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post() Mono<Void> result = client.post()
.uri("/path", id) .uri("/path", id)
.body(formData) .bodyValue(formData)
.retrieve() .retrieve()
.bodyToMono(Void.class); .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: You can also supply form data in-line by using `BodyInserters`, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.web.reactive.function.BodyInserters.*; 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() .retrieve()
.bodyToMono(Void.class); .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 headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a
multipart request. The following example shows how to create a `MultiValueMap<String, ?>`: multipart request. The following example shows how to create a `MultiValueMap<String, ?>`:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
MultipartBodyBuilder builder = new MultipartBodyBuilder(); MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue"); 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(); 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 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 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 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: through the `body` method, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
MultipartBodyBuilder builder = ...; MultipartBodyBuilder builder = ...;
@ -442,6 +668,17 @@ through the `body` method, as the following example shows:
.retrieve() .retrieve()
.bodyToMono(Void.class); .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 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 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, As an alternative to `MultipartBodyBuilder`, you can also provide multipart content,
inline-style, through the built-in `BodyInserters`, as the following example shows: inline-style, through the built-in `BodyInserters`, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.web.reactive.function.BodyInserters.*; 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() .retrieve()
.bodyToMono(Void.class); .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` You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
in order to intercept and modify requests, as the following example shows: in order to intercept and modify requests, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
WebClient client = WebClient.builder() WebClient client = WebClient.builder()
.filter((request, next) -> { .filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request) ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar") .header("foo", "bar")
.build(); .build();
return next.exchange(filtered); return next.exchange(filtered);
}) })
.build(); .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 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: a filter for basic authentication through a static factory method:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .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() val client = WebClient.builder()
.filter(basicAuthentication("user", "password")) .filter(basicAuthentication("user", "password"))
.build(); .build()
---- ----
Filters apply globally to every request. To change a filter's behavior for a specific 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 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: by all filters in the chain, as the following example shows:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
WebClient client = WebClient.builder() WebClient client = WebClient.builder()
.filter((request, next) -> { .filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute"); 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/") client.get().uri("https://example.org/")
.attribute("myAttribute", "...") .attribute("myAttribute", "...")
.retrieve() .retrieve()
.bodyToMono(Void.class); .awaitBody<Unit>()
}
---- ----
You can also replicate an existing `WebClient`, insert new filters, or remove already You can also replicate an existing `WebClient`, insert new filters, or remove already
registered filters. The following example, inserts a basic authentication filter at registered filters. The following example, inserts a basic authentication filter at
index 0: index 0:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
// static import of ExchangeFilterFunctions.basicAuthentication WebClient client = webClient.mutate()
.filters(filterList -> {
WebClient client = webClient.mutate() filterList.add(0, basicAuthentication("user", "password"));
.filters(filterList -> { })
filterList.add(0, basicAuthentication("user", "password")); .build();
}) ----
.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: `WebClient` can be used in synchronous style by blocking at the end for the result:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Person person = client.get().uri("/person/{id}", i).retrieve() Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class) .bodyToMono(Person.class)
.block(); .block();
List<Person> persons = client.get().uri("/persons").retrieve() List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class) .bodyToFlux(Person.class)
.collectList() .collectList()
.block(); .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 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: response individually, and instead wait for the combined result:
[source,java,intent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
Mono<Person> personMono = client.get().uri("/person/{id}", personId) Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class); .retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList(); .retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>(); Map<String, String> map = new LinkedHashMap<>();
map.put("person", personName); map.put("person", person);
map.put("hobbies", hobbies); map.put("hobbies", hobbies);
return map; return map;
}) })
.block(); .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 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] [NOTE]
==== ====
You should never have to block in a Spring MVC controller. Simply return the resulting With `Flux` or `Mono`, you should never have to block in a Spring MVC or Spring WebFlux controller.
`Flux` or `Mono` from the controller method. 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`. To create a WebSocket server, you can first create a `WebSocketHandler`.
The following example shows how to do so: The following example shows how to do so:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession; 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: Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
static class WebConfig { class WebConfig {
@Bean @Bean
public HandlerMapping handlerMapping() { 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 The most basic implementation of a handler is one that handles the inbound stream. The
following example shows such an implementation: following example shows such an implementation:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
class ExampleHandler implements WebSocketHandler { class ExampleHandler implements WebSocketHandler {
@Override @Override
public Mono<Void> handle(WebSocketSession session) { public Mono<Void> handle(WebSocketSession session) {
return session.receive() <1> return session.receive() // <1>
.doOnNext(message -> { .doOnNext(message -> {
// ... <2> // ... // <2>
}) })
.concatMap(message -> { .concatMap(message -> {
// ... <3> // ... // <3>
}) })
.then(); <4> .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. <1> Access the stream of inbound messages.
<2> Do something with each message. <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: The following implementation combines the inbound and outbound streams:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
class ExampleHandler implements WebSocketHandler { class ExampleHandler implements WebSocketHandler {
@Override @Override
public Mono<Void> handle(WebSocketSession session) { public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive() <1> Flux<WebSocketMessage> output = session.receive() // <1>
.doOnNext(message -> { .doOnNext(message -> {
// ... // ...
}) })
.concatMap(message -> { .concatMap(message -> {
// ... // ...
}) })
.map(value -> session.textMessage("Echo " + value)); <2> .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. <1> Handle the inbound message stream.
<2> Create the outbound message, producing a combined flow. <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, Inbound and outbound streams can be independent and be joined only for completion,
as the following example shows: as the following example shows:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
class ExampleHandler implements WebSocketHandler { class ExampleHandler implements WebSocketHandler {
@Override @Override
public Mono<Void> handle(WebSocketSession session) { public Mono<Void> handle(WebSocketSession session) {
Mono<Void> input = session.receive() <1> Mono<Void> input = session.receive() <1>
.doOnNext(message -> { .doOnNext(message -> {
// ... // ...
}) })
.concatMap(message -> { .concatMap(message -> {
// ... // ...
}) })
.then(); .then();
Flux<String> source = ... ; Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage)); <2> 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. <1> Handle inbound message stream.
<2> Send outgoing messages. <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 options available for the underlying WebSocket engine. The following example sets
WebSocket options when running on Tomcat: WebSocket options when running on Tomcat:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
@Configuration @Configuration
static class WebConfig { class WebConfig {
@Bean @Bean
public WebSocketHandlerAdapter handlerAdapter() { 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, Check the upgrade strategy for your server to see what options are available. Currently,
only Tomcat and Jetty expose such options. 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` To start a WebSocket session, you can create an instance of the client and use its `execute`
methods: methods:
[source,java,indent=0] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
[subs="verbatim,quotes"] .Java
---- ----
WebSocketClient client = new ReactorNettyWebSocketClient(); WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path"); URI url = new URI("ws://localhost:8080/path");
client.execute(url, session -> client.execute(url, session ->
session.receive() session.receive()
.doOnNext(System.out::println) .doOnNext(System.out::println)
.then()); .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 Some clients, such as Jetty, implement `Lifecycle` and need to be stopped and started

File diff suppressed because it is too large Load Diff