Spring WebFlux містить клієнт для виконання запитів HTTP. WebClient має функціональний, текучий API, заснований на Reactor, що дозволяє декларативно компонувати асинхронну логіку без необхідності працювати з потоками чи паралелізмом. Він повністю неблокований, підтримує потокову передачу і заснований на тих же кодеках, які використовуються для кодування та декодування вмісту запитів і відповідей на стороні сервера. запитів. Є вбудована підтримка:

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

Найпростіший спосіб створити 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: Використовуваний максимальний термін на макс.
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)));
// Створюємо a 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));
// Create a 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()

Це можна використовувати для наскрізної функціональності, наприклад, аутентифікації. У наступному прикладі використовується фільтр для базової аутентифікації через статичний фабричний метод:

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 -> {
        // виконуємо вкладений запит (контекст поширюється автоматично)...
})

Синхронне використання

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.