Chamadas simultâneas do WebClient da Primavera
1. Visão geral
Normalmente, ao fazer solicitações HTTP em nossos aplicativos, executamos essas chamadas sequencialmente. No entanto, há ocasiões em que podemos querer executar essas solicitações simultaneamente.
Por exemplo, podemos fazer isso ao recuperar dados de várias fontes ou quando queremos simplesmente dar um aumento de desempenho ao nosso aplicativo.
Neste tutorial rápido,we’ll take a look at several approaches to see how we can accomplish this bymaking parallel service calls using theSpring reactive WebClient.
2. Recapitulação da Programação Reativa
Para recapitular rapidamente,WebClient foi introduzido no Spring 5 e está incluído como parte do módulo Spring Web Reactive. It provides a reactive, non-blocking interface for sending HTTP requests.
Para obter um guia detalhado de programação reativa com WebFlux, verifique nosso excelenteGuide to Spring 5 WebFlux.
3. Um serviço de usuário simples
Estaremos usando uma APIUser simples em nossos exemplos. This API has a GET method that exposes one method getUser for retrieving a user using the id as a parameter.
Vamos dar uma olhada em como fazer uma única chamada para recuperar um usuário para um determinado ID:
WebClient webClient = WebClient.create("http://localhost:8080");
public Mono getUser(int id) {
LOG.info(String.format("Calling getUser(%d)", id));
return webClient.get()
.uri("/user/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
Na próxima seção, aprenderemos como podemos chamar esse método simultaneamente.
4. Fazendo chamadasWebClient simultâneas
In this section, we’re going see several examples for calling our getUser method concurrently. Também daremos uma olhada nas implementações de editorFlux eMono nos exemplos.
4.1. Várias chamadas para o mesmo serviço
Let’s now imagine that we want to fetch data about five users simultaneously and return the result as a list of users:
public Flux fetchUsers(List userIds) {
return Flux.fromIterable(userIds)
.parallel()
.runOn(Schedulers.elastic())
.flatMap(this::getUser)
.ordered((u1, u2) -> u2.id() - u1.id());
}
Vamos decompor as etapas para entender o que fizemos:
-
Começamos criando umFlux de nossa lista deuserIds usando o métodofromIterable estático
-
Em seguida, chamamos o métodoparallel, que cria umParallelFlux –this indicates the simultaneous character of the execution
-
Neste exemplo, decidimos usarelasticscheduler para executar a chamada, mas fique à vontade para escolher outra configuração
-
Em seguida, invocamosflatMap para executar o métodogetUser que criamos anteriormente, que retornaParallelFlux
-
Em seguida, precisamos especificar como converterParallelFlux emFlux simples. Usaremos o métodoordered com o próprio comparador
É importante notar que, uma vez que as operações estão acontecendo em paralelo, não sabemos a ordem resultante, portanto, a API fornece o métodoordered.
4.2. Várias chamadas para serviços diferentes retornando o mesmo tipo
Let’s now take a look at how we can call multiple services simultaneously.
Neste exemplo, vamos criar outro endpoint que retorna o mesmo tipoUser:
public Mono getOtherUser(int id) {
return webClient.get()
.uri("/otheruser/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
Agora, o método para executar duas ou mais chamadas em paralelo se torna:
public Flux fetchUserAndOtherUser(int id) {
return Flux.merge(getUser(id), getOtherUser(id))
.parallel()
.runOn(Schedulers.elastic())
.ordered((u1, u2) -> u2.id() - u1.id());
}
The main difference in this example is that we’ve used the static method merge instead of the fromIterable method. Usando o método merge, podemos combinar dois ou maisFluxes em um resultado.
4.3. Várias chamadas para diferentes serviços Diferentes tipos
A probabilidade de ter dois serviços retornando a mesma coisa é bastante baixa. More typically we’ll have another service providing a different response type and our goal is to merge two (or more) responses.
A classeMono fornece o método zip estático que nos permite combinar dois ou mais resultados:
public Mono fetchUserAndItem(int userId, int itemId) {
Mono user = getUser(userId).subscribeOn(Schedulers.elastic());
Mono- item = getItem(itemId).subscribeOn(Schedulers.elastic());
return Mono.zip(user, item, UserWithItem::new);
}
Outro ponto importante a observar é que precisamos chamarsubscribeOn antes de passar os resultados para o métodozip.
However, the subscribeOn method does not subscribe to the Mono.
Ele especifica que tipo deScheduler usar quando a chamada de assinatura acontecer. Again we’re using the elastic scheduler in this example which ensures each subscription happens on a dedicated single thread.
The last step is to call the zip method which combines the given user and item Monos into a new Mono with the type UserWithItem. Este é um objeto POJO simples que envolve um usuário e um item.
5. Teste
Nesta seção, vamos ver como podemos testar o código que já vimos e, em particular, verificar se as chamadas de serviço estão acontecendo em paralelo.
Para isso, usaremosWiremock para criar um servidor simulado e testaremos o métodofetchUsers:
@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 userIds = 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);
}
Neste exemplo, a abordagem que adotamos é simular o serviço do usuário e fazê-lo responder a qualquer solicitação em um segundo. Now if we make five calls using our WebClient we can assume that it shouldn’t take more than two seconds as the calls happen concurrently.
Se dermos uma olhada mais de perto nos logs quando executamos nosso teste. Podemos ver que cada solicitação está acontecendo em um segmento diferente:
[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)
Para aprender sobre outras técnicas para testarWebClient, verifique nosso guia paraMocking a WebClient in Spring.
6. Conclusão
Neste tutorial, exploramos umfew ways we can make HTTP service calls simultaneously using the Spring 5 Reactive WebClient.
Primeiro, mostramos como fazer chamadas em paralelo ao mesmo serviço. Mais tarde, vimos um exemplo de como chamar dois serviços retornando tipos diferentes. Em seguida, mostramos como podemos testar esse código usando um servidor simulado.
Como sempre, o código-fonte deste artigo está disponívelover on GitHub.