1224 lines
35 KiB
Plaintext
1224 lines
35 KiB
Plaintext
[[webflux-client]]
|
|
= WebClient
|
|
|
|
Spring WebFlux includes a client to perform HTTP requests with. `WebClient` has a
|
|
functional, fluent API based on Reactor, see <<web-reactive.adoc#webflux-reactive-libraries>>,
|
|
which enables declarative composition of asynchronous logic without the need to deal with
|
|
threads or concurrency. It is fully non-blocking, it supports streaming, and relies on
|
|
the same <<web-reactive.adoc#webflux-codecs, codecs>> that are also used to encode and
|
|
decode request and response content on the server side.
|
|
|
|
`WebClient` needs an HTTP client library to perform requests with. There is built-in
|
|
support for the following:
|
|
|
|
* https://github.com/reactor/reactor-netty[Reactor Netty]
|
|
* https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html[JDK HttpClient]
|
|
* https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient]
|
|
* https://hc.apache.org/index.html[Apache HttpComponents]
|
|
* Others can be plugged via `ClientHttpConnector`.
|
|
|
|
|
|
|
|
|
|
[[webflux-client-builder]]
|
|
== Configuration
|
|
|
|
The simplest way to create a `WebClient` is through one of the static factory methods:
|
|
|
|
* `WebClient.create()`
|
|
* `WebClient.create(String baseUrl)`
|
|
|
|
You can also use `WebClient.builder()` with further options:
|
|
|
|
* `uriBuilderFactory`: Customized `UriBuilderFactory` to use as a base URL.
|
|
* `defaultUriVariables`: default values to use when expanding URI templates.
|
|
* `defaultHeader`: Headers for every request.
|
|
* `defaultCookie`: Cookies for every request.
|
|
* `defaultRequest`: `Consumer` to customize every request.
|
|
* `filter`: Client filter for every request.
|
|
* `exchangeStrategies`: HTTP message reader/writer customizations.
|
|
* `clientConnector`: HTTP client library settings.
|
|
* `observationRegistry`: the registry to use for enabling <<integration.adoc#integration.observability.http-client.webclient, Observability support>>.
|
|
* `observationConvention`: <<integration.adoc#integration.observability.config,an optional, custom convention to extract metadata>> for recorded observations.
|
|
|
|
For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.builder()
|
|
.codecs(configurer -> ... )
|
|
.build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val webClient = WebClient.builder()
|
|
.codecs { configurer -> ... }
|
|
.build()
|
|
----
|
|
|
|
Once built, a `WebClient` is immutable. However, you can clone it and build a
|
|
modified copy as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client1 = WebClient.builder()
|
|
.filter(filterA).filter(filterB).build();
|
|
|
|
WebClient client2 = client1.mutate()
|
|
.filter(filterC).filter(filterD).build();
|
|
|
|
// client1 has filterA, filterB
|
|
|
|
// 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
|
|
----
|
|
|
|
[[webflux-client-builder-maxinmemorysize]]
|
|
=== MaxInMemorySize
|
|
|
|
Codecs have <<web-reactive.adoc#webflux-codecs-limits,limits>> for buffering data in
|
|
memory to avoid application memory issues. By default those are set to 256KB.
|
|
If that's not enough you'll get the following error:
|
|
|
|
----
|
|
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
|
|
----
|
|
|
|
To change the limit for default codecs, use the following:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient webClient = WebClient.builder()
|
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
|
|
.build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val webClient = WebClient.builder()
|
|
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
|
|
.build()
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-builder-reactor]]
|
|
=== Reactor Netty
|
|
|
|
To customize Reactor Netty settings, provide a pre-configured `HttpClient`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
|
|
|
|
WebClient webClient = WebClient.builder()
|
|
.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]]
|
|
==== Resources
|
|
|
|
By default, `HttpClient` participates in the global Reactor Netty resources held in
|
|
`reactor.netty.http.HttpResources`, including event loop threads and a connection pool.
|
|
This is the recommended mode, since fixed, shared resources are preferred for event loop
|
|
concurrency. In this mode global resources remain active until the process exits.
|
|
|
|
If the server is timed with the process, there is typically no need for an explicit
|
|
shutdown. However, if the server can start or stop in-process (for example, a Spring MVC
|
|
application deployed as a WAR), you can declare a Spring-managed bean of type
|
|
`ReactorResourceFactory` with `globalResources=true` (the default) to ensure that the Reactor
|
|
Netty global resources are shut down when the Spring `ApplicationContext` is closed,
|
|
as the following example shows:
|
|
|
|
--
|
|
[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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
@Bean
|
|
public ReactorResourceFactory resourceFactory() {
|
|
ReactorResourceFactory factory = new ReactorResourceFactory();
|
|
factory.setUseGlobalResources(false); // <1>
|
|
return factory;
|
|
}
|
|
|
|
@Bean
|
|
public WebClient webClient() {
|
|
|
|
Function<HttpClient, HttpClient> mapper = client -> {
|
|
// Further customizations...
|
|
};
|
|
|
|
ClientHttpConnector connector =
|
|
new ReactorClientHttpConnector(resourceFactory(), mapper); // <2>
|
|
|
|
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.
|
|
<2> Use the `ReactorClientHttpConnector` constructor with resource factory.
|
|
<3> Plug the connector into the `WebClient.Builder`.
|
|
--
|
|
|
|
|
|
[[webflux-client-builder-reactor-timeout]]
|
|
==== Timeouts
|
|
|
|
To configure a connection timeout:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import io.netty.channel.ChannelOption;
|
|
|
|
HttpClient httpClient = HttpClient.create()
|
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
|
|
|
|
WebClient webClient = WebClient.builder()
|
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
|
.build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import io.netty.channel.ChannelOption
|
|
|
|
val httpClient = HttpClient.create()
|
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
|
|
|
|
val webClient = WebClient.builder()
|
|
.clientConnector(ReactorClientHttpConnector(httpClient))
|
|
.build();
|
|
----
|
|
|
|
To configure a read or write timeout:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
|
import io.netty.handler.timeout.WriteTimeoutHandler;
|
|
|
|
HttpClient httpClient = HttpClient.create()
|
|
.doOnConnected(conn -> conn
|
|
.addHandlerLast(new ReadTimeoutHandler(10))
|
|
.addHandlerLast(new WriteTimeoutHandler(10)));
|
|
|
|
// Create WebClient...
|
|
|
|
----
|
|
[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()
|
|
.doOnConnected { conn -> conn
|
|
.addHandlerLast(ReadTimeoutHandler(10))
|
|
.addHandlerLast(WriteTimeoutHandler(10))
|
|
}
|
|
|
|
// Create WebClient...
|
|
----
|
|
|
|
To configure a response timeout for all requests:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HttpClient httpClient = HttpClient.create()
|
|
.responseTimeout(Duration.ofSeconds(2));
|
|
|
|
// Create WebClient...
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val httpClient = HttpClient.create()
|
|
.responseTimeout(Duration.ofSeconds(2));
|
|
|
|
// Create WebClient...
|
|
----
|
|
|
|
To configure a response timeout for a specific request:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient.create().get()
|
|
.uri("https://example.org/path")
|
|
.httpRequest(httpRequest -> {
|
|
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
|
|
reactorRequest.responseTimeout(Duration.ofSeconds(2));
|
|
})
|
|
.retrieve()
|
|
.bodyToMono(String.class);
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
WebClient.create().get()
|
|
.uri("https://example.org/path")
|
|
.httpRequest { httpRequest: ClientHttpRequest ->
|
|
val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
|
|
reactorRequest.responseTimeout(Duration.ofSeconds(2))
|
|
}
|
|
.retrieve()
|
|
.bodyToMono(String::class.java)
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-builder-jdk-httpclient]]
|
|
=== JDK HttpClient
|
|
|
|
The following example shows how to customize the JDK `HttpClient`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HttpClient httpClient = HttpClient.newBuilder()
|
|
.followRedirects(Redirect.NORMAL)
|
|
.connectTimeout(Duration.ofSeconds(20))
|
|
.build();
|
|
|
|
ClientHttpConnector connector =
|
|
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
|
|
|
|
WebClient webClient = WebClient.builder().clientConnector(connector).build();
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val httpClient = HttpClient.newBuilder()
|
|
.followRedirects(Redirect.NORMAL)
|
|
.connectTimeout(Duration.ofSeconds(20))
|
|
.build()
|
|
|
|
val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory())
|
|
|
|
val webClient = WebClient.builder().clientConnector(connector).build()
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-builder-jetty]]
|
|
=== Jetty
|
|
|
|
The following example shows how to customize Jetty `HttpClient` settings:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HttpClient httpClient = new HttpClient();
|
|
httpClient.setCookieStore(...);
|
|
|
|
WebClient webClient = WebClient.builder()
|
|
.clientConnector(new JettyClientHttpConnector(httpClient))
|
|
.build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val httpClient = HttpClient()
|
|
httpClient.cookieStore = ...
|
|
|
|
val webClient = WebClient.builder()
|
|
.clientConnector(JettyClientHttpConnector(httpClient))
|
|
.build();
|
|
----
|
|
--
|
|
|
|
By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`),
|
|
which remain active until the process exits or `stop()` is called.
|
|
|
|
You can share resources between multiple instances of the Jetty client (and server) and
|
|
ensure that the resources are shut down when the Spring `ApplicationContext` is closed by
|
|
declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example
|
|
shows:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
@Bean
|
|
public JettyResourceFactory resourceFactory() {
|
|
return new JettyResourceFactory();
|
|
}
|
|
|
|
@Bean
|
|
public WebClient webClient() {
|
|
|
|
HttpClient httpClient = new HttpClient();
|
|
// Further customizations...
|
|
|
|
ClientHttpConnector connector =
|
|
new 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`.
|
|
|
|
[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-builder-http-components]]
|
|
=== HttpComponents
|
|
|
|
The following example shows how to customize Apache HttpComponents `HttpClient` settings:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
|
|
clientBuilder.setDefaultRequestConfig(...);
|
|
CloseableHttpAsyncClient client = clientBuilder.build();
|
|
|
|
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
|
|
|
|
WebClient webClient = WebClient.builder().clientConnector(connector).build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val client = HttpAsyncClients.custom().apply {
|
|
setDefaultRequestConfig(...)
|
|
}.build()
|
|
val connector = HttpComponentsClientHttpConnector(client)
|
|
val webClient = WebClient.builder().clientConnector(connector).build()
|
|
----
|
|
|
|
|
|
[[webflux-client-retrieve]]
|
|
== `retrieve()`
|
|
|
|
The `retrieve()` method can be used to declare how to extract the response. For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.create("https://example.org");
|
|
|
|
Mono<ResponseEntity<Person>> result = client.get()
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
.retrieve()
|
|
.toEntity(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()
|
|
.toEntity<Person>().awaitSingle()
|
|
----
|
|
|
|
Or to get only the body:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.create("https://example.org");
|
|
|
|
Mono<Person> result = client.get()
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
.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>()
|
|
----
|
|
|
|
To get a stream of decoded objects:
|
|
|
|
[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, 4xx or 5xx responses result in an `WebClientResponseException`, including
|
|
sub-classes for specific HTTP status codes. To customize the handling of error
|
|
responses, use `onStatus` handlers as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Mono<Person> result = client.get()
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
.retrieve()
|
|
.onStatus(HttpStatus::is4xxClientError, response -> ...)
|
|
.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>()
|
|
----
|
|
|
|
|
|
|
|
|
|
[[webflux-client-exchange]]
|
|
== Exchange
|
|
|
|
The `exchangeToMono()` and `exchangeToFlux()` methods (or `awaitExchange { }` and `exchangeToFlow { }` in Kotlin)
|
|
are useful for more advanced cases that require more control, such as to decode the response differently
|
|
depending on the response status:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Mono<Person> entityMono = client.get()
|
|
.uri("/persons/1")
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
.exchangeToMono(response -> {
|
|
if (response.statusCode().equals(HttpStatus.OK)) {
|
|
return response.bodyToMono(Person.class);
|
|
}
|
|
else {
|
|
// Turn to error
|
|
return response.createError();
|
|
}
|
|
});
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val entity = client.get()
|
|
.uri("/persons/1")
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
.awaitExchange {
|
|
if (response.statusCode() == HttpStatus.OK) {
|
|
return response.awaitBody<Person>()
|
|
}
|
|
else {
|
|
throw response.createExceptionAndAwait()
|
|
}
|
|
}
|
|
----
|
|
|
|
When using the above, after the returned `Mono` or `Flux` completes, the response body
|
|
is checked and if not consumed it is released to prevent memory and connection leaks.
|
|
Therefore the response cannot be decoded further downstream. It is up to the provided
|
|
function to declare how to decode the response if needed.
|
|
|
|
|
|
|
|
|
|
[[webflux-client-body]]
|
|
== Request Body
|
|
|
|
The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`,
|
|
like `Mono` or Kotlin Coroutines `Deferred` as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Mono<Person> personMono = ... ;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/persons/{id}", id)
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.body(personMono, Person.class)
|
|
.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)
|
|
.body<Person>(personDeferred)
|
|
.retrieve()
|
|
.awaitBody<Unit>()
|
|
----
|
|
|
|
You can also have a stream of objects be encoded, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Flux<Person> personFlux = ... ;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/persons/{id}", id)
|
|
.contentType(MediaType.APPLICATION_STREAM_JSON)
|
|
.body(personFlux, Person.class)
|
|
.retrieve()
|
|
.bodyToMono(Void.class);
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val people: Flow<Person> = ...
|
|
|
|
client.post()
|
|
.uri("/persons/{id}", id)
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.body(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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Person person = ... ;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/persons/{id}", id)
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.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>()
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-body-form]]
|
|
=== Form Data
|
|
|
|
To send form data, you can provide a `MultiValueMap<String, String>` as the body. Note that the
|
|
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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
MultiValueMap<String, String> formData = ... ;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/path", id)
|
|
.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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import static org.springframework.web.reactive.function.BodyInserters.*;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/path", id)
|
|
.body(fromFormData("k1", "v1").with("k2", "v2"))
|
|
.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>()
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-body-multipart]]
|
|
=== Multipart Data
|
|
|
|
To send multipart data, you need to provide a `MultiValueMap<String, ?>` whose values are
|
|
either `Object` instances that represent part content or `HttpEntity` instances that represent the content and
|
|
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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
|
builder.part("fieldPart", "fieldValue");
|
|
builder.part("filePart1", new FileSystemResource("...logo.png"));
|
|
builder.part("jsonPart", new Person("Jason"));
|
|
builder.part("myPart", part); // Part from a server request
|
|
|
|
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", FileSystemResource("...logo.png"))
|
|
part("jsonPart", 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
|
|
or, in the case of a `Resource`, based on the file extension. If necessary, you can
|
|
explicitly provide the `MediaType` to use for each part through one of the overloaded
|
|
builder `part` methods.
|
|
|
|
Once a `MultiValueMap` is prepared, the easiest way to pass it to the `WebClient` is
|
|
through the `body` method, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
MultipartBodyBuilder builder = ...;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/path", id)
|
|
.body(builder.build())
|
|
.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
|
|
set the `Content-Type` to `multipart/form-data`. This is always the case when using
|
|
`MultipartBodyBuilder`, which ensures an `HttpEntity` wrapper.
|
|
|
|
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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import static org.springframework.web.reactive.function.BodyInserters.*;
|
|
|
|
Mono<Void> result = client.post()
|
|
.uri("/path", id)
|
|
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
|
|
.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>()
|
|
----
|
|
|
|
==== `PartEvent`
|
|
|
|
To stream multipart data sequentially, you can provide multipart content through `PartEvent`
|
|
objects.
|
|
|
|
- Form fields can be created via `FormPartEvent::create`.
|
|
- File uploads can be created via `FilePartEvent::create`.
|
|
|
|
You can concatenate the streams returned from methods via `Flux::concat`, and create a request for
|
|
the `WebClient`.
|
|
|
|
For instance, this sample will POST a multipart form containing a form field and a file.
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Resource resource = ...
|
|
Mono<String> result = webClient
|
|
.post()
|
|
.uri("https://example.com")
|
|
.body(Flux.concat(
|
|
FormPartEvent.create("field", "field value"),
|
|
FilePartEvent.create("file", resource)
|
|
), PartEvent.class)
|
|
.retrieve()
|
|
.bodyToMono(String.class);
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
var resource: Resource = ...
|
|
var result: Mono<String> = webClient
|
|
.post()
|
|
.uri("https://example.com")
|
|
.body(
|
|
Flux.concat(
|
|
FormPartEvent.create("field", "field value"),
|
|
FilePartEvent.create("file", resource)
|
|
)
|
|
)
|
|
.retrieve()
|
|
.bodyToMono()
|
|
----
|
|
|
|
On the server side, `PartEvent` objects that are received via `@RequestBody` or
|
|
`ServerRequest::bodyToFlux(PartEvent.class)` can be relayed to another service
|
|
via the `WebClient`.
|
|
|
|
|
|
|
|
[[webflux-client-filter]]
|
|
== Filters
|
|
|
|
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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.builder()
|
|
.filter((request, next) -> {
|
|
|
|
ClientRequest filtered = ClientRequest.from(request)
|
|
.header("foo", "bar")
|
|
.build();
|
|
|
|
return next.exchange(filtered);
|
|
})
|
|
.build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val client = WebClient.builder()
|
|
.filter { request, next ->
|
|
|
|
val filtered = ClientRequest.from(request)
|
|
.header("foo", "bar")
|
|
.build()
|
|
|
|
next.exchange(filtered)
|
|
}
|
|
.build()
|
|
----
|
|
|
|
This can be used for cross-cutting concerns, such as authentication. The following example uses
|
|
a filter for basic authentication through a static factory method:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import static org.springframework.web.reactive.function.client.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
|
|
|
|
val client = WebClient.builder()
|
|
.filter(basicAuthentication("user", "password"))
|
|
.build()
|
|
----
|
|
|
|
Filters can be added or removed by mutating an existing `WebClient` instance, resulting
|
|
in a new `WebClient` instance that does not affect the original one. For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
|
|
|
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()
|
|
----
|
|
|
|
`WebClient` is a thin facade around the chain of filters followed by an
|
|
`ExchangeFunction`. It provides a workflow to make requests, to encode to and from higher
|
|
level objects, and it helps to ensure that response content is always consumed.
|
|
When filters handle the response in some way, extra care must be taken to always consume
|
|
its content or to otherwise propagate it downstream to the `WebClient` which will ensure
|
|
the same. Below is a filter that handles the `UNAUTHORIZED` status code but ensures that
|
|
any response content, whether expected or not, is released:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
public ExchangeFilterFunction renewTokenFilter() {
|
|
return (request, next) -> next.exchange(request).flatMap(response -> {
|
|
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
|
|
return response.releaseBody()
|
|
.then(renewToken())
|
|
.flatMap(token -> {
|
|
ClientRequest newRequest = ClientRequest.from(request).build();
|
|
return next.exchange(newRequest);
|
|
});
|
|
} else {
|
|
return Mono.just(response);
|
|
}
|
|
});
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
fun renewTokenFilter(): ExchangeFilterFunction? {
|
|
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
|
|
next.exchange(request!!).flatMap { response: ClientResponse ->
|
|
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
|
|
return@flatMap response.releaseBody()
|
|
.then(renewToken())
|
|
.flatMap { token: String? ->
|
|
val newRequest = ClientRequest.from(request).build()
|
|
next.exchange(newRequest)
|
|
}
|
|
} else {
|
|
return@flatMap Mono.just(response)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-attributes]]
|
|
== Attributes
|
|
|
|
You can add attributes to a request. This is convenient if you want to pass information
|
|
through the filter chain and influence the behavior of filters for a given request.
|
|
For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.builder()
|
|
.filter((request, next) -> {
|
|
Optional<Object> usr = request.attribute("myAttribute");
|
|
// ...
|
|
})
|
|
.build();
|
|
|
|
client.get().uri("https://example.org/")
|
|
.attribute("myAttribute", "...")
|
|
.retrieve()
|
|
.bodyToMono(Void.class);
|
|
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val client = WebClient.builder()
|
|
.filter { request, _ ->
|
|
val usr = request.attributes()["myAttribute"];
|
|
// ...
|
|
}
|
|
.build()
|
|
|
|
client.get().uri("https://example.org/")
|
|
.attribute("myAttribute", "...")
|
|
.retrieve()
|
|
.awaitBody<Unit>()
|
|
----
|
|
|
|
Note that you can configure a `defaultRequest` callback globally at the
|
|
`WebClient.Builder` level which lets you insert attributes into all requests,
|
|
which could be used for example in a Spring MVC application to populate
|
|
request attributes based on `ThreadLocal` data.
|
|
|
|
|
|
[[webflux-client-context]]
|
|
== Context
|
|
|
|
<<webflux-client-attributes>> provide a convenient way to pass information to the filter
|
|
chain but they only influence the current request. If you want to pass information that
|
|
propagates to additional requests that are nested, e.g. via `flatMap`, or executed after,
|
|
e.g. via `concatMap`, then you'll need to use the Reactor `Context`.
|
|
|
|
The Reactor `Context` needs to be populated at the end of a reactive chain in order to
|
|
apply to all operations. For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient client = WebClient.builder()
|
|
.filter((request, next) ->
|
|
Mono.deferContextual(contextView -> {
|
|
String value = contextView.get("foo");
|
|
// ...
|
|
}))
|
|
.build();
|
|
|
|
client.get().uri("https://example.org/")
|
|
.retrieve()
|
|
.bodyToMono(String.class)
|
|
.flatMap(body -> {
|
|
// perform nested request (context propagates automatically)...
|
|
})
|
|
.contextWrite(context -> context.put("foo", ...));
|
|
----
|
|
|
|
|
|
|
|
[[webflux-client-synchronous]]
|
|
== Synchronous Use
|
|
|
|
`WebClient` can be used in synchronous style by blocking at the end for the result:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Person person = client.get().uri("/person/{id}", i).retrieve()
|
|
.bodyToMono(Person.class)
|
|
.block();
|
|
|
|
List<Person> persons = client.get().uri("/persons").retrieve()
|
|
.bodyToFlux(Person.class)
|
|
.collectList()
|
|
.block();
|
|
----
|
|
[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,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
|
|
.retrieve().bodyToMono(Person.class);
|
|
|
|
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
|
|
.retrieve().bodyToFlux(Hobby.class).collectList();
|
|
|
|
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
|
|
Map<String, String> map = new LinkedHashMap<>();
|
|
map.put("person", person);
|
|
map.put("hobbies", hobbies);
|
|
return map;
|
|
})
|
|
.block();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val data = runBlocking {
|
|
val personDeferred = async {
|
|
client.get().uri("/person/{id}", personId)
|
|
.retrieve().awaitBody<Person>()
|
|
}
|
|
|
|
val hobbiesDeferred = async {
|
|
client.get().uri("/person/{id}/hobbies", personId)
|
|
.retrieve().bodyToFlow<Hobby>().toList()
|
|
}
|
|
|
|
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
|
|
}
|
|
----
|
|
|
|
The above is merely one example. There are lots of other patterns and operators for putting
|
|
together a reactive pipeline that makes many remote calls, potentially some nested,
|
|
interdependent, without ever blocking until the end.
|
|
|
|
[NOTE]
|
|
====
|
|
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 .
|
|
====
|
|
|
|
|
|
|
|
|
|
[[webflux-client-testing]]
|
|
== Testing
|
|
|
|
To test code that uses the `WebClient`, you can use a mock web server, such as the
|
|
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example
|
|
of its use, check out
|
|
{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`]
|
|
in the Spring Framework test suite or the
|
|
https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`]
|
|
sample in the OkHttp repository.
|