2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client]]
|
|
|
|
= WebClient
|
|
|
|
|
2018-08-14 20:48:14 +08:00
|
|
|
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.
|
2018-02-15 06:32:24 +08:00
|
|
|
|
2018-08-14 20:48:14 +08:00
|
|
|
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`.
|
2017-09-28 11:34:45 +08:00
|
|
|
|
|
|
|
|
2018-02-15 06:32:24 +08:00
|
|
|
|
2018-08-14 20:48:14 +08:00
|
|
|
|
|
|
|
[[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
|
2018-08-14 22:34:17 +08:00
|
|
|
`ReactorResourceFactory` with `globalResources=true` (the default) to ensure the Reactor
|
2018-08-14 20:48:14 +08:00
|
|
|
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`.
|
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-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
|
|
|
----
|
|
|
|
|
2018-08-08 19:07:01 +08:00
|
|
|
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:
|
2017-09-28 11:34:45 +08:00
|
|
|
|
|
|
|
[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
|
|
|
|
|
2018-03-24 07:00:59 +08:00
|
|
|
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:
|
2017-11-07 05:28:27 +08:00
|
|
|
|
|
|
|
[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();
|
2018-03-24 07:00:59 +08:00
|
|
|
----
|
|
|
|
|
|
|
|
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 = ...;
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2017-11-21 05:28:00 +08:00
|
|
|
Mono<Void> result = client.post()
|
|
|
|
.uri("/path", id)
|
2018-03-24 07:00:59 +08:00
|
|
|
.syncBody(**builder.build()**)
|
2017-11-21 05:28:00 +08:00
|
|
|
.retrieve()
|
|
|
|
.bodyToMono(Void.class);
|
2017-11-07 05:28:27 +08:00
|
|
|
----
|
|
|
|
|
2018-03-24 07:00:59 +08:00
|
|
|
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.
|
2017-11-07 05:28:27 +08:00
|
|
|
|
2018-03-24 07:00:59 +08:00
|
|
|
As an alternative to `MultipartBodyBuilder`, you can also provide multipart content,
|
|
|
|
inline-style, through the built-in `BodyInserters`. For example:
|
2017-11-07 05:28:27 +08:00
|
|
|
|
|
|
|
[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
|
|
|
|
2018-03-24 07:00:59 +08:00
|
|
|
|
2017-09-28 11:34:45 +08:00
|
|
|
[[webflux-client-filter]]
|
2018-05-24 19:16:54 +08:00
|
|
|
== Client Filters
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2018-08-14 20:48:14 +08:00
|
|
|
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
|
|
|
|
in order to intercept and/or modify requests:
|
2017-09-28 11:34:45 +08:00
|
|
|
|
|
|
|
[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();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
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:
|
2017-09-28 11:34:45 +08:00
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
----
|
|
|
|
|
2018-05-24 19:16:54 +08:00
|
|
|
// static import of ExchangeFilterFunctions.basicAuthentication
|
2017-09-28 11:34:45 +08:00
|
|
|
|
2018-05-24 19:16:54 +08:00
|
|
|
WebClient client = WebClient.builder()
|
|
|
|
.filter(basicAuthentication("user", "password"))
|
|
|
|
.build();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
|
|
|
|
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:
|
2017-09-28 11:34:45 +08:00
|
|
|
|
|
|
|
[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();
|
2017-09-28 11:34:45 +08:00
|
|
|
----
|
2018-02-15 06:32:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[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.
|