spring-framework/src/docs/asciidoc/web/webflux-webclient.adoc

466 lines
14 KiB
Plaintext
Raw Normal View History

[[webflux-client]]
= WebClient
Spring WebFlux includes a reactive, non-blocking `WebClient` for performing HTTP requests
using a functional-style API that exposes Reactor `Flux` and `Mono` types, see
<<web-reactive.adoc#webflux-reactive-libraries>>. The client relies on the same
<<web-reactive.adoc#webflux-codecs,codecs>> that WebFlux server applications use to work
with request and response content.
Internally `WebClient` delegates to an HTTP client library. By default it uses
https://github.com/reactor/reactor-netty[Reactor Netty], there is built-in support for
the Jetty https://github.com/jetty-project/jetty-reactive-httpclient[reactive HtpClient],
and others can be plugged in through a `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)`
The above uses Reactor Netty `HttpClient` from "io.projectreactor.netty:reactor-netty"
with default settings and participates in global resources such for event loop threads and
a connection pool, see <<webflux-client-builder-reactor, Reactor Netty configuration>>.
The `WebClient.Builder` can be used for access to further options:
* `uriBuilderFactory` -- customized `UriBuilderFactory` to use as a base URL.
* `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.
For example, to configure <<web-reactive.adoc#webflux-codecs,HTTP codecs>>:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
// ...
})
.build();
WebClient 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:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
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
----
[[webflux-client-builder-reactor]]
=== Reactor Netty
To customize Reactor Netty settings:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
HttpClient httpClient = HttpClient.create()
httpClient.secure(sslSpec -> ...);
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
----
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, e.g. Spring MVC
application deployed as a WAR, you can declare a Spring-managed bean of type
`ReactorResourceFactory` with `globaResources=true` (the default) to ensure the Reactor
Netty global resources are shut down when the Spring `ApplicationContext` is closed:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
----
You may also choose not to participate in the global Reactor Netty resources. However keep
in mind in this mode the burden is on you to ensure all Reactor Netty client and server
instances use shared resources:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setGlobalResources(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 `ReactorClientHttpConnector` constructor with resource factory.
<3> Plug the connector into the `WebClient.Builder`.
[[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"]
----
WebClient client = WebClient.create("http://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
----
You can also get a stream of objects decoded from the response:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
----
By default, responses with 4xx or 5xx status codes result in an
`WebClientResponseException` or one of its HTTP status specific sub-classes such as
`WebClientResponseException.BadRequest`, `WebClientResponseException.NotFound`, and others.
You can also use the `onStatus` method to customize the resulting exception:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
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);
----
[[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"]
----
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
----
At this level you can also create a full `ResponseEntity`:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
----
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]
====
When using `exchange()` you must always use any of the body or toEntity methods of
`ClientResponse` to ensure resources are released and to avoid potential issues with HTTP
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.
====
[[webflux-client-body]]
== Request body
The request body can be encoded from an Object:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
----
You can also have a stream of objects encoded:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
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);
----
Or if you have the actual value, use the `syncBody` shortcut method:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.syncBody(person)
.retrieve()
.bodyToMono(Void.class);
----
[[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"]
----
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.syncBody(formData)
.retrieve()
.bodyToMono(Void.class);
----
You can also supply form data in-line via `BodyInserters`:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
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);
----
[[webflux-client-body-multipart]]
=== Multipart data
To send multipart data, you need to provide a `MultiValueMap<String, ?>` whose values are
either Objects representing part content, or `HttpEntity` representing the content and
headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a
multipart request:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
MultiValueMap<String, HttpEntity<?>> 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 fo the overloaded
builder `part` methods.
Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is
through the `syncBody` method:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.syncBody(**builder.build()**)
.retrieve()
.bodyToMono(Void.class);
----
If the `MultiValueMap` contains at least one non-String value, which could also be
represent regular form data (i.e. "application/x-www-form-urlencoded"), you don't have to
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`. For example:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
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);
----
[[webflux-client-filter]]
2018-05-24 19:16:54 +08:00
== Client Filters
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
in order to intercept and/or modify requests:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
2018-05-24 19:16:54 +08:00
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
----
2018-05-24 19:16:54 +08:00
This can be used for cross-cutting concerns such as authentication. The example below uses
a filter for basic authentication through a static factory method:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
2018-05-24 19:16:54 +08:00
// static import of ExchangeFilterFunctions.basicAuthentication
2018-05-24 19:16:54 +08:00
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
----
2018-05-24 19:16:54 +08:00
Filters apply globally to every request. To change how 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:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
2018-05-24 19:16:54 +08:00
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("http://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
----
You can also replicate an existing `WebClient`, and insert new filters or remove already
registered filters. In the example below, a basic authentication filter is inserted at
index 0:
[source,java,intent=0]
[subs="verbatim,quotes"]
----
// static import of ExchangeFilterFunctions.basicAuthentication
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
----
[[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 example
use, check
https://github.com/spring-projects/spring-framework/blob/master/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[WebClientIntegrationTests]
in the Spring Framework tests, or the
https://github.com/square/okhttp/tree/master/samples/static-server[static-server]
sample in the OkHttp repository.