Reputation: 139
The issue I need to solve (in Java) is:
games
from an API. games
N times:
gamesWithDetails
array.gamesWithDetails
.I cannot fetch the details of all the games with a single request, I have to hit the API endpoint every time per game. So I want to execute these requests asynchronously from each other.
This is a working example in JavaScript in case it's useful. However I'd like to make it work for 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));
});
})
Upvotes: 4
Views: 3777
Reputation: 3403
Quite interesting question as JavaScript implements the famous event loop which means its functions are asynchronous and non-blocking. Spring Boot restTemplate
class will block the thread of execution until the response is back, therefore wasting a lot of resources (the one thread per request model).
@Slacky's answer is technically right as you asked about asynchronous HTTP requests but I'd like to share a better option which is both asynchronous and non-blocking, meaning a single thread is able to handle 100s or even 1000s of requests and their responses (reactive programming).
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.
It is also worth mentioning that Java being statically typed requires you to declare classes to represent your data, in this case something like (using Lombok for brevity):
@Data
class Match {
private String gameId;
// ...
}
@Data
class MatchDetails {
// ...
}
Here is the code following @Slacky's answer naming convention to make the comparison easier.
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
}
}
Upvotes: 2
Reputation: 147
Basically you will want to do something like this:
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();
}
}
}
Please note that it is not a refined code, but just a quick example of how to achieve this. Ideally you would replace that JSON processing that I've done "by hand" using Map by a response bean that matches the structure of the response you get from the service you are calling.
A quick walk through:
String responseAsString = restTemplate.getForObject(ACCOUNT_URI_BASE + accountId, String.class);
This executes the first REST request and gets it as a String (the JSON response). You will want to properly map this using a Bean object instead. Then this is processed using the ObjectMapper provided by Jackson and transformed into a map so you can navigate the JSON and get the 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());
Once we have all the matches we will use the Stream API to transform them into CompletableFutures that will be executed asynchronously. Each thread will make another request in order to get the response for each individual matchId.
System.out.println(r);
This will be executed for each response that you get for each matchId, just like in your example. This should also be replaced by a proper bean matching the output for clearer processing.
Note that List<CompletableFuture<Void>> futures
only "holds the code" but will not get executed until we combine everything in the end using CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
and execute the blocking get()
method.
Upvotes: 4