Spring WebFlux содержит клиент для выполнения HTTP-запросов. WebClient имеет функциональный, текучий API, основанный на Reactor, который позволяет декларативно компоновать асинхронную логику без необходимости работать с потоками или параллелизмом. Он полностью неблокируемый, поддерживает потоковую передачу и основан на тех же кодеках, которые используются для кодирования и декодирования содержимого запросов и ответов на стороне сервера.

WebClient требуется клиентская HTTP-библиотека для выполнения запросов. Имеется встроенная поддержка для:

Конфигурация

Самый простой способ создать WebClient – это использовать один из статических фабричных методов:

  • WebClient.create()

  • WebClient.create(String baseUrl)

Вы также можете использовать WebClient.builder() с дополнительными параметрами:

  • uriBuilderFactory: Настроенная UriBuilderFactory для использования в качестве базового URL-адреса.

  • defaultUriVariables: значения по умолчанию для использования при расширении URI-шаблонов.

  • defaultHeader: Заголовки для каждого запроса.

  • defaultCookie: Файлы cookie для каждого запроса.

  • defaultRequest: Consumer для настройки каждого запроса.

  • filter: Клиентский фильтр для каждого запроса.

  • exchangeStrategies: Настройки чтения/записи HTTP-сообщений.

  • clientConnector: Настройки HTTP-библиотеки клиента.

Например

Java
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()

После создания WebClient является неизменяемым. Однако его можно клонировать и создать модифицированную копию следующим образом:

Java
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 имеет filterA, filterB
// client2 имеет filterA, filterB, filterC, filterD
Kotlin
val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 имеет filterA, filterB
// client2 имеет filterA, filterB, filterC, filterD

MaxInMemorySize

Кодеки имеют ограничения на буферизацию данных в памяти, чтобы избежать проблем с памятью приложения. По умолчанию они установлены на 256 Кбайт. Если этого окажется недостаточно, то будет получена следующая ошибка:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

Чтобы изменить ограничение для кодеков по умолчанию, выполните следующее:

Java
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()

Reactor Netty

Чтобы настроить параметры Reactor Netty, предоставьте предварительно сконфигурированный HttpClient:

Java
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()

Ресурсы

По умолчанию HttpClient принимает участие в использовании глобальных ресурсов Reactor Netty, хранящихся в reactor.netty.http.HttpResources, включая потоки цикла ожидания событий и пул соединений. Этот режим является рекомендуемым, поскольку в целях параллелизма циклов ожидания событий предпочтительнее использовать фиксированные, общие ресурсы. В этом режиме глобальные ресурсы остаются активными до завершения процесса.

Если сервер синхронизирован с процессом, обычно необходимости в явном завершении работы нет. Однако если сервер может запускаться или останавливаться внутрипроцессно (как в случае с приложеним Spring MVC, развернутым в виде WAR-файла), то можно объявить управляемый Spring бин типа ReactorResourceFactory с параметром globalResources=true (по умолчанию), чтобы использование глобальных ресурсов Reactor Netty гарантированно было завершено при закрытии ApplicationContext из Spring, как показано в следующем примере:

Java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

Можно также избежать участия в использовании глобальных ресурсов Reactor Netty. Однако в этом режиме на вас ложится ответственность за то, чтобы все экземпляры клиента и сервера Reactor Netty использовали общие ресурсы, как это показано в следующем примере:

Java
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false); 
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Дальнейшая настройка...
};
ClientHttpConnector connector =
    new ReactorClientHttpConnector(resourceFactory(), mapper); 
return WebClient.builder().clientConnector(connector).build(); 
}
  1. Создаем ресурсы, независимые от глобальных.
  2. Используем конструктор ReactorClientHttpConnector с фабрикой ресурсов.
  3. Подключаем коннектор к WebClient.Builder.
Kotlin
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false 
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Дальнейшая настройка...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) 
return WebClient.builder().clientConnector(connector).build() 
}
  1. Создаем ресурсы, независимые от глобальных.
  2. Используем конструктор ReactorClientHttpConnector с фабрикой ресурсов.
  3. Подключаем коннектор к WebClient.Builder.

Время ожидания

Настройка значений времени ожидания соединения:

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();
Kotlin
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

Настройка значения времени ожидания чтения и записи:

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)));
// Создаем WebClient...
Kotlin
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10))
}
// Создаем WebClient...

Настройка времени ожидания ответа для конкретного запроса:

Java
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Создаем WebClient...
Kotlin
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Создаем WebClient...

В следующем примере показано, как настроить параметры HttpClient из Jetty:

Java
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
    HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
    reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
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)

Jetty

В следующем примере показано, как настроить параметры HttpClient из Jetty:

Java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
Kotlin
val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();

По умолчанию HttpClient создает свои собственные ресурсы (Executor, ByteBufferPool, Scheduler), которые остаются активными до завершения выполнения процесса или вызова функции stop().

Можно разделить ресурсы между несколькими экземплярами клиента Jetty (и сервера) и обеспечить завершение использования ресурсов при закрытии ApplicationContext из Spring, объявив управляемый Spring бин типа JettyResourceFactory, как показано в следующем примере:

Java
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Дальнейшая настройка...
ClientHttpConnector connector =
    new JettyClientHttpConnector(httpClient, resourceFactory()); 
return WebClient.builder().clientConnector(connector).build(); 
}
  1. Используем конструктор JettyClientHttpConnector с фабрикой ресурсов.
  2. Подключаем коннектор к WebClient.Builder.
Kotlin
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Дальнейшая настройка...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) 
return WebClient.builder().clientConnector(connector).build() 
}
  1. Используем конструктор JettyClientHttpConnector с фабрикой ресурсов.
  2. Подключаем коннектор к WebClient.Builder.

HttpComponents

В следующем примере показано, как настроить параметры HttpClient из Apache HttpComponents:

Java
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin
val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()

retrieve()

Метод retrieve() можно использовать для объявления способа извлечения ответа. Например:

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);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()

Или получаем только тело:

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);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()

Получение потока декодированных объектов:

Java
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
Kotlin
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()

По умолчанию ответы 4xx или 5xx приводят к генерации WebClientResponseException, включая подклассы для определенных кодов состояния HTTP. Чтобы настроить обработку сообщений об ошибках, используйте обработчики onStatus следующим образом:

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);
Kotlin
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()

Exchange

Методы exchangeToMono() и exchangeToFlux() (или awaitExchange { } и exchangeToFlow { } в Kotlin) полезны для более сложных случаев, требующих большего контроля, например, для разного декодирования ответа в зависимости от статуса ответа:

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 {
        // Обращаемся к ошибке
        return response.createException().flatMap(Mono::error);
    }
});
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()
}
}

При использовании вышеуказанного кода, после завершения работы возвращенного Mono или Flux, тело ответа проверяется и, если не используется, то освобождается, чтобы предотвратить утечку памяти и соединений. Поэтому ответ нельзя декодировать далее в нисходящем направлении. Предоставляемая функция должна сама определять, как декодировать ответ, если это необходимо.

Тело запроса

Тело запроса может кодироваться из любого асинхронного типа, обрабатываемого ReactiveAdapterRegistry, например, Mono или Deferred из сопрограмм Kotlin, как показано в следующем примере:

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);
Kotlin
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()

Можно также кодировать поток объектов, как показано в следующем примере:

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);
Kotlin
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()

Кроме того, если имеется фактическое значение, то можно использовать сокращенный метод bodyValue, как показано в следующем примере:

Java
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()

Данные формы

Чтобы отправить данные формы, можно указать MultiValueMap<String, String> в качестве тела. Обратите внимание, что содержимое автоматически устанавливается в application/x-www-form-urlencoded с помощью FormHttpMessageWriter. В следующем примере показано, как использовать MultiValueMap<String, String>:

Java
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()

Вы также можете добавлять данные формы встраиваемым образом с помощью BodyInserters, как показано в следующем примере:

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);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()

Многокомпонентные данные

Для отправки многокомпонентных данных необходимо указать строку MultiValueMap<String, ?>, значениями которой являются либо экземпляры Object, представляющие содержимое компонента, либо экземпляры HttpEntity, представляющие содержимое и заголовки компонента. MultipartBodyBuilder предусматривает удобный API для подготовки многокомпонентного запроса. В следующем примере показано, как создать MultiValueMap<String, ?>:

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();
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()

В большинстве случаев не требуется задавать Content-Type для каждого компонента. Тип содержимого определяется автоматически на основе HttpMessageWriter, выбранного для сериализации, или, в случае Resource, на основе расширения файла. При необходимости можно явным образом задать MediaType для каждого компонента через один из перегруженных методов средства сборки part.

После подготовки MultiValueMap проще всего передать её WebClient через метод body, как показано в следующем примере:

Java
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
Kotlin
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()

Если MultiValueMap содержит хотя бы одно не-String значение, которое также может представлять обычные данные формы (то есть application/x-www-form-urlencoded), не требуется устанавливать Content-Type в multipart/form-data. Это всегда происходит при использовании MultipartBodyBuilder, который обеспечивает функцию-обёртку HttpEntity.

В качестве альтернативы MultipartBodyBuilder, также можно предоставить многокомпонентное содержимое во встроенном стиле с помощью встроенных BodyInserters, как показано в следующем примере:

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);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()

Фильтры

Можно зарегистрировать клиентский фильтр ExchangeFilterFunction) через WebClient.Builder, чтобы перехватывать и модифицировать запросы, как показано в следующем примере:

Java
WebClient client = WebClient.builder()
.filter((request, next) -> {
    ClientRequest filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build();
    return next.exchange(filtered);
})
.build();
Kotlin
val client = WebClient.builder()
.filter { request, next ->
    val filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build()
    next.exchange(filtered)
}
.build()

Это можно использовать для сквозной функциональности, например, аутентификации. В следующем примере используется фильтр для базовой аутентификации через статический фабричный метод:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
Kotlin
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()

Фильтры можно добавлять или удалять путем изменения существующего экземпляра WebClient, в результате чего создается новый экземпляр WebClient, не влияющий на исходный. Например:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
    filterList.add(0, basicAuthentication("user", "password"));
})
.build();
Kotlin
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()

WebClient – это тонкий интерфейс над цепочкой фильтров, сопровождаемый ExchangeFunction. Он обеспечивает рабочий процесс для выполнения запросов, кодирования в объекты более высокого уровня и обратно, а также помогает гарантировать, что содержимое ответа будет всегда потребляется. Если фильтры каким-либо образом обрабатывают ответ, необходимо позаботиться о том, чтобы его содержимое всегда потреблялось или иным образом распространялось в нисходящем направлении к WebClient, который будет обеспечивать то же самое. Ниже приведен фильтр, который обрабатывает код состояния UNAUTHORIZED, но гарантирует, что любое содержимое ответа, будь то ожидаемое или нет, будет выдано:

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);
}
});
}
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)
    }
}
}
}

Атрибуты

К запросу можно добавлять атрибуты. Это удобно, если нужно передавать информацию по цепочке фильтров и влиять на логику работы фильтров в рамках данного запроса. Например:

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);
}
Kotlin
val client = WebClient.builder()
.filter { request, _ ->
    val usr = request.attributes()["myAttribute"];
    // ...
}
.build()
client.get().uri("https://example.org/")
    .attribute("myAttribute", "...")
    .retrieve()
    .awaitBody<Unit>()

Обратите внимание, что можно глобально сконфигурировать обратный вызов defaultRequest на уровне WebClient.Builder, который позволяет вставлять атрибуты во все запросы, что можно использовать, например, в приложении на Spring MVC для заполнения атрибутов запроса на основе данных ThreadLocal.

Context

Атрибуты обеспечивают удобную передачу информации в цепочку фильтров, но влияют только на текущий запрос. Если требуется передать информацию, распространяющуюся на дополнительные запросы, которые вложены, например, через flatMap, или выполняются после, например, через concatMap, то нужно использовать Context из Reactor.

Context из проекта Reactor нужно заполнять в конце реактивной цепочки, чтобы он применялся ко всем операциям. Например:

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 -> {
        // выполняем вложенный запрос (контекст распространяется автоматически)...
})
.contextWrite(context -> context.put("foo", ...));

Синхронное использование

WebClient можно использовать в синхронном стиле, блокируя в конце для получения результата:

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();
Kotlin
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
    .awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
    .bodyToFlow<Person>()
    .toList()
}

Однако если необходимо выполнить несколько вызовов, эффективнее не блокировать каждый ответ по отдельности, а дождаться совокупного результата:

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();
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())
}

Приведенное выше – лишь один из примеров. Существует множество других шаблонов и операторов для создания реактивного конвейера, который выполняет множество удаленных вызовов, потенциально несколько вложенных, взаимозависимых, без блокировки до самого конца.

При использовании Flux или Mono не придется вообще блокировать контроллер Spring MVC или Spring WebFlux. Просто можно будет вернуть результирующий реактивный тип из метода контроллера. Тот же принцип применим к сопрограммам Kotlin и Spring WebFlux – просто используйте приостанавливающую функцию или возврат Flow в методе контроллера.

Тестирование

Для тестирования кода, использующего WebClient, можно использовать объект-имитацию веб-сервера, например, OkHttp MockWebServer. Чтобы ознакомиться с примером его использования, см. WebClientIntegrationTests в тестовом комплекте Spring Framework или пример статического сервера в репозитории OkHttp.