2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client]]
|
|
|
|
= WebClient
|
|
|
|
|
|
|
|
The `spring-webflux` module includes a non-blocking, reactive client for HTTP requests
|
|
|
|
with Reactive Streams back pressure. It shares
|
|
|
|
<<web-reactive.adoc#webflux-codecs,HTTP codecs>> and other infrastructure with the
|
|
|
|
server <<web-reactive.adoc#webflux-fn,functional web framework>>.
|
|
|
|
|
|
|
|
`WebClient` provides a higher level API over HTTP client libraries. By default
|
|
|
|
it uses https://github.com/reactor/reactor-netty[Reactor Netty] but that is pluggable
|
|
|
|
with a different `ClientHttpConnector`. The `WebClient` API returns Reactor `Flux` or
|
|
|
|
`Mono` for output and accepts Reactive Streams `Publisher` as input (see
|
|
|
|
<<web-reactive.adoc#webflux-reactive-libraries>>).
|
|
|
|
|
|
|
|
[TIP]
|
|
|
|
====
|
|
|
|
By comparison to the
|
|
|
|
<<integration.adoc#rest-resttemplate,RestTemplate>>, the `WebClient` offers a more
|
|
|
|
functional and fluent API that taking full advantage of Java 8 lambdas. It supports both
|
|
|
|
sync and async scenarios, including streaming, and brings the efficiency of
|
|
|
|
non-blocking I/O.
|
|
|
|
====
|
|
|
|
|
|
|
|
|
2017-10-19 02:24:17 +08:00
|
|
|
|
|
|
|
|
2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client-retrieve]]
|
|
|
|
== Retrieve
|
|
|
|
|
|
|
|
The `retrieve()` method is the easiest way to get a response body and decode it:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient client = WebClient.create("http://example.org");
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Person> result = client.get()
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Person.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
You can also get a stream of objects decoded from the response:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Flux<Quote> result = client.get()
|
|
|
|
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToFlux(Quote.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
By default, responses with 4xx or 5xx status codes result in an error of type
|
|
|
|
`WebClientResponseException` but you can customize that:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Person> result = client.get()
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
|
|
.retrieve()
|
|
|
|
.onStatus(HttpStatus::is4xxServerError, response -> ...)
|
|
|
|
.onStatus(HttpStatus::is5xxServerError, response -> ...)
|
|
|
|
.bodyToMono(Person.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-10-19 02:24:17 +08:00
|
|
|
|
2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client-exchange]]
|
|
|
|
== Exchange
|
|
|
|
|
|
|
|
The `exchange()` method provides more control. The below example is equivalent
|
|
|
|
to `retrieve()` but also provides access to the `ClientResponse`:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Person> result = client.get()
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
|
|
.exchange()
|
|
|
|
.flatMap(response -> response.bodyToMono(Person.class));
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
At this level you can also create a full `ResponseEntity`:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<ResponseEntity<Person>> result = client.get()
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
|
|
|
.exchange()
|
|
|
|
.flatMap(response -> response.toEntity(Person.class));
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
[CAUTION]
|
|
|
|
====
|
2017-11-16 09:08:57 +08:00
|
|
|
When using `exchange()` you must always use any of the body or toEntity methods of
|
2017-09-29 04:05:44 +08:00
|
|
|
`ClientResponse` to ensure resources are released and to avoid potential issues with HTTP
|
2017-11-16 09:08:57 +08:00
|
|
|
connection pooling. You can use `bodyToMono(Void.class)` if no response content is
|
|
|
|
expected. However keep in mind that if the response does have content, the connection
|
|
|
|
will be closed and will not be placed back in the pool.
|
2017-09-28 11:34:45 +08:00
|
|
|
====
|
|
|
|
|
|
|
|
|
2017-10-19 02:24:17 +08:00
|
|
|
|
|
|
|
|
2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client-body]]
|
|
|
|
== Request body
|
|
|
|
|
|
|
|
The request body can be encoded from an Object:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Person> personMono = ... ;
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/persons/{id}", id)
|
|
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
|
|
.body(personMono, Person.class)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
You can also have a stream of objects encoded:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Flux<Person> personFlux = ... ;
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/persons/{id}", id)
|
|
|
|
.contentType(MediaType.APPLICATION_STREAM_JSON)
|
|
|
|
.body(personFlux, Person.class)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
Or if you have the actual value, use the `syncBody` shortcut method:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
Person person = ... ;
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/persons/{id}", id)
|
|
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
|
|
.syncBody(person)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
|
2017-10-19 02:24:17 +08:00
|
|
|
|
2017-11-07 05:28:27 +08:00
|
|
|
[[webflux-client-body-form]]
|
|
|
|
=== Form data
|
|
|
|
|
|
|
|
To send form data, provide a `MultiValueMap<String, String>` as the body. Note that the
|
|
|
|
content is automatically set to `"application/x-www-form-urlencoded"` by the
|
|
|
|
`FormHttpMessageWriter`:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
MultiValueMap<String, String> formData = ... ;
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/path", id)
|
|
|
|
.syncBody(formData)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-11-07 05:28:27 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
You can also supply form data in-line via `BodyInserters`:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*;
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/path", id)
|
|
|
|
.body(fromFormData("k1", "v1").with("k2", "v2"))
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-11-07 05:28:27 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webflux-client-body-multipart]]
|
|
|
|
=== Multipart data
|
|
|
|
|
|
|
|
To send multipart data, provide a `MultiValueMap<String, ?>` where values are either an
|
|
|
|
Object representing the part body, or an `HttpEntity` representing the part body and
|
|
|
|
headers. `MultipartBodyBuilder` can be used to build the parts:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
|
|
|
builder.part("fieldPart", "fieldValue");
|
|
|
|
builder.part("filePart", new FileSystemResource("...logo.png"));
|
|
|
|
builder.part("jsonPart", new Person("Jason"));
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/path", id)
|
|
|
|
.syncBody(parts)
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-11-07 05:28:27 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
Note that the content type for each part is automatically set based on the extension of the
|
|
|
|
file being written or the type of Object. If you prefer you can also be more explicit and
|
|
|
|
specify the content type for each part.
|
|
|
|
|
|
|
|
You can also supply multipart data in-line via `BodyInserters`:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*;
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/path", id)
|
|
|
|
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
|
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-11-07 05:28:27 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
|
2017-10-19 02:24:17 +08:00
|
|
|
|
2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client-builder]]
|
|
|
|
== Builder options
|
|
|
|
|
|
|
|
A simple way to create `WebClient` is through the static factory methods `create()` and
|
|
|
|
`create(String)` with a base URL for all requests. You can also use `WebClient.builder()`
|
|
|
|
for access to more options.
|
|
|
|
|
|
|
|
To customize the underlying HTTP client:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
SslContext sslContext = ...
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
ClientHttpConnector connector = new ReactorClientHttpConnector(
|
|
|
|
builder -> builder.sslContext(sslContext));
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient webClient = WebClient.builder()
|
|
|
|
.clientConnector(connector)
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
To customize the <<web-reactive.adoc#webflux-codecs,HTTP codecs>> used for encoding and
|
|
|
|
decoding HTTP messages:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
|
|
|
.codecs(configurer -> {
|
|
|
|
// ...
|
|
|
|
})
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient webClient = WebClient.builder()
|
|
|
|
.exchangeStrategies(strategies)
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
The builder can be used to insert <<webflux-client-filter>>.
|
|
|
|
|
|
|
|
Explore the `WebClient.Builder` in your IDE for other options related to URI building,
|
|
|
|
default headers (and cookies), and more.
|
|
|
|
|
|
|
|
After the `WebClient` is built, you can always obtain a new builder from it, in order to
|
|
|
|
build a new `WebClient`, based on, but without affecting the current instance:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient modifiedClient = client.mutate()
|
|
|
|
// user builder methods...
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webflux-client-filter]]
|
|
|
|
== Filters
|
|
|
|
|
|
|
|
`WebClient` supports interception style request filtering:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient client = WebClient.builder()
|
|
|
|
.filter((request, next) -> {
|
|
|
|
ClientRequest filtered = ClientRequest.from(request)
|
|
|
|
.header("foo", "bar")
|
|
|
|
.build();
|
|
|
|
return next.exchange(filtered);
|
|
|
|
})
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
`ExchangeFilterFunctions` provides a filter for basic authentication:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
// static import of ExchangeFilterFunctions.basicAuthentication
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient client = WebClient.builder()
|
|
|
|
.filter(basicAuthentication("user", "pwd"))
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
You can also mutate an existing `WebClient` instance without affecting the original:
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
2017-11-21 05:28:00 +08:00
|
|
|
WebClient filteredClient = client.mutate()
|
|
|
|
.filter(basicAuthentication("user", "pwd")
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|