Spring WebFlux содержит клиент для выполнения HTTP-запросов. WebClient
имеет функциональный, текучий API, основанный на Reactor, который позволяет декларативно компоновать асинхронную логику без необходимости работать с потоками или параллелизмом. Он полностью неблокируемый, поддерживает потоковую передачу и основан на тех же кодеках, которые используются для кодирования и декодирования содержимого запросов и ответов на стороне сервера.
WebClient
требуется клиентская HTTP-библиотека для выполнения запросов. Имеется встроенная поддержка для:
-
Другие клиенты можно подключить через
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: Exceeded limit on max bytes to buffer
Чтобы изменить ограничение для кодеков по умолчанию, выполните следующее:
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)));
// Создаем 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));
// Создаем 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 -> {
// выполняем вложенный запрос (контекст распространяется автоматически)...
})
.contextWrite(context -> context.put("foo", ...));
Синхронное использование
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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ