Перейти к основному содержимому

Одновременные вызовы Spring WebClient

· 5 мин. чтения

1. Обзор

Обычно при выполнении HTTP-запросов в наших приложениях мы выполняем эти вызовы последовательно. Однако бывают случаи, когда мы можем захотеть выполнить эти запросы одновременно.

Например, мы можем захотеть сделать это при извлечении данных из нескольких источников или когда мы просто хотим повысить производительность нашего приложения.

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

2. Резюме по реактивному программированию

Чтобы быстро подвести итог, WebClient был представлен в Spring 5 и включен как часть модуля Spring Web Reactive. Он предоставляет реактивный неблокирующий интерфейс для отправки HTTP-запросов .

Чтобы получить подробное руководство по реактивному программированию с помощью WebFlux, ознакомьтесь с нашим превосходным руководством по Spring 5 WebFlux .

3. Простой пользовательский сервис

В наших примерах мы будем использовать простой пользовательский API. Этот API имеет метод GET, который предоставляет один метод getUser для получения пользователя с использованием идентификатора в качестве параметра .

Давайте посмотрим, как сделать один вызов для получения пользователя с заданным идентификатором:

WebClient webClient = WebClient.create("http://localhost:8080");
public Mono<User> getUser(int id) {
LOG.info(String.format("Calling getUser(%d)", id));

return webClient.get()
.uri("/user/{id}", id)
.retrieve()
.bodyToMono(User.class);
}

В следующем разделе мы узнаем, как мы можем вызывать этот метод одновременно.

4. Выполнение одновременных вызовов WebClient

В этом разделе мы рассмотрим несколько примеров одновременного вызова нашего метода getUser . Мы также рассмотрим обе реализации издателя Flux и Mono в примерах.

4.1. Несколько обращений к одной и той же службе

Давайте теперь представим, что мы хотим получить данные о пяти пользователях одновременно и вернуть результат в виде списка пользователей :

public Flux fetchUsers(List userIds) {
return Flux.fromIterable(userIds)
.flatMap(this::getUser);
}

Давайте разберем шаги, чтобы понять, что мы сделали:

Мы начинаем с создания Flux из нашего списка идентификаторов пользователей с помощью статического метода fromIterable . ``

Затем мы вызываем flatMap для запуска созданного ранее метода getUser. Этот реактивный оператор по умолчанию имеет уровень параллелизма 256, что означает, что он выполняет не более 256 вызовов getUser одновременно. Это число настраивается с помощью параметра метода с использованием перегруженной версии flatMap .

Стоит отметить, что, поскольку операции выполняются параллельно, мы не знаем итоговый порядок. Если нам нужно сохранить порядок ввода, мы можем вместо этого использовать оператор flatMapSequential .

Поскольку Spring WebClient использует неблокирующий HTTP-клиент под капотом, пользователю не нужно определять какой-либо планировщик. WebClient самостоятельно планирует вызовы и публикует их результаты в соответствующих потоках без блокировки.

4.2. Множественные вызовы к разным службам, возвращающие один и тот же тип

Давайте теперь посмотрим, как мы можем вызывать несколько служб одновременно .

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

public Mono<User> getOtherUser(int id) {
return webClient.get()
.uri("/otheruser/{id}", id)
.retrieve()
.bodyToMono(User.class);
}

Теперь метод для параллельного выполнения двух или более вызовов становится следующим:

public Flux fetchUserAndOtherUser(int id) {
return Flux.merge(getUser(id), getOtherUser(id));
}

Основное отличие в этом примере заключается в том, что мы использовали статический метод merge вместо метода fromIterable . Используя метод слияния, мы можем объединить два или более Flux в один результат.

4.3. Несколько обращений к разным службам разных типов

Вероятность того, что два сервиса вернут одно и то же, довольно мала. Чаще всего у нас будет другая служба, предоставляющая другой тип ответа, и наша цель — объединить два (или более) ответа .

Класс Mono предоставляет статический метод zip, который позволяет нам объединять два или более результатов:

public Mono fetchUserAndItem(int userId, int itemId) {
Mono user = getUser(userId);
Mono item = getItem(itemId);

return Mono.zip(user, item, UserWithItem::new);
}

Метод zip объединяет данные пользователя и элемент Mono в новый Mono с типом UserWithItem . Это простой объект POJO, обертывающий пользователя и элемент.

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

В этом разделе мы увидим, как мы можем протестировать код, который мы уже видели, и, в частности, проверить, что вызовы службы происходят параллельно.

Для этого мы собираемся использовать Wiremock для создания фиктивного сервера и протестируем метод fetchUsers :

@Test
public void givenClient_whenFetchingUsers_thenExecutionTimeIsLessThanDouble() {

int requestsNumber = 5;
int singleRequestTime = 1000;

for (int i = 1; i <= requestsNumber; i++) {
stubFor(get(urlEqualTo("/user/" + i)).willReturn(aResponse().withFixedDelay(singleRequestTime)
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(String.format("{ \"id\": %d }", i))));
}

List<Integer> userIds = IntStream.rangeClosed(1, requestsNumber)
.boxed()
.collect(Collectors.toList());

Client client = new Client("http://localhost:8089");

long start = System.currentTimeMillis();
List<User> users = client.fetchUsers(userIds).collectList().block();
long end = System.currentTimeMillis();

long totalExecutionTime = end - start;

assertEquals("Unexpected number of users", requestsNumber, users.size());
assertTrue("Execution time is too big", 2 * singleRequestTime > totalExecutionTime);
}

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

Чтобы узнать о других методах тестирования WebClient , ознакомьтесь с нашим руководством по имитации WebClient в Spring .

6. Заключение

В этом руководстве мы рассмотрели несколько способов одновременного выполнения вызовов службы HTTP с помощью Spring 5 Reactive WebClient.

Сначала мы показали, как параллельно звонить в один и тот же сервис. Позже мы увидели пример вызова двух сервисов, возвращающих разные типы. Затем мы показали, как можно протестировать этот код с помощью фиктивного сервера.

Как всегда, исходный код этой статьи доступен на GitHub.