Параллельные асинхронные HTTP-запросы в Java и / или Spring Boot - PullRequest
1 голос
/ 03 мая 2020

Проблема, которую мне нужно решить (в Java):

  1. Извлечение массива games из API.
  2. Итерации по games N раз:
    • (асинхронный c / одновременный)
      1. Создайте индивидуальный HTTP-запрос для каждой игры, чтобы получить ее детали.
      2. Сохранить сведения об этом в массиве gamesWithDetails.
  3. Готово. У меня есть мой массив gamesWithDetails.

. Я не могу получить информацию обо всех играх одним запросом, мне приходится каждый раз достигать конечной точки API в каждой игре. Поэтому я хочу выполнять эти запросы асинхронно друг от друга.

Это рабочий пример в JavaScript на случай, если это будет полезно. Однако я бы хотел, чтобы это работало для Spring Boot.

axios.get(`https://la2.api.riotgames.com/lol/match/v4/matchlists/by-account/${data.accountId}`, {
  headers: { "X-Riot-Token": "asdasdasdasdadasdasdasd"}
})
  .then(resp => {
    const promises = [];

    for ( match of resp.data.matches ) {
      promises.push(
        axios.get(`https://la2.api.riotgames.com/lol/match/v4/matches/${match.gameId}`, {
          headers: { "X-Riot-Token": "asdasdasdasdasdasdasdasd"}
        })
      )
    }

    Promise.all(promises)
      .then(matchesDetails => {
        matchesDetails.forEach(({ data }) => console.log(data.gameId));
      });

  })

Ответы [ 2 ]

1 голос
/ 03 мая 2020

По сути, вы захотите сделать что-то вроде этого:

package com.example.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

public class GamesProcessor {
    private static final String GAME_URI_BASE = "https://la2.api.riotgames.com/lol/match/v4/matches/";
    private static final String ACCOUNT_URI_BASE = "https://la2.api.riotgames.com/lol/match/v4/matchlists/by-account/";
    private Executor executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1);

    @Autowired
    private RestTemplate restTemplate;

    public void processGames(String accountId) throws JsonProcessingException, ExecutionException, InterruptedException {
        String responseAsString = restTemplate.getForObject(ACCOUNT_URI_BASE + accountId, String.class);
        ObjectMapper objectMapper = new ObjectMapper();

        if (responseAsString != null) {
            Map<String, Object> response = objectMapper.readValue(responseAsString, new TypeReference<Map<String, Object>>() {
            });

            List<Map<String, Object>> matches = (List<Map<String, Object>>) ((Map<String, Object>) response.get("data")).get("matches");

            List<CompletableFuture<Void>> futures = matches.stream()
                    .map(m -> (String) m.get("gameId"))
                    .map(gameId -> CompletableFuture.supplyAsync(() -> restTemplate.getForObject(GAME_URI_BASE + gameId, String.class), executor)
                            .thenAccept(r -> {
                                System.out.println(r); //do whatever you wish with the response here
                            }))
                    .collect(Collectors.toList());

            // now we execute all requests asynchronously
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
        }
    }
}

Обратите внимание, что это не улучшенный код, а просто быстрый пример того, как этого добиться. В идеале вы должны заменить JSON обработку, которую я выполнял "вручную" с использованием Map, на компонент EJB, который соответствует структуре ответа, получаемого от вызываемой вами службы.

A быстрый просмотр:

String responseAsString = restTemplate.getForObject(ACCOUNT_URI_BASE + accountId, String.class);

Выполняет первый запрос REST и получает его в виде строки (ответ JSON). Вы захотите правильно отобразить это, используя объект Bean. Затем это обрабатывается с использованием ObjectMapper, предоставленного Джексоном, и преобразуется в карту, чтобы вы могли перемещаться по JSON и получать совпадения.

List<CompletableFuture<Void>> futures = matches.stream()
                    .map(m -> (String) m.get("gameId"))
                    .map(gameId -> CompletableFuture.supplyAsync(() -> restTemplate.getForObject(GAME_URI_BASE + gameId, String.class), executor)
                            .thenAccept(r -> {
                                System.out.println(r); //do whatever you wish with the response here
                            }))
                    .collect(Collectors.toList());

Как только у нас будут все совпадения, мы будем использовать Stream API для преобразовать их в CompletableFutures, которые будут выполняться асинхронно. Каждый поток сделает другой запрос, чтобы получить ответ для каждого отдельного matchId.

System.out.println(r);

Это будет выполняться для каждого ответа, который вы получаете для каждого matchId, как в вашем примере. Это также должно быть заменено соответствующим bean-компонентом, соответствующим выходу для более четкой обработки.

Обратите внимание, что List<CompletableFuture<Void>> futures только "содержит код", но не будет выполняться, пока мы не объединим все в конце, используя CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); и выполнить метод блокировки get().

0 голосов
/ 05 мая 2020

Довольно интересный вопрос, поскольку JavaScript реализует известное событие l oop, что означает, что его функции асинхронны и неблокируют . Spring Boot restTemplate class будет блокировать поток выполнения до тех пор, пока ответ не вернется, поэтому тратится много ресурсов (один поток на модель запроса).

@ Ответ Слаки технически верен, когда вы спрашивали об асинхронном HTTP-запросы, но я бы хотел поделиться лучшим вариантом, который является как асинхронным, так и неблокирующим, то есть один поток может обрабатывать 100 или даже 1000 запросов и их ответов ( реактивное программирование ).

Способ реализации в Spring Boot, эквивалентный вашему JavaScript примеру, состоит в использовании класса Project Reactor WebClient, который является неблокирующим реактивным клиентом для выполнения HTTP-запросов.

Стоит также отметить, что для статической типизации Java требуется, чтобы вы объявили классы для представления ваших данных, в данном случае что-то вроде (для краткости используется Lombok):

@Data
class Match {
    private String gameId;
    // ...
}

@Data
class MatchDetails {
    // ...
}

Здесь код, следующий за соглашением об именах ответов @ Slacky, чтобы упростить сравнение.

public class GamesProcessor {
    private static final String BASE_URL = "https://la2.api.riotgames.com";
    private static final String GAME_URI = "/lol/match/v4/matches/%s";
    private static final String ACCOUNT_URI = "/lol/match/v4/matchlists/by-account/%s";

    public static List<MatchDetails> processGames(String accountId) { 
        final WebClient webClient = WebClient
            .builder()
            .baseUrl(BASE_URL)
            .defaultHeader("X-Riot-Token", "asdasdasdasdadasdasdasd") 
            .build();   

        // Issues the first request to get list of matches
        List<Match> matches = webClient
            .get()
            .uri(String.format(ACCOUNT_URI, accountId))
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<List<Match>>() {})
            .block(); // blocks to wait for response

        // Processes the list of matches asynchronously and collect all responses in a list of matches details
        return Flux.fromIterable(matches)
            .flatMap(match -> webClient
                    .get()
                    .uri(String.format(GAME_URI, match.getGameId()))
                    .accept(MediaType.APPLICATION_JSON)
                    .retrieve()
                    .bodyToMono(MatchDetails.class))
            .collectList()
            .block();  // Blocks to wait for all responses
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...