Spring WebFlux містить клієнт для виконання запитів HTTP. WebClient
має функціональний, текучий API,
заснований на Reactor, що дозволяє декларативно компонувати асинхронну логіку без необхідності працювати з потоками
чи паралелізмом. Він повністю неблокований, підтримує потокову передачу і заснований на тих же кодеках, які
використовуються для кодування та декодування вмісту запитів і відповідей на стороні сервера. запитів. Є вбудована
підтримка:
Інші клієнти можна підключити через
ClientHttpConnector
.
Конфігурація
Найпростіший спосіб створити WebClient
— це використовувати один із
статичних фабричних методів:
WebClient.create()
WebClient.create(String baseUrl)
Ти також можеш використовувати WebClient.builder()
з додатковими параметрами:
uriBuilderFactory
: НалаштованаUriBuilderFactory
для використання як базова URL-адреса.defaultUriVariables
: Стандартні значення для використання при розширенні URI-шаблонів.defaultHeader
: Заголовки для кожного запиту.defaultCookie
: Файли cookie для кожного запиту.defaultRequest
:Consumer
для налаштування кожного запиту.filter
: Клієнтський фільтр для кожного запиту.exchangeStrategies
: Налаштування читання/запису HTTP-повідомлень.clientConnector
: Налаштування HTTP-бібліотеки клієнта.
Наприклад
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()
Після створення WebClient
є незмінним. Однак його можна клонувати та створити модифіковану копію
таким чином:
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
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: Використовуваний максимальний термін на макс.
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()
Reactor Netty
Щоб налаштувати параметри Reactor Netty, надай попередньо налаштований HttpClient
:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
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, як показано в наступному прикладі:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
Можна також уникнути участі у використанні глобальних ресурсів Reactor Netty. Однак у цьому режимі на тебе лягає відповідальність за те, щоб усі екземпляри клієнта та сервера Reactor Netty використовували спільні ресурси, як це показано в наступному прикладі:
@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();
}
- Створюємо ресурси, незалежні від глобальних.
- Використовуємо конструктор
ReactorClientHttpConnector
з фабрикою ресурсів. - Підключаємо конектор до
WebClient.Builder
.
@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()
}
- Створюємо ресурси, незалежні від глобальних.
- Використовуємо конструктор
ReactorClientHttpConnector
з фабрикою ресурсів. - Підключаємо конектор до
WebClient.Builder
.
Час очікування
Налаштування значень часу очікування з'єднання:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Налаштування значення часу очікування та запису:
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...
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...
Налаштування часу очікування відповіді для конкретного запиту:
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create a WebClient...
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Створюємо WebClient...
У наступному прикладі показано, як налаштувати параметри HttpClient
з Jetty:
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
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:
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
За замовчуванням HttpClient
створює власні ресурси (Executor
,
ByteBufferPool
, Scheduler
), які залишаються активними до завершення виконання
процесу
або виклику функції stop()
.
Можна розділити ресурси між кількома екземплярами клієнта Jetty (і сервера) та забезпечити завершення
використання ресурсів при закритті ApplicationContext
зі Spring, оголосивши керований
Spring бін
типу JettyResourceFactory
, як показано в наступному прикладі:
@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();
}
- Використовуємо конструктор
JettyClientHttpConnector
з фабрикою ресурсів. - Підключаємо конектор до
WebClient.Builder
.
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Подальше налаштування...
val connector = JettyClientHttpConnector(httpClient, resourceFactory())
return WebClient.builder().clientConnector(connector).build()
}
- Використовуємо конструктор
JettyClientHttpConnector
з фабрикою ресурсів. - Підключаємо конектор до
WebClient.Builder
.
HttpComponents
У наступному прикладі показано, як налаштувати параметри HttpClient
з Apache HttpComponents:
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()
retrieve()
Метод retrieve()
можна використовувати для оголошення способу отримання відповіді.
Наприклад:
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);
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()
Або отримуємо тільки тіло:
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()
Отримання потоку декодованих об'єктів:
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()
За замовчуванням відповіді 4xx або 5xx призводять до генерації WebClientResponseException
,
включно з підкласами для певних кодів стану HTTP. Щоб налаштувати обробку повідомлень про помилки,
використовуйте обробники onStatus
таким чином:
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);
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) корисні для більш складних випадків, що
потребують
більшого контролю, наприклад, для різного декодування відповіді залежно від статусу відповіді:
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);
}
});
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, як показано в наступному прикладі:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
Можна також кодувати потік об'єктів, як показано в наступному прикладі:
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);
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()
До того ж, якщо є фактичне значення, можна використовувати скорочений метод
bodyValue
, як
показано в наступному прикладі:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
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>
:
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()
Ти також можеш додавати дані форми вбудованим чином за допомогою BodyInserters
, як
показано в
наступному прикладі:
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);
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, ?>
:
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();
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
, як
показано
в наступному прикладі:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
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
, як показано в наступному прикладі:
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);
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
,
щоб перехоплювати та модифікувати запити, як показано в наступному прикладі :
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
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();
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()
Фільтри можна додавати або видаляти шляхом зміни існуючого екземпляра WebClient
,
внаслідок чого
створюється новий екземпляр WebClient
, який не впливає на вихідний. Наприклад:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
WebClient
— це тонкий інтерфейс над ланцюжком фільтрів, що супроводжується
ExchangeFunction
.
Він забезпечує робочий процес для виконання запитів, кодування в об'єкти вищого рівня та
назад, а також допомагає
гарантувати, що вміст відповіді завжди споживається. Якщо фільтри будь-яким чином
опрацьовують відповідь, необхідно
подбати про те, щоб його вміст завжди споживався або іншим чином поширювався у низхідному
напрямку до WebClient
,
який забезпечуватиме те ж саме. Нижче наведено фільтр, який обробляє код стану UNAUTHORIZED
,
але
гарантує, що будь-який вміст відповіді, будь то очікуваний чи ні, буде видано:
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);
}
});
}
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)
}
}
}
}
Атрибути
До запиту можна додавати атрибути. Це зручно, якщо потрібно передавати інформацію ланцюжком фільтрів та впливати на логіку роботи фільтрів у рамках цього запиту. Наприклад:
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);
}
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 потрібно заповнювати в кінці реактивного ланцюжка, щоб
він
застосовувався до всіх операцій. Наприклад:
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
можна використовувати в синхронному стилі, блокуючи в
кінці для отримання результату:
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();
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve( )
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}}
Але якщо необхідно здійснити кілька викликів, ефективніше не блокувати кожну відповідь окремо, а дочекатися сукупного результату:
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();
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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ