JDK 11 introduced the java.net.http.HttpClient which is a large improvement over what the JDK had to offer as support for implementing HTTP clients.
Starting with JDK 11 Java now offers HTTP-2 support in package java.net.http
that is much more developer friendly and higher level than what was available out of the box before with the URL
and URLConnection
classes.
Introduction to the usage of the HttpClient class
The class around which the java.net.http-HTTP-2 API revolves is HttpClient
which is used to send Http-Requests and retrieve the corresponding Http-Responses.
To use the HttpClient
class an instance of it has to be set up using the “Builder”-Pattern with HttpRequest.newBuilder()
. Using this builder one can configure several aspects of the client instance like connection timeouts, proxies, handling of cookies, and more.
Before initiating an Http-Request via the HttpClient
an HttpRequest
Object describing the request to be made has to be created using the “Builder”-Pattern again using HttpRequest.newBuilder(URI). With this builder the request properties like headers, Http-Method, Version, etc. can be configured.
For requests using Http-Methods sending content via the request body an HttpRequest.BodyPublisher
is used to convert Java-Objects to bytes which will be sent over the network. The JDK provides several types of BodyPublishers out of the box or a custom implementation can be used.
The counterpart of the HttpRequest
class is the HttpResponse
interface, instances of which are returned by HttpClients as a result of sending an HttpRequest. Via the HttpResponse
one can access the response headers, body, and more.
To unmarshal the bytes received over the network an HttpClient
uses an instance of interface HttpResponse.BodyHandler
that is provided to the client with the method invocation that starts the request (like HttpClient.send(HttpRequest, HttpResponse.BodyHandler)
).
To summarize, the following objects have to be setup to make HTTP requests using HttpClient
- an HttpClient most likely using
HttpClient.newBuilder()
- an HttpRequest using
HttpRequest.newBuilder()
- for requests involving an http request body an HttpRequest.BodyPublisher is needed. Either an out-of-the-box one provided by
HttpRequest.BodyPublishers
or a custom implementation can be used. - an HttpResponse.BodyHandler is needed for unmarshalling the response bytes. Either an out of the box one from HttpResponse.BodyHandlers or a custom implementation can be used.
The BodyHandler
interface facilitates the examination of the response code and headers before the actual response is received. Its primary role is to generate the response BodySubscriber
, which is responsible for consuming the raw response body bytes, often transforming them into a more abstract Java type.
In essence, a BodyHandler
can be seen as a function that accepts a ResponseInfo
object and yields a BodySubscriber
. This function is called at the point when the response status code and headers become accessible, just before the actual response body bytes are received.
As an example, the following code adapted from the Java documentation for the class HttpClient
demonstrates a simple use case:
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
// No BodyPublisher is needed here since GET does not send a request body
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://www.foo.com/"))
.build();
HttpResponse.BodyHandler<String> stringBodyHandler=HttpResponse.BodyHandlers.ofString();
HttpResponse<String> response = client.send(request, stringBodyHandler);
System.out.println(response.statusCode());
System.out.println(response.body());
}
Handling content-types not supported out-of-the-box like JSON
Unfortunately, the java.net.http-HTTP-2 API with its central class HttpClient
does not support content like JSON, YML, or XML out-of-the-box although many everyday use cases need to process content like this.
To support content like JSON it is necessary to either work with raw Strings and do the conversion to and/or from Java-Objects outside the context of the HttpClient
or to write custom implementations of BodyPublisher
when sending such content oder BodyHandler
when consuming it.
As an example for how to consume content-types that are not supported by default BodyHandler implementations a client for the Zippopotam.us web-service has been built. The Zippopotam.us service provides a free API with a JSON response format offering information about zip codes for many different countries.
The complete sourcecode for the example can be found on GitHub.
The abstract base class AbstractZippoClient
found in package de.rieck.demo provides records and an enum to model the response
payload for zipcode-requests and country codes used by the rest-service. This base class prepares the resource URL for requesting
zipcode information from Zippopotam.us and builds an HttpRequest
that is then handed to the abstract method ZippoPostcodeData requestPostcodeData(HttpRequest zippoHttpRequest)
which is implemented
in the subclasses to show different ways of handling JSON responses.
The code provides a JUnit-5 based parameterized test driver (HttpClientIntegrationTest.java) which integration-tests each of the implementations of AbstractZippoClient.java namely HttpClientUsingCustomBodyHandler.java, HttpClientUsingAsyncRequest.java and HttpClientUsingPredefinedBodyHandler.java in turn each time using the same requests.
Using asynchronous requests
In this implementation an asynchronous request is used which, among other options, allows to transform the response using
Lambda-Functions. Such a function is used here to transform the String
valued response payload to a ZippoPostcodeData
Object
using the thenApply
method which then returns a CompletableFuture
. Finally the parsed result is consumed using the get()
method of the CompletableFuture
which synchronizes the asynchronous response with the control flow again.
public class HttpClientUsingAsyncRequest extends AbstractZippoClient {
private final ObjectMapper jacksonMapper = new ObjectMapper();
protected ZippoPostcodeData requestPostcodeData(HttpRequest zippoHttpRequest) {
try {
return HttpClient.newHttpClient()
.sendAsync(zippoHttpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.thenApply(this::parseJSONResponse)
.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
private ZippoPostcodeData parseJSONResponse(HttpResponse<String> httpStringResponse) {
try {
return jacksonMapper.readValue(httpStringResponse.body(), ZippoPostcodeData.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
Using a custom BodyHandler
Another option for transforming the raw response offered by the HttpClient is using a custom implementation
of class HttpResponse.BodyHandler
that parses the JSON response and turns it into a POJO.
BodyHandlers
work by optionally inspecting a ResponseInfo
object and then constructing and returning a
BodySubscriber
object which is then used to actually handle and transform the response payload.
In this implementation a custom BodySubscriber
is chained with a predefined
BodySubscriber which turns the raw payload into a string first.
An instance of the custom JSONBodyHandler
class is handed to the HttpClient.send(HttpRequest,HttpResponse.BodyHandler)
method which results in the body returned by the HttpResponse to be a ZippoPostcodeData POJO.
public class HttpClientUsingAsyncRequest extends AbstractZippoClient {
private final ObjectMapper jacksonMapper = new ObjectMapper();
protected ZippoPostcodeData requestPostcodeData(HttpRequest zippoHttpRequest) {
try {
return HttpClient.newHttpClient()
.sendAsync(zippoHttpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.thenApply(this::parseJSONResponse)
.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
private ZippoPostcodeData parseJSONResponse(HttpResponse<String> httpStringResponse) {
try {
return jacksonMapper.readValue(httpStringResponse.body(), ZippoPostcodeData.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
Handle the JSON response as a string parsed outside the context of the HttpClient
The simplest option is to just use a predefined BodyHandler
like HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
which is used in this last example. The HttpResponse just provides a String in this case that has to be handled outside the context of the HttpClient.
public class HttpClientUsingPredefinedBodyHandler extends AbstractZippoClient {
private final ObjectMapper jacksonMapper = new ObjectMapper();
protected ZippoPostcodeData requestPostcodeData(HttpRequest zippoHttpRequest) {
try {
HttpResponse<String> jsonResponse = HttpClient.newHttpClient()
.send(zippoHttpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return jacksonMapper.readValue(jsonResponse.body(), ZippoPostcodeData.class);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Conclusion
The JDK 11 HttpClient offers significant improvements over previous Java support for HTTP clients. Although HttpClient does not natively support JSON, YML, or XML content, developers can overcome this issue by using custom implementations of BodyPublisher or BodyHandler, or processing raw Strings outside the HttpClient context. The examples provided here offer examples how to deal with JSON responses using different strategies.