parent
5b4ad8bf36
commit
14558844bc
|
@ -83,12 +83,12 @@ 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}")
|
||||
|
@ -100,7 +100,26 @@ public class AccountController {
|
|||
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,13 +138,13 @@ 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) {
|
||||
|
@ -136,21 +155,40 @@ public class AccountController {
|
|||
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>
|
||||
@CrossOrigin("https://domain2.com") // <2>
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
|
@ -160,7 +198,30 @@ public class AccountController {
|
|||
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,12 +252,12 @@ 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) {
|
||||
|
@ -210,7 +271,27 @@ public class WebConfig implements WebFluxConfigurer {
|
|||
|
||||
// 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,11 +313,11 @@ 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();
|
||||
|
||||
|
@ -252,5 +333,27 @@ CorsWebFilter corsFilter() {
|
|||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
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)
|
||||
}
|
||||
----
|
||||
|
|
|
@ -28,24 +28,24 @@ 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()
|
||||
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 {
|
||||
|
||||
// ...
|
||||
|
||||
|
@ -60,9 +60,43 @@ public class PersonHandler {
|
|||
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>
|
||||
|
||||
// ...
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private void validate(Person person) {
|
||||
Errors errors = new BeanPropertyBindingResult(person, "person");
|
||||
validator.validate(person, errors);
|
||||
if (errors.hasErrors) {
|
||||
throw new ServerWebInputException(errors.toString()); <3>
|
||||
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()
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.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:
|
||||
|
@ -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,28 +578,39 @@ 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()
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
|
@ -421,6 +618,19 @@ RouterFunction<ServerResponse> route = route()
|
|||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webflux-fn-running]]
|
||||
|
@ -457,12 +667,12 @@ 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() {
|
||||
|
@ -490,7 +700,39 @@ public class WebConfig implements WebFluxConfigurer {
|
|||
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 fun addCorsMappings(registry: CorsRegistry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
// 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,13 +783,12 @@ 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()
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
|
@ -557,6 +804,11 @@ RouterFunction<ServerResponse> route = route()
|
|||
})
|
||||
.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.
|
||||
We allow only the handler function to be executed when access is allowed.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,30 +213,51 @@ 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()
|
||||
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()
|
||||
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 -> {
|
||||
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,10 +719,10 @@ 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()
|
||||
WebClient client = WebClient.builder()
|
||||
.filter((request, next) -> {
|
||||
|
||||
ClientRequest filtered = ClientRequest.from(request)
|
||||
|
@ -486,60 +733,101 @@ WebClient client = WebClient.builder()
|
|||
})
|
||||
.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()
|
||||
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
|
||||
|
||||
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()
|
||||
WebClient client = WebClient.builder()
|
||||
.filter((request, next) -> {
|
||||
Optional<Object> usr = request.attribute("myAttribute");
|
||||
// ...
|
||||
})
|
||||
.build();
|
||||
|
||||
client.get().uri("https://example.org/")
|
||||
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()
|
||||
|
||||
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()
|
||||
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,39 +836,70 @@ 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()
|
||||
Person person = client.get().uri("/person/{id}", i).retrieve()
|
||||
.bodyToMono(Person.class)
|
||||
.block();
|
||||
|
||||
List<Person> persons = client.get().uri("/persons").retrieve()
|
||||
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)
|
||||
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
|
||||
.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();
|
||||
|
||||
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.put("person", personName);
|
||||
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
|
||||
together a reactive pipeline that makes many remote calls, potentially some nested,
|
||||
|
@ -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 .
|
||||
====
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
return session.receive() // <1>
|
||||
.doOnNext(message -> {
|
||||
// ... <2>
|
||||
// ... // <2>
|
||||
})
|
||||
.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.
|
||||
<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) {
|
||||
|
||||
Flux<WebSocketMessage> output = session.receive() <1>
|
||||
Flux<WebSocketMessage> output = session.receive() // <1>
|
||||
.doOnNext(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.
|
||||
<2> Create the outbound message, producing a combined flow.
|
||||
|
@ -164,10 +241,10 @@ 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) {
|
||||
|
@ -186,7 +263,34 @@ class ExampleHandler implements WebSocketHandler {
|
|||
|
||||
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,17 +407,29 @@ 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 ->
|
||||
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
|
||||
before you can use them. All clients have constructor options related to configuration
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue