How do I chain asynchronous calls using Java 11 HttpClient and CompletableFuture?

Chaining asynchronous calls using Java 11’s HttpClient and CompletableFuture can be achieved by leveraging the reactive capabilities of CompletableFuture. The sendAsync method of HttpClient supports asynchronous processing, and you can chain multiple calls together using methods like thenApply, thenCompose, or thenAccept. Here’s a step-by-step example:


Key Concepts Used:

  1. CompletableFuture:
    • Allows for async processing and chaining of dependent tasks.
  2. HttpClient and HttpRequest:
    • The async calls are made using the HttpClient.sendAsync method.
  3. Chaining methods:
    • Use thenApply to transform the response or thenCompose to chain dependent async calls.

Example: Chaining Multiple HTTP Requests

Say we need to:

  1. Fetch data using one API.
  2. Use the response data to make another API call.
  3. Process the final response.

Here’s how you can do that:

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class AsyncChainingExample {

   public static void main(String[] args) {
      HttpClient client = HttpClient.newHttpClient();

      // First API Request
      HttpRequest firstRequest = HttpRequest.newBuilder()
              .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
              .GET()
              .build();

      // First Async Call
      CompletableFuture<Void> future = client.sendAsync(firstRequest, HttpResponse.BodyHandlers.ofString())
              .thenApply(HttpResponse::body) // Extract body from response
              .thenCompose(body -> {
                 System.out.println("First API Response: " + body);

                 // Use data from the first response to make the second API request
                 String secondApiUri = "https://jsonplaceholder.typicode.com/comments?postId=1";
                 HttpRequest secondRequest = HttpRequest.newBuilder()
                         .uri(URI.create(secondApiUri))
                         .GET()
                         .build();

                 return client.sendAsync(secondRequest, HttpResponse.BodyHandlers.ofString());
              })
              .thenApply(HttpResponse::body) // Extract body from second response
              .thenAccept(secondResponse -> {
                 // Final result processing
                 System.out.println("Second API Response: " + secondResponse);
              });

      // Wait for all the tasks to complete
      future.join();
   }
}

Explanation of the Code:

  1. Create the HttpClient:
    • HttpClient.newHttpClient() initializes the HTTP client that will send requests asynchronously.
  2. First API Call:
    • The first API request (firstRequest) is created using HttpRequest.newBuilder.
    • Send the request asynchronously with:
    client.sendAsync(firstRequest, HttpResponse.BodyHandlers.ofString());
    
    • thenApply is used to extract the body of the response.
  3. Second API Call (Chained):
    • In thenCompose, the code prepares and sends the second API request. This ensures that the second API call happens only after the first call completes.
    • The response of this call is again processed by extracting the body.
  4. Response Processing:
    • thenAccept is used at the end of the chain to process the final response.
  5. Waiting for Completion:
    • Since the operations are asynchronous, future.join() blocks the main thread until all the chained calls complete.

Output:

Sample output from the above example (when run):

First API Response: {
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati",
  "body": "quia et suscipit..."
}

Second API Response: [
  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    ...
  },
  ...
]

Key Functions Used in the Chain:

  1. thenApply(Function)
    • Transforms the result of the previous step (e.g., extract the body).
  2. thenCompose(Function)
    • Used for dependent async calls. Ensures one CompletableFuture waits for another.
  3. thenAccept(Consumer)
    • Consumes the result without returning anything.

Advantages of this Approach:

  • No need for manual thread management.
  • Non-blocking I/O.
  • Easily scalable chaining of async calls.

This is a modern, clean solution for handling asynchronous HTTP requests in Java using HttpClient and CompletableFuture.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.