How do I use CompletableFuture for async tasks?

CompletableFuture is a powerful tool in Java to implement asynchronous programming. It allows you to perform tasks in the background, chain multiple async tasks together, handle both success and failure scenarios, and combine multiple async computations.

Here’s a summary of how to use CompletableFuture with examples:


1. Run a task asynchronously

Use supplyAsync() (if the task produces a result) or runAsync() (if the task doesn’t return anything).

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Running task in background...");
});

Or with a result:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Hello, World!";
});

2. Process the result

You can process the result of a CompletableFuture using methods like thenAccept or thenApply.

future.thenApply(result -> {
    System.out.println("Received result: " + result);
    return result.toUpperCase();
}).thenAccept(uppercaseResult -> {
    System.out.println("Transformed result: " + uppercaseResult);
});

3. Combine multiple tasks

You can run multiple tasks in parallel and combine their results.

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task2");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (task1Result, task2Result) -> {
    return task1Result + " and " + task2Result;
});

combinedFuture.thenAccept(result -> {
    System.out.println("Combined Result: " + result);
});

4. Wait for all tasks

If you have multiple tasks and want to wait for all of them to complete, use CompletableFuture.allOf.

CompletableFuture<Void> allTasks = CompletableFuture.allOf(
    CompletableFuture.runAsync(() -> System.out.println("Task 1 completed")),
    CompletableFuture.runAsync(() -> System.out.println("Task 2 completed"))
);

allTasks.join(); // Blocks the thread and waits for completion
System.out.println("All tasks completed.");

5. Handle errors

You can handle exceptions in async tasks using exceptionally().

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    throw new RuntimeException("Oops, something went wrong!");
}).exceptionally(ex -> {
    System.out.println("Error: " + ex.getMessage());
    return null;
});

6. Compose dependent tasks

Use thenCompose to chain dependent tasks where the second task depends on the result of the first one.

CompletableFuture.supplyAsync(() -> "Task 1 Result")
    .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " Task 2 Result"))
    .thenAccept(finalResult -> System.out.println("Final Result: " + finalResult));

7. Custom Executor

By default, CompletableFuture uses the ForkJoinPool.commonPool for async tasks. You can provide a custom executor for better control over threads.

ExecutorService executor = Executors.newFixedThreadPool(10);

CompletableFuture.runAsync(() -> {
    System.out.println("Running task on custom executor");
}, executor);

Key Methods in CompletableFuture

Method Description
runAsync Run a task asynchronously (does not return result).
supplyAsync Run a task asynchronously and return a result.
thenApply Transform the result of a CompletableFuture.
thenAccept Consumes the result of a CompletableFuture (no further processing).
thenCompose Chains dependent tasks where the next uses the result of the previous.
thenCombine Combines two CompletableFuture results.
allOf / anyOf Wait for all or any of multiple futures to complete.
exceptionally Handle exceptions thrown during the async computation.
join / get Block and wait for the task to complete and retrieve its result (not recommended for non-blocking).

Example Workflow with Pipeline

Here’s an example pipeline:

  1. Fetch data
  2. Process it
  3. Save it
  4. Notify the user
ExecutorService executor = Executors.newFixedThreadPool(4);

CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching data...");
    return "Raw Data";
}, executor).thenApply(data -> {
    System.out.println("Processing data...");
    return data.toUpperCase();
}).thenAccept(processedData -> {
    System.out.println("Saving: " + processedData);
}).thenRun(() -> {
    System.out.println("Notification: Done!");
}).exceptionally(ex -> {
    System.err.println("Pipeline failed due to: " + ex.getMessage());
    return null;
}).join();

Using CompletableFuture, you can build flexible, high-performance, and non-blocking applications.

How do I use Java 11 HttpClient in a multithreaded environment?

When using the Java 11 HttpClient in a multithreaded environment, you need to ensure it is used in a thread-safe and efficient manner. Thankfully, the HttpClient introduced in Java 11 is designed to be thread-safe and can be shared among multiple threads without additional synchronization.

Here are the important points about using HttpClient in a multithreaded environment:


1. Create a Single HttpClient Instance

  • HttpClient is thread-safe, so create a single shared instance of HttpClient and reuse it across your application. Do not create a new instance of HttpClient for every request, as it can lead to performance inefficiencies.
  • Use the factory method HttpClient.newHttpClient() to create a default instance, or configure it with a custom builder.

Example:

package org.kodejava.net.http;

import java.net.http.HttpClient;
import java.time.Duration;

public class HttpClientBuilder {
    // Create a shared instance of HttpClient
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2) // Support HTTP/2
            .connectTimeout(Duration.ofSeconds(10)) // Set up a timeout
            .build();

    public static HttpClient getHttpClient() {
        return httpClient;
    }
}

2. Use Thread-Safe Methods

  • Use the methods send() (for synchronous requests) or sendAsync() (for asynchronous requests) provided by HttpClient. These methods are thread-safe and can be invoked concurrently by multiple threads.
  • You should handle response callbacks correctly in the case of asynchronous usage since they will be executed by the internal thread pool.

Synchronous Request Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class SynchronousRequest {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .build();

        // Using the shared HttpClient instance
        HttpResponse<String> response = HttpClientBuilder.getHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Response code: " + response.statusCode());
        System.out.println("Response body: " + response.body());
    }
}

Asynchronous Request Example:

package org.kodejava.net.http;

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

public class AsynchronousRequest {
    public static void main(String[] args) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .build();

        // Asynchronous call
        CompletableFuture<HttpResponse<String>> futureResponse =
                HttpClientBuilder.getHttpClient()
                        .sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Handle response when it is ready
        futureResponse.thenAccept(response -> {
            System.out.println("Response code: " + response.statusCode());
            System.out.println("Response body: " + response.body());
        }).join(); // Wait for the response to complete
    }
}

3. Internal Thread Pool

  • By default, the HttpClient uses an internal thread pool managed by a ForkJoinPool for asynchronous requests. You can customize this by providing your own Executor when configuring the HttpClient with the builder.

Example: Custom Executor

package org.kodejava.net.http;

import java.net.http.HttpClient;
import java.util.concurrent.Executors;

public class CustomExecutorExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClient.newBuilder()
                .executor(Executors.newFixedThreadPool(10))
                .build();

        // Use this HttpClient instance for requests
    }
}

4. Timeouts

  • Set a connection timeout at the HttpClient level using the connectTimeout method.
  • For individual requests, you can specify a request timeout.

Example:

package org.kodejava.net.http;

import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;

public class RequestWithTimeout {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                .timeout(Duration.ofSeconds(5)) // Set request-specific timeout
                .GET()
                .build();

        HttpResponse<String> response = HttpClientBuilder.getHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println("Response body: " + response.body());
    }
}

5. Handle Exceptions

  • Handle exceptions like timeouts, ConnectException, or server errors gracefully when making HTTP calls.
  • Use try-catch blocks for synchronous methods and handle() or exceptionally() for asynchronous methods.

Example:

package org.kodejava.net.http;

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

public class HandleExceptionsExample {
    public static void main(String[] args) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://invalid.url"))
                .GET()
                .build();

        CompletableFuture<HttpResponse<String>> futureResponse = HttpClientBuilder.getHttpClient()
                .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .exceptionally(e -> {
                    System.out.println("Request failed: " + e.getMessage());
                    return null;
                });

        futureResponse.thenAccept(response -> {
            if (response != null) {
                System.out.println("Response code: " + response.statusCode());
            }
        }).join();
    }
}

6. Handling Concurrency

  • In multithreaded scenarios, the HttpClient can process concurrent requests efficiently since it is non-blocking, especially when using sendAsync(), and does not require manual synchronization.
  • You can run multiple requests in parallel by delegating each request to a thread or submitting tasks to an ExecutorService.

Example: Multiple Requests in Parallel

package org.kodejava.net.http;

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

public class ParallelRequestsExample {
    public static void main(String[] args) {
        List<URI> uris = List.of(
                URI.create("https://jsonplaceholder.typicode.com/posts/1"),
                URI.create("https://jsonplaceholder.typicode.com/posts/2"),
                URI.create("https://jsonplaceholder.typicode.com/posts/3")
        );

        List<CompletableFuture<Void>> futures = uris.stream()
                .map(uri -> {
                    HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
                    return HttpClientBuilder.getHttpClient()
                            .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                            .thenAccept(response -> {
                                System.out.println("Response for " + uri + " : " + response.body());
                            });
                })
                .toList();

        // Wait for all requests to complete
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    }
}

Key Best Practices

  1. Reuse a single HttpClient instance for better resource management.
  2. Use asynchronous methods (sendAsync) for non-blocking and parallel requests.
  3. Configure timeouts to avoid hanging.
  4. Customize the thread pool if your application has specific concurrency requirements.
  5. Properly handle errors and exceptions in both synchronous and asynchronous requests.

By following these guidelines, you can effectively use HttpClient in a multithreaded environment.

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.

How do I use Java 11 HttpClient to send asynchronous requests?

In Java 11, the HttpClient API provides a modern and user-friendly way to send both synchronous and asynchronous HTTP requests. To send asynchronous requests, you’ll use the sendAsync method, which returns a CompletableFuture.

Here’s how to use it:


Step-by-Step Guide to Sending Asynchronous Requests:

  1. Initialize the HttpClient:
    Use HttpClient to create an instance. This is the central object for sending requests.
  2. Create an HttpRequest:
    Prepare your HTTP request using the HttpRequest class, where you can specify the URI, HTTP method, headers, body, etc.
  3. Send an Asynchronous Request with sendAsync:
    Call the sendAsync method of the HttpClient, passing the request and body handler as arguments. This returns a CompletableFuture, which allows you to perform non-blocking operations.
  4. Process the Response:
    Use the CompletableFuture chain methods, like thenApply and thenAccept, to process the response once it’s available.

Example: Sending an Asynchronous GET Request

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.time.Duration;
import java.util.concurrent.CompletableFuture;

public class AsyncHttpClientExample {

    public static void main(String[] args) {
        // Create HttpClient instance
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10)) // Optional timeout
                .build();

        // Prepare HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .header("Accept", "application/json")
                .build();

        // Send asynchronous request
        CompletableFuture<HttpResponse<String>> futureResponse =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Process response asynchronously
        futureResponse.thenApply(HttpResponse::body) // Extract the response body
                .thenAccept(System.out::println) // Print the body
                .exceptionally(ex -> {
                    System.err.println("Request failed: " + ex.getMessage());
                    return null;
                });

        // Do other tasks while the response is being fetched...
        System.out.println("Request is sent. Waiting for response...");

        // Wait until the response completes to prevent the program from exiting early
        futureResponse.join();
    }
}

Explanation:

  1. HttpClient.newBuilder():
    Creates a new instance of the HttpClient. You can optionally configure timeouts, proxies, or redirect policies.
  2. HttpRequest.newBuilder():
    Creates an HTTP request. You specify the URI, headers, and HTTP method (e.g., GET, POST, etc.).
  3. sendAsync:
    Sends the request asynchronously. It accepts two arguments:

    • The HttpRequest object.
    • A BodyHandler to determine how the HTTP response body should be handled, such as ofString() for plain text.
  4. CompletableFuture Chain:
    • thenApply: Manipulates the asynchronous result as it becomes available.
    • thenAccept: Consumes the result of the future once it’s ready.
    • exceptionally: Handles any exceptions that occur during execution.
  5. join():
    Blocks the main thread until the asynchronous operation is complete (used here to prevent premature termination of the program).

Example: Sending an Asynchronous POST Request with JSON Body

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.time.Duration;
import java.util.concurrent.CompletableFuture;

public class AsyncPostExample {

    public static void main(String[] args) {
        // Create HttpClient
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        // Prepare JSON body
        String jsonBody = "{ \"title\": \"foo\", \"body\": \"bar\", \"userId\": 1 }";

        // Create HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .header("Content-Type", "application/json")
                .build();

        // Send asynchronous POST request
        CompletableFuture<HttpResponse<String>> futureResponse =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Handle response
        futureResponse.thenApply(HttpResponse::body)
                .thenAccept(System.out::println)
                .exceptionally(ex -> {
                    System.err.println("Error: " + ex.getMessage());
                    return null;
                });

        // Keep the program running to wait for response
        System.out.println("POST request sent. Waiting for response...");
        futureResponse.join();
    }
}

Keynotes:

  • Thread-Safe HttpClient:
    The HttpClient instance is thread-safe and can be reused for multiple requests.
  • Non-blocking Nature:
    Asynchronous requests are non-blocking, so you can perform other tasks while waiting for the response.
  • Error Handling:
    Use the exceptionally method of the CompletableFuture to handle any errors during the request.
  • Keepalive:
    By default, HttpClient connections have keepalive enabled. It’s more efficient for high-performance applications.
  • Timeouts:
    Always configure timeouts to prevent indefinite blockage (connectTimeout or read timeouts).

Using this approach, you can efficiently perform asynchronous HTTP communication with HttpClient.

How do I cancel an ongoing HTTP request using Java 11 HttpClient?

In Java 11, the HttpClient API does not provide an explicit “cancel” method for ongoing HTTP requests, but you can use a java.util.concurrent.Future to achieve cancellation. When you send an asynchronous request using HttpClient in Java 11, the sendAsync method returns a CompletableFuture. You can call cancel on the CompletableFuture to attempt to cancel the ongoing HTTP operation.

Here’s how you can cancel an ongoing HTTP request:

Example Code

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 HttpClientCancelExample {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newHttpClient();

        // Create the HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/delay/5"))  // A 5-second delay URL
                .GET()
                .build();

        // Send the request asynchronously
        CompletableFuture<HttpResponse<String>> futureResponse =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Simulate a need to cancel the request
        try {
            // Wait 2 seconds
            Thread.sleep(2000);
            // Attempt to cancel the request
            boolean cancelResult = futureResponse.cancel(true);

            if (cancelResult) {
                System.out.println("Request was cancelled successfully.");
            } else {
                System.out.println("Request could not be cancelled.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Handle the original CompletableFuture response (if not cancelled)
        futureResponse.whenComplete((response, throwable) -> {
            if (throwable != null) {
                System.out.println("Request was cancelled or completed with error: " + throwable.getMessage());
            } else {
                System.out.println("Response received: " + response.body());
            }
        });
    }
}

Explanation

  1. Create an Asynchronous Request: Use HttpClient.sendAsync to initiate the HTTP request asynchronously. This returns a CompletableFuture.
  2. Cancel the Request: Use CompletableFuture.cancel(true) to cancel the request. The true parameter interrupts the thread performing the computation if it is running.
  3. Handle Cancellation or Completion: Use whenComplete on the CompletableFuture to handle the scenario where the operation is either cancelled or successfully completed.
  4. Resource Cleanup: If cancellation succeeds, you should be aware that the underlying network connection may not always terminate immediately, as it depends on the implementation.

Things to Note

  1. Cancellation Result: The cancel method returns true if the cancellation was successful and false if the task could not be cancelled, such as if it was already completed.
  2. Non-blocking Behavior: This approach is non-blocking and makes use of asynchronous programming.