How to Make Multiple REST Calls in CompletableFuture
Parallelism, where multiple tasks are executed simultaneously, can significantly improve performance by reducing the overall wait time for all the tasks to be completed. Let us delve into understanding Java’s CompletableFuture for asynchronous programming that allows for efficient management of parallel tasks. We will demonstrate how to perform multiple REST calls using the CompletableFuture class in a series of examples.
1. Introduction
Making multiple REST calls sequentially can be inefficient and slow, especially when dealing with latency and network delays. In modern applications, especially those that rely heavily on external APIs, it’s common to have scenarios where multiple REST calls need to be made. Waiting for each call to complete before starting the next one is not optimal and can lead to significant performance bottlenecks.
2. Why Use Parallelism in REST Calls?
REST (Representational State Transfer) is a common architecture style for designing networked applications. It uses standard HTTP methods and allows for the integration of different systems and platforms. When making multiple REST calls, such as fetching data from different endpoints or services, doing so sequentially can lead to long wait times, especially if some endpoints are slower than others.
By using parallelism, we can initiate all REST calls simultaneously. This approach reduces the overall waiting time since we are not waiting for one call to finish before starting the next. This can be particularly beneficial in scenarios such as:
- Aggregating data from multiple sources to provide a comprehensive response.
- Fetching data that can be processed independently.
- Improving the responsiveness of applications by reducing the latency experienced by users.
Parallelism can also lead to better resource utilization, as it allows for making full use of available CPU and network bandwidth.
3. Using CompletableFuture for Parallelism
CompletableFuture
is a class introduced in Java 8 that supports asynchronous programming. It provides a flexible way to create and combine asynchronous tasks, allowing for complex workflows to be constructed in a more readable and maintainable manner. The CompletableFuture
class offers a variety of methods to run tasks asynchronously and to handle their results once they are complete. Let’s delve into a step-by-step example of making multiple REST calls using CompletableFuture
:
package com.jcg.example; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; public class ParallelRestCalls { private static final HttpClient client = HttpClient.newHttpClient(); public static CompletableFuture<String> fetch(String url) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .build(); return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body); } public static void main(String[] args) throws ExecutionException, InterruptedException { List<String> urls = Arrays.asList( "https://api.example.com/resource1", "https://api.example.com/resource2", "https://api.example.com/resource3" ); List<CompletableFuture<String>> futures = urls.stream() .map(ParallelRestCalls::fetch) .collect(Collectors.toList()); CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); CompletableFuture<List<String>> allResponses = allOf.thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()) ); List<String> responses = allResponses.get(); responses.forEach(System.out::println); } }
The code defines a:
- fetch: This method takes a URL and returns a
CompletableFuture<String>
representing the asynchronous HTTP request. It builds an HTTP request and sends it asynchronously usingHttpClient.sendAsync
. ThethenApply
method is used to process the response and extract the body as a string. - Main Method:
urls
: A list of URLs to be fetched.futures
: A stream ofCompletableFuture
objects, each representing an asynchronous fetch operation for a URL.allOf
: A combined future that completes when all the given futures complete. This is achieved usingCompletableFuture.allOf
, which takes an array of futures and returns a new future that is completed when all of the given futures are complete.allResponses
: A future representing a list of responses. ThethenApply
method is used to transform the result ofallOf
into a list of response strings by joining each future in thefutures
list.responses
: The final list of responses after all futures have been completed. This is obtained by callingallResponses.get()
, which blocks until the future is completed and then returns the result.
4. Handling Errors
Error handling is crucial in any asynchronous operation, as failures can occur at various stages of the process. With CompletableFuture
, we can handle errors using the exceptionally
or handle
methods. The exceptionally
method allows you to provide a fallback result or take some action if an exception occurs. Here’s how we can update the fetch
method to include error handling:
public static CompletableFuture<String> fetch(String url) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .build(); return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .exceptionally(ex -> "Error: " + ex.getMessage()); }
In this version, if an exception occurs during the HTTP request, the exceptionally
method handles it and returns an error message. The handle
method can be used for more sophisticated error handling. It allows you to handle both the result and the exception in one place:
public static CompletableFuture<String> fetch(String url) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .build(); return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .handle((response, ex) -> { if (ex != null) { return "Error: " + ex.getMessage(); } return response.body(); }); }
In this version, the handle
method checks if an exception occurred. If so, it returns an error message; otherwise, it returns the response body. Using these error-handling techniques ensures that your application can gracefully handle failures in individual REST calls without affecting the overall operation.
5. Conclusion
Using CompletableFuture
for making multiple REST calls in parallel can significantly enhance the performance of your applications by reducing wait times. By leveraging parallelism, you can make better use of system resources and provide a more responsive experience to users. CompletableFuture
also provides robust error-handling mechanisms, ensuring that your application can handle failures gracefully. This makes it a powerful tool for building resilient and efficient asynchronous workflows.
In summary, the key benefits of using CompletableFuture
for parallel REST calls include:
- Improved performance by reducing overall wait times.
- Better resource utilization by executing tasks simultaneously.
- Robust error handling capabilities.
- Enhanced code readability and maintainability through a declarative programming style.
By adopting these techniques, you can build more efficient and responsive applications that are better equipped to handle the demands of modern software development.