1. Overview
In this lesson, we’ll explore the modern Standardized HTTP Client that was introduced as an incubator feature in Java 9 and became a standard part of Java in version 11.
This new API provides a clean, fluent, and powerful way to handle HTTP requests, replacing the old HttpURLConnection API.
The relevant module we need to import when starting this lesson is: httpclient-start.
If we want to reference the fully implemented lesson, we can import: httpclient-end.
2. The Old Way: Limitations of HttpUrlConnection
Before the new HTTP Client, our primary built-in tool for making HTTP calls was the HttpURLConnection class. This API, which dates back to the early days of Java, remains part of the JDK.
While the new HttpClient is the recommended choice for modern development, the old HttpURLConnection hasn’t been deprecated. It remains in the platform primarily for backward compatibility, as it’s deeply embedded in many existing applications and older libraries.
However, it has several major drawbacks that led to the creation of the new API.
First, it’s low-level, verbose, and requires a lot of boilerplate code for setup and configuration. Also, it’s blocking by design, making it inefficient for applications that need to handle many concurrent connections. Finally, its API isn’t fluent or intuitive, making it difficult to work with.
Because of these limitations, there are very few cases where we’d choose to use HttpURLConnection for new code. The new HttpClient was designed from the ground up to solve these problems, offering a modern API that supports both synchronous and asynchronous operations, as well as modern protocols like HTTP/2 and WebSockets.
3. The New HTTP Client
The new HTTP Client API is primarily composed of three main classes:
- HttpClient: The class responsible for sending requests and receiving responses. We build a client once (it’s immutable and thread-safe) and can reuse it for many requests. We typically build it using its HttpClient.newBuilder() method.
- HttpRequest: This represents the request we want to send. We build it using HttpRequest.newBuilder(), specifying the URI, the method (GET, POST, etc.), and any headers or body.
- HttpResponse: This represents the response received from the server. It contains the status code, headers, and the response body.
One of the major advantages of the new HTTP Client is its built-in support for HTTP/2. By default, the client automatically negotiates HTTP/2 with the server and falls back to HTTP/1.1 if HTTP/2 isn’t supported.
4. Performing Synchronous Requests
A synchronous request is the simplest form of API call. Our code sends the request and blocks, waiting until the response is fully received before continuing. This is useful for simple scripts or applications where we need the data immediately.
To make a synchronous request, we first need a client. We can create a client using the HttpClient.newHttpClient() factory method. It provides a default client with standard settings.
Let’s add a new test to our NewJavaFeaturesUnitTest class to demonstrate a synchronous GET request by calling https://postman-echo.com/get, a helpful service that echoes request information:
@Test
void whenRetrievingData_thenParametersAreIncludedInResponse() throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/get?param=value")).build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("param=value"));
}
In the code above, after creating a client object, we build an HttpRequest, specifying the target URI.
Next, we send the request using the client.send() method. This method takes the HttpRequest and a HttpResponse.BodyHandler as arguments.
The body handler tells the client what to do with the response body. A common choice is HttpResponse.BodyHandlers.ofString(), which converts the response body into a String.
The send() method returns an HttpResponse object, which we then query for its statusCode() and body().
Other useful built-in handlers include BodyHandlers.ofFile() (to download a file), BodyHandlers.ofByteArray() (for binary data), and BodyHandlers.discarding() (if we only care about the status code).
A good practice is to check this status code to ensure it’s in the 200 (OK) range before trying to process the body(). In a real-world application, “processing the body” often means parsing it from a format like JSON into our Java domain objects. For example, we might use a library like Jackson to convert the JSON string from response.body() into a Task object.
5. Performing Asynchronous Requests
The real power of the new HTTP Client is its native support for asynchronous, non-blocking requests. This is essential for modern, high-performance applications.
Let’s add an asynchronous GET request test to the NewJavaFeaturesUnitTest class:
@Test
void whenRetrievingData_thenResponseIsHandledAsynchronously() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/get")).build();
CompletableFuture<HttpResponse<String>> futureResponse =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
CompletableFuture<String> futureBody = futureResponse.thenApply(HttpResponse::body);
CompletableFuture<Void> assertionFuture = futureBody
.thenAccept(body -> assertTrue(body.contains("\"host\":\"postman-echo.com\"")));
assertionFuture.join();
}
Here, instead of the send() method, we use the sendAsync() method. This method returns immediately with a CompletableFuture<HttpResponse>.
A CompletableFuture is a standard feature of Java’s concurrency API (introduced in Java 8) that represents a future result of an asynchronous computation. It’s a “promise” that the HTTP response will be available later, without our main thread having to wait for it. We can then attach “callbacks” to the CompletableFuture to handle the response when it arrives.
Then, we use thenApply() to transform the HttpResponse into its body(), and thenAccept() to perform a final action, like running our assertions. At the very end, we call join(). This waits for the asynchronous pipeline to complete, which is necessary in a test to ensure the test doesn’t finish before our assertions run.
6. Sending a POST Request
The HttpClient class also makes it easy to send data, such as a JSON payload, using a POST request.
Let’s add a final test to send a JSON representation of a Task class:
@Test
void whenCreatingTask_thenTaskIsCreatedSuccessfully() throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
String jsonBody = "{\"code\":\"task-1\", \"name\":\"Test Task\", \"description\":\"Test POST\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("Test POST"));
}
In the code above, we use the POST() method in the HttpRequest builder. This method takes a BodyPublisher. The HttpRequest.BodyPublishers.ofString() is a simple way to convert a String (like our JSON) into a request body. For simplicity, we use a synchronous approach.
In addition to POST(), the builder also provides a PUT() method that works similarly, as well as a DELETE() method. For any other HTTP verb, we can use the generic method() builder method.
7. Configuring the HttpClient
While we can use HttpClient.newHttpClient() for a default client, in most real-world applications, configuring it gives us more flexibility. We can do this using the builder pattern.
For example, we can configure a connection timeout to prevent our application from hanging indefinitely, or add redirect policies.
This builder pattern makes it very clear what our client’s behavior will be. If we wanted to use this in our test methods, we’d simply replace the line HttpClient.newHttpClient() with this builder:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
In this example, we set a 10-second connection timeout and configure the client to always follow redirects.
The official documentation provides all configuration options for the HttpClient.
8. Conclusion
In this lesson, we explored the modern Java HTTP Client. We saw how it replaces the old HttpUrlConnection with a clean, fluent, and powerful API.
We also saw how to perform asynchronous, non-blocking requests with sendAsync() and CompletableFuture, and how to configure the default HttpClient.