Learnitweb

HttpClient in Java 11

1. Introduction

The HTTP Client was added in Java 11. This class was introduced to replace the legacy HttpUrlConnection class. Due to the issues with HttpUrlConnection, programmers used third-party libraries such as Apache HttpClient and Jetty.

HttpClient is an abstract class available in java.net.http module and java.net.http package. This class represents an HTTP Client. An HttpClient can be used to send requests and retrieve their responses.

HttpClient can be used to request HTTP resources over the network. HttpClient supports both HTTP/1.1 and HTTP/2. HttpClient can be used to make both synchronous and asynchronous calls. It can also handle request and response bodies as reactive-streams.

Once created HttpClient is immutable, and can be used to send multiple requests.

The Java HTTP Client supports both HTTP/1.1 and HTTP/2. By default, when making requests, the client will utilize HTTP/2. If the server does not support HTTP/2, the requests will automatically revert to HTTP/1.1.

Here’s an overview of the key enhancements introduced by HTTP/2:

  • Header Compression: HTTP/2 employs HPACK compression, minimizing overhead.
  • Single Connection to the server: Reduces the necessity for numerous TCP connections to be established, thereby cutting down on round trips.
  • Multiplexing: Allows multiple requests concurrently on a single connection.
  • Server Push: Permits the server to proactively send additional required resources to the client.
  • Binary format: Offers a more concise representation.

Following are some important points to note about this change:

  • The change was implemented as a part of JEP 321. The new HTTP API was incubated in Java 9. This new API in now included in Java SE API. The new HTTP APIs can be found in java.net.HTTP.*.
  • The new HTTP API now supports asynchronous. Asynchronous calls are implemented using CompletableFuture.
  • The core classes and interface providing the core functionality include:
    The HttpClient class, java.net.http.HttpClient
    The HttpRequest class, java.net.http.HttpRequest
    The HttpResponse interface, java.net.http.HttpResponse
    The WebSocket interface, java.net.http.WebSocket

A BodyHandler must be provided for each HttpRequest sent. A BodyHandler is a handler for response bodies.
Requests can be sent either synchronously or asynchronously. There are two methods to send request synchronously and asynchronously:

  • send​(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler): Sends the given request using the client, blocking if necessary to get the response.
  • sendAsync​(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler): Sends the given request asynchronously using the client with the given response body handler. The sendAsync method returns immediately with a CompletableFuture. The CompletableFuture completes when the response becomes available.

Security checks are performed by the HTTP Client’s sending methods if a security manager is present.

Before sending a request, HttpClient need to be created. HttpClient supports the builder pattern. An HttpClient is created through a builder. The Builder can be used to configure state of client like connection timeout, preferred protocol version (HTTP/1.1 OR HTTP2), authenticator, proxy, follow redirects etc.

HttpClient client = HttpClient.newBuilder()
      .version(Version.HTTP_2)
      .followRedirects(Redirect.NORMAL)
      .proxy(ProxySelector.of(new InetSocketAddress("www.learnitweb.com", 8080)))
      .authenticator(Authenticator.getDefault())
      .build();

2. HttpRequest

An HttpRequest instance is built through an HttpRequest builder. HttpRequest can be used to set a request’s URI, headers, and body. Builders can be reused by copying and modifying many times when required in order to build multiple related requests that differ in some parameters.

HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://learnitweb.com/"))
      .timeout(Duration.ofMinutes(1))
      .header("Content-Type", "application/json")
      .POST(BodyPublishers.ofFile(Paths.get("file.json")))
      .build()

3. Setting Executor for Asynchronous Calls

We can also define an Executor that provides threads to be used by asynchronous calls.

This way we can, for example, set the limit on the number of threads used for processing requests:

ExecutorService executorService = Executors.newFixedThreadPool(2);

CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
  .executor(executorService)
  .build()
  .sendAsync(request, HttpResponse.BodyHandlers.ofString());

CompletableFuture<HttpResponse<String>> response2 = HttpClient.newBuilder()
  .executor(executorService)
  .build()
  .sendAsync(request, HttpResponse.BodyHandlers.ofString());

4. Setting a CookieHandler

We can use builder method cookieHandler(CookieHandler cookieHandler) to define client-specific CookieHandler.

  HttpClient.newBuilder()
  .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
  .build();

5. Setting a Proxy

We can define a proxy for the connection by just calling proxy() method on a Builder instance:

  HttpResponse<String> response = HttpClient
  .newBuilder()
  .proxy(ProxySelector.getDefault())
  .build()
  .send(request, BodyHandlers.ofString());

In this example, we used the default system proxy.

6. Setting a Request Body

We can add a body to a request by using the request builder methods: POST(BodyPublisher body), PUT(BodyPublisher body) and DELETE(). The new API provides a number of BodyPublisher implementations:

  • StringProcessor – One of the overloaded method is ofString​(String body). This method returns a request body publisher whose body is the given String, converted using the UTF_8 character set.
  • InputStreamProcessor – One of the overloaded method is ofInputStream​(Supplier streamSupplier) – A request body publisher that reads its data from an InputStream.
  • ByteArrayProcessor – One of the overloaded method is ofByteArray​(byte[] buf) . This method returns a request body publisher whose body is the given byte array.
  • FileProcessor – reads body from a file at the given path, created with HttpRequest.BodyPublishers.ofFile.
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://someapi-endpoint"))
  .POST(HttpRequest.BodyPublishers.noBody())
  .build();

7. Setting headers

In case we want to add additional headers to our request, we can use the provided builder methods. Headers are passed as key-value pairs.

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://some-endpoint"))
  .headers("key1", "value1", "key2", "value2")
  .GET()
  .build();

8. Synchronous API call example

Following is an example of an API call using HttpClient.

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

public class HttpClientExample {
    public static void main(String args[]) {
        //Create an instance of HttpClient
        HttpClient httpClient = HttpClient.newHttpClient();

        // Define the URL you want to send the GET request to
        String url = "https://jsonplaceholder.typicode.com/todos/1";

        // Create a HttpRequest object for the GET request
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

        try {
            // Send the GET request synchronously and capture the response
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            // Print the response status code
            System.out.println("Response Status Code: " + response.statusCode());

            // Print the response body
            System.out.println("Response Body: " + response.body());
        } catch (Exception e) {
            // Handle any exceptions
            e.printStackTrace();
        }
    }
}

Output

Response Status Code: 200
Response Body: {
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

9. Asynchronous call

Let us modify the earlier example for asynchronous call.

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 AsyncHttpClientExample {
    public static void main(String[] args) {
        // Create an instance of HttpClient
        HttpClient httpClient = HttpClient.newHttpClient();

        // Define the URL you want to send the GET request to
        String url = "https://jsonplaceholder.typicode.com/todos/1";

        // Create a HttpRequest object for the GET request
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

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

        // Handle the response when it becomes available
        futureResponse.thenAccept(response -> {
            // Print the response status code
            System.out.println("Response Status Code: " + response.statusCode());

            // Print the response body
            System.out.println("Response Body: " + response.body());
        }).join(); // Wait for the asynchronous operation to complete
    }	
}

We use httpClient.sendAsync() to send the GET request asynchronously. This method returns a CompletableFuture<HttpResponse<String>>, which represents a future completion of the HTTP request.
We use futureResponse.thenAccept() to specify a callback function to handle the response when it becomes available. Inside the callback, we print the response status code and body.

10. Get Response details

We’ll update the code to get the response code, response headers, response version and URI of response object. Other part of the program in omitted for brevity.

// Handle the response when it becomes available
        futureResponse.thenAccept(response -> {
            // Print the response status code
            System.out.println("Response Status Code: " + response.statusCode());

            // Print the response headers
            System.out.println("Response Headers:");
            Map<String, List<String>> headers = response.headers().map();
            headers.forEach((name, value) -> System.out.println(name + ": " + value));

            // Print the response version
            System.out.println("Response Version: " + response.version());

            // Print the URI of the response object
            System.out.println("Response URI: " + response.uri());

            // Print the response body
            System.out.println("Response Body: " + response.body());
        }).join(); // Wait for the asynchronous operation to complete

11. Conclusion

In conclusion, Java 11’s HttpClient offers a powerful and versatile solution for handling HTTP requests and responses in your applications. Throughout this tutorial, we’ve delved into its various features, from simple API calls to more advanced functionalities like asynchronous execution and handling of redirects.

Furthermore, HttpClient’s support for modern protocols and standards, such as HTTP/2 and WebSocket, ensures compatibility with the evolving landscape of web technologies. Its flexibility in configuring connection management, timeouts, and proxy settings enables developers to tailor their network interactions according to specific requirements.