1. Обзор
Обычно при выполнении HTTP-запросов в наших приложениях мы выполняем эти вызовы последовательно. Однако бывают случаи, когда мы можем захотеть выполнить эти запросы одновременно.
Например, мы можем захотеть сделать это при получении данных из нескольких источников или когда мы просто хотим попытаться повысить производительность нашего приложения.
В этом кратком руководстве мы рассмотрим несколько подходов, чтобы увидеть, как мы можем достичь этого, | делая параллельные вызовы служб с помощью Spring reactive WebClient .
2. Краткое изложение реактивного программирования
Чтобы быстро резюмировать WebClient был представлен в Spring 5 и включен в состав модуля Spring Web Reactive. Он обеспечивает реактивный, неблокирующий интерфейс для отправки HTTP-запросов .
Для получения подробного руководства по реактивному программированию с помощью Web Flux ознакомьтесь с нашим отличным руководством по Spring 5 WebFlux .
3. Простой Пользовательский Сервис
В наших примерах мы будем использовать простой User API. Этот API имеет метод GET, который предоставляет один метод get User для получения пользователя, использующего идентификатор в качестве параметра .
Давайте рассмотрим, как сделать один вызов, чтобы получить пользователя для данного идентификатора:
WebClient webClient = WebClient.create("http://localhost:8080");
public MonogetUser(int id) { LOG.info(String.format("Calling getUser(%d)", id)); return webClient.get() .uri("/user/{id}", id) .retrieve() .bodyToMono(User.class); }
В следующем разделе мы узнаем, как мы можем вызывать этот метод одновременно.
4. Одновременные Вызовы Веб-Клиента
В этом разделе мы рассмотрим несколько примеров одновременного вызова нашего метода getUser //. Мы также рассмотрим обе реализации издателя Flux | и Mono |/в примерах.
4.1. Несколько звонков на одну и ту же службу
Давайте теперь представим, что мы хотим получить данные о пяти пользователях одновременно и вернуть результат в виде списка пользователей :
public FluxfetchUsers(List userIds) { return Flux.fromIterable(userIds) .parallel() .runOn(Schedulers.elastic()) .flatMap(this::getUser) .ordered((u1, u2) -> u2.id() - u1.id()); }
Давайте разложим шаги, чтобы понять, что мы сделали:
- Мы начинаем с создания Flux из нашего списка идентификаторов пользователей с использованием статического из итерационного метода
- Затем мы вызываем метод parallel , который создает Параллельный поток – это указывает на одновременный характер выполнения
- В этом примере мы решили использовать elastic scheduler для запуска вызова, но не стесняйтесь выбирать другую конфигурацию
- Затем мы вызываем flatMap для запуска созданного ранее метода getUser , который возвращает Parallel Flux
- Затем нам нужно указать, как преобразовать Параллельный поток в простой Поток . Мы будем использовать метод ordered с собственным компаратором
Стоит отметить, что, поскольку операции выполняются параллельно, мы не знаем результирующего порядка, поэтому API предоставляет метод ordered .
4.2. Несколько вызовов Различных Служб, Возвращающих один и тот же тип
Давайте теперь посмотрим, как мы можем вызывать несколько служб одновременно .
В этом примере мы собираемся создать другую конечную точку, которая возвращает тот же тип User :
public MonogetOtherUser(int id) { return webClient.get() .uri("/otheruser/{id}", id) .retrieve() .bodyToMono(User.class); }
Теперь метод для параллельного выполнения двух или более вызовов становится:
public FluxfetchUserAndOtherUser(int id) { return Flux.merge(getUser(id), getOtherUser(id)) .parallel() .runOn(Schedulers.elastic()) .ordered((u1, u2) -> u2.id() - u1.id()); }
Основное отличие в этом примере заключается в том, что мы использовали статический метод merge вместо метода fromIterable |. Используя метод слияния, мы можем объединить два или более Flux es в один результат.
4.3. Несколько звонков в Разные Службы Разных Типов
Вероятность того, что две службы вернут одно и то же, довольно мала. Более типично, что у нас будет другая служба, предоставляющая другой тип ответа, и наша цель-объединить два (или более) ответа .
Класс Mono предоставляет статический метод zip, который позволяет объединить два или более результатов:
public Mono fetchUserAndItem(int userId, int itemId) { Monouser = getUser(userId).subscribeOn(Schedulers.elastic()); Mono - item = getItem(itemId).subscribeOn(Schedulers.elastic()); return Mono.zip(user, item, UserWithItem::new); }
Еще один важный момент, который следует отметить, заключается в том, что нам нужно вызвать subscribe On перед передачей результатов в метод zip .
Однако метод subscribe не подписывается на Mono .
Он указывает, какой тип Планировщика использовать при вызове подписки. Опять же, в этом примере мы используем эластичный планировщик, который гарантирует, что каждая подписка выполняется в выделенном отдельном потоке .
Последний шаг-вызвать метод zip , который объединяет данные user и item /Mono s в новый 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)))); } ListuserIds = IntStream.rangeClosed(1, requestsNumber) .boxed() .collect(Collectors.toList()); Client client = new Client("http://localhost:8089"); long start = System.currentTimeMillis(); List users = client.fetchUsers(userIds); 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); }
В этом примере мы использовали подход, который заключается в том, чтобы издеваться над пользовательской службой и заставить ее отвечать на любой запрос за одну секунду. Теперь, если мы сделаем пять звонков с помощью нашего веб-клиента , мы можем предположить, что это не займет более двух секунд, так как вызовы происходят одновременно .
Если мы поближе посмотрим на журналы, когда мы запустим наш тест. Мы видим, что каждый запрос выполняется в другом потоке:
[elastic-6] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(5) [elastic-3] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(2) [elastic-5] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(4) [elastic-2] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(1) [elastic-4] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(3)
Чтобы узнать о других методах тестирования Веб-клиент ознакомьтесь с нашим руководством по Созданию веб-клиента весной .
6. Заключение
В этом уроке мы рассмотрели несколько способов одновременного выполнения вызовов служб HTTP с помощью реактивного веб-клиента Spring 5.
Во-первых, мы показали, как совершать звонки параллельно одной и той же службе. Позже мы увидели пример того, как вызвать две службы, возвращающие разные типы. Затем мы показали, как мы можем протестировать этот код с помощью макетного сервера.
Как всегда, исходный код этой статьи доступен на GitHub.