簡體   English   中英

Java 和/或 Spring 引導中的並發異步 HTTP 請求

[英]Concurrent asynchronous HTTP requests in Java and/or Spring Boot

我需要解決的問題(在 Java 中)是:

  1. 從 API 獲取一系列games
  2. 迭代games N 次:
    • (異步/並發)
      1. 為每個游戲發出單獨的 HTTP 請求以獲取其詳細信息。
      2. 將其詳細信息存儲在gamesWithDetails數組中。
  3. 完畢。 我有我的數組gamesWithDetails

我無法通過單個請求獲取所有游戲的詳細信息,每次游戲時我都必須點擊 API 端點。 所以我想彼此異步執行這些請求。

這是JavaScript 中的一個工作示例,以防它有用。 但是我想讓它適用於 Spring 引導。

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));
      });

  })

基本上你會想做這樣的事情:

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();
        }
    }
}

請注意,這不是一個精煉的代碼,而只是一個如何實現這一點的快速示例。 理想情況下,您可以將我使用 Map “手動”完成的 JSON 處理替換為與您從調用的服務獲得的響應結構相匹配的響應 bean。

快速瀏覽:

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

這將執行第一個 REST 請求並將其作為字符串獲取(JSON 響應)。 您將希望使用 Bean object 來正確 map 。 然后使用 Jackson 提供的 ObjectMapper 對其進行處理,並將其轉換為 map,以便您可以導航 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 獲得的每個響應執行,就像在您的示例中一樣。 這也應該替換為與 output 匹配的適當 bean,以進行更清晰的處理。

請注意, List<CompletableFuture<Void>> futures僅“保存代碼”,但在我們最終使用CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();組合所有內容之前不會執行。 並執行阻塞的get()方法。

非常有趣的問題,因為 JavaScript 實現了著名的事件循環,這意味着它的功能是異步非阻塞的 Spring Boot restTemplate class 將阻塞執行線程,直到響應返回,因此浪費了大量資源(每個請求一個線程模型)。

@Slacky 的回答在技術上是正確的,因為您詢問了異步 HTTP 請求,但我想分享一個更好的選擇,它既是異步的又是非阻塞的,這意味着單個線程能夠處理 100 甚至 1000 的請求及其響應(反應式編程)。

The way to implement in Spring Boot the equivalent to your JavaScript example is to use the Project Reactor WebClient class which is a non-blocking, reactive client to perform HTTP requests.

還值得一提的是,靜態類型的 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
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM