I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we’ll explore Java 9’s new incubating HttpClient.

Until very recently, Java provided only the HttpURLConnection API – which is low-level and isn’t known for being feature-rich and user-friendly.

Therefore, some widely used third-party libraries were commonly used – such as Apache HttpClient, Jetty, and Spring’s RestTemplate.

2. Initial Setup

The HTTP Client module is bundled as an incubator module in JDK 9 and supports HTTP/2 with backward compatibility still facilitating HTTP/1.1.

To use it, we need to define our module using a module-info.java file which also indicates the required module to run our application:

module com.baeldung.java9.httpclient {   
  requires jdk.incubator.httpclient;
}

3. HTTP Client API Overview

Unlike HttpURLConnection, HTTP Client provides synchronous and asynchronous request mechanisms.

The API consists of 3 core classes:

  • HttpRequestrepresents the request to be sent via the HttpClient
  • HttpClientbehaves as a container for configuration information common to multiple requests
  • HttpResponserepresents the result of an HttpRequest call

We’ll examine each of them in more details in the following sections. First, let’s focus on a request.

4. HttpRequest

HttpRequest, as the name suggests, is an object which represents request we want to send. New instances can be created using HttpRequest.Builder.

We can get it by calling HttpRequest.newBuilder(). Builder class provides a bunch of methods which we can use to configure our request.

We’ll cover the most important ones.

4.1. Setting URI

The first thing we have to do when creating a request is to provide the URL.

We can do that in two ways – by using the constructor for Builder with URI parameter or by calling method uri(URI) on the Builder instance:

HttpRequest.newBuilder(new URI("https://postman-echo.com/get"))
 
HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))

The last thing we have to configure to create a basic request is an HTTP method.

4.2. Specifying the HTTP Method

We can define HTTP method which our request will use by calling one of the methods from Builder:

  • GET()
  • POST(BodyProcessor body)
  • PUT(BodyProcessor body)
  • DELETE(BodyProcessor body)

We’ll cover BodyProcessor in detail, later. Now, let’s just create a very simple GET request example:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .GET()
  .build();

This request has all parameters required by HttpClient. However, sometimes we need to add additional parameters to our request; here are some important ones are:

  • the version of the HTTP protocol
  • headers
  • a timeout

4.3. Setting HTTP Protocol Version

The API fully leverages the HTTP/2 protocol and uses it by default but we can define which version of protocol we want to use.

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .version(HttpClient.Version.HTTP_2)
  .GET()
  .build();

Important to mention here is that the client will fallback to, e.g., HTTP/1.1 if HTTP/2 isn’t supported.

4.4. Setting Headers

In case we want to add additional headers to our request, we can use the provided builder methods.

We can do that in one of two ways:

  • passing all headers as key-value pairs to the headers() method or by
  • using header() method for the single key-value header:
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .headers("key1", "value1", "key2", "value2")
  .GET()
  .build();

HttpRequest request2 = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .header("key1", "value1")
  .header("key2", "value2")
  .GET()
  .build();

The last useful method we can use to customize our request is timeout().

4.5. Setting a Timeout

Let’s now define the amount of time we want to wait for a response.

If the set time expires, a HttpTimeoutException will be thrown; the default timeout is set to infinity.

The timeout can be set with the Duration object – by calling method timeout() on the builder instance:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .timeout(Duration.of(10, SECONDS))
  .GET()
  .build();

5. Setting a Request Body

We can add a body to a request by using the request builder methods: POST(BodyProcessor body), PUT(BodyProcessor body) and DELETE(BodyProcessor body).

The new API provides a number of BodyProcessor implementations out-of-the-box which simplify passing the request body:

  • StringProcessor (reads body from a String, created with HttpRequest.BodyProcessor.fromString)
  • InputStreamProcessor (reads body from an InputStream, created with HttpRequest.BodyProcessor.fromInputStream)
  • ByteArrayProcessor (reads body from a byte array, created with HttpRequest.BodyProcessor.fromByteArray)
  • FileProcessor (reads body from a file at given path, created with HttpRequest.BodyProcessor.fromFile)

In case we don’t need a body, we can simply pass in an HttpRequest.noBody():

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .POST(HttpRequest.noBody())
  .build();

5.1. StringBodyProcessor

Setting a request body with any BodyProcessor implementation is very simple and intuitive.

For example, if we want to pass simple String as a body, we can use StringBodyProcessor.

As we already mentioned, this object can be created with a factory method fromString(); it takes just a String object as an argument and creates a body from it:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromString("Sample request body"))
  .build();

5.2. InputStreamBodyProcessor

To do that, the InputStream has to be passed as a Supplier (to make its creation lazy), so it’s a little bit different than described above StringBodyProcessor.

However, this is also quite straightforward:

byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor
   .fromInputStream(() -> new ByteArrayInputStream(sampleData)))
  .build();

Notice how we used a simple ByteArrayInputStream here; that can, of course, be any InputStream implementation.

5.3. ByteArrayProcessor

We can also use ByteArrayProcessor and pass an array of bytes as the parameter:

byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromByteArray(sampleData))
  .build();

5.4. FileProcessor

To work with a File, we can make use of the provided FileProcessor; its factory method takes a path to the file as a parameter and creates a body from the content:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromFile(
    Paths.get("src/test/resources/sample.txt")))
  .build();

We covered how to create HttpRequest and how to set additional parameters in it.

Now it’s time to take a deeper look on HttpClient class which is responsible for sending requests and receiving responses.

6. HttpClient

All requests are sent using HttpClient which can be instantiated using the HttpClient.newBuilder() method or by calling HttpClient.newHttpClient().

It provides a lot of useful and self-describing methods we can use to handle our request/response.

Let’s cover some of these here.

6.1. Setting a Proxy

We can define a proxy for the connection. Just merely call proxy() method on a Builder instance:

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

In our example, we used the default system proxy.

6.2. Setting the Redirect Policy

Sometimes the page we want to access has moved to a different address.

In that case, we’ll receive HTTP status code 3xx, usually with the information about new URI. HttpClient can redirect the request to the new URI automatically if we set appropriate redirect policy.

We can do it with the followRedirects() method on Builder:

HttpResponse<String> response = HttpClient.newBuilder()
  .followRedirects(HttpClient.Redirect.ALWAYS)
  .build()
  .send(request, HttpResponse.BodyHandler.asString());

All policies are defined and described in enum HttpClient.Redirect.

6.3. Setting Authenticator for a Connection

An Authenticator is an object which negotiates credentials (HTTP authentication) for a connection.

It provides different authentication schemes (like e.g., basic or digest authentication). In most cases, authentication requires username and password to connect to a server.

We can use PasswordAuthentication class which is just a holder of these values:

HttpResponse<String> response = HttpClient.newBuilder()
  .authenticator(new Authenticator() {
    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication(
        "username", 
        "password".toCharArray());
    }
}).build()
  .send(request, HttpResponse.BodyHandler.asString());

In the example above we passed the username and password values as a plaintext; of course, in a production scenario, this will have to be different.

Note that not every request should use the same username and password. The Authenticator class provides a number of getXXX (e.g., getRequestingSite()) methods that can be used to find out what values should be provided.

Now we’re going to explore one of the most useful features of new HttpClient – asynchronous calls to the server.

6.4. Send Requests – Sync vs. Async

New HttpClient provides two possibilities for sending a request to a server:

  • send(…) – synchronously (blocks until the response comes)
  • sendAsync(…) – asynchronously (doesn’t wait for response, non-blocking)

Up until now, the send(…) method naturally waits for a response:

HttpResponse<String> response = HttpClient.newBuilder()
  .build()
  .send(request, HttpResponse.BodyHandler.asString());

This call returns an HttpResponse object, and we’re sure that the next instruction from our application flow will be executed only when the response is already here.

However, it has a lot of drawbacks especially when we are processing large amounts of data.

So, now, we can use sendAsync(…) method – which returns CompletableFeature<HttpResponse>to process a request asynchronously:

CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
  .build()
  .sendAsync(request, HttpResponse.BodyHandler.asString());

The new API can also deal with multiple responses, and stream the request and response bodies:

List<URI> targets = Arrays.asList(
  new URI("https://postman-echo.com/get?foo1=bar1"),
  new URI("https://postman-echo.com/get?foo2=bar2"));
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = targets.stream()
  .map(target -> client
    .sendAsync(
      HttpRequest.newBuilder(target).GET().build(),
      HttpResponse.BodyHandler.asString())
    .thenApply(response -> response.body()))
  .collect(Collectors.toList());

6.5. Setting Executor for Asynchronous Calls

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

This way we can, for example, limit 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.BodyHandler.asString());

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

By default, the HttpClient uses executor java.util.concurrent.Executors.newCachedThreadPool().

6.6. Defining a CookieManager

With new API and builder, it’s straightforward to set a CookieManager for our connection. We can use builder method cookieManager(CookieManager cookieManager) to define client-specific CookieManager.

Let’s, for example, define CookieManager which doesn’t allow to accept cookies at all:

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

In case our CookieManager allows cookies to be stored, we can access them by checking CookieManager from our HttpClient:

httpClient.cookieManager().get().getCookieStore()

Now let’s focus on the last class from Http API – the HttpResponse.

7. HttpResponse Object

The HttpResponse class represents the response from the server. It provides a number of useful methods – but two the most important are:

  • statusCode() – returns status code (type int) for a response (HttpURLConnection class contains possible values)
  • body() – returns a body for a response (return type depends on the response BodyHandler parameter passed to the send() method)

The response object has other useful method which we’ll cover like uri(), headers(), trailers() and version().

7.1. URI of Response Object

The method uri() on the response object returns the URI from which we received the response.

Sometimes it can be different than URI in the request object, because a redirection may occur:

assertThat(request.uri()
  .toString(), equalTo("http://stackoverflow.com"));
assertThat(response.uri()
  .toString(), equalTo("https://stackoverflow.com/"));

7.2. Headers from Response

We can obtain headers from the response by calling method headers() on a response object:

HttpResponse<String> response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
HttpHeaders responseHeaders = response.headers();

It returns HttpHeaders object as a return type. This is a new type defined in jdk.incubator.http package which represents a read-only view of HTTP Headers.

It has some useful methods which simplify searching for headers value.

7.3. Get Trailers from Response

The HTTP response may contain additional headers which are included after the response content. These headers are called trailer headers.

We can obtain them by calling method trailers() on HttpResponse:

HttpResponse<String> response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
CompletableFuture<HttpHeaders> trailers = response.trailers();

Note that trailers() method returns CompletableFuture object.

7.4. Version of the Response

The method version() defines which version of HTTP protocol was used to talk with a server.

Remember, that even if we define that we want to use HTTP/2, the server can answer via HTTP/1.1.

The version in which server answered is specified in the response:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .version(HttpClient.Version.HTTP_2)
  .GET()
  .build();
HttpResponse<String> response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));

8. Conclusion

In this article, we explored Java 9’s HttpClient API which provides a lot of flexibility and powerful features.

As always, the complete code can be found over on GitHub.

Note: In the examples, we’ve used sample REST endpoints provided by https://postman-echo.com.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS

  Subscribe  
newest oldest most voted
Notify of
Gagandeep kalra
Guest

is this NIO based Async or simple thread blocking one?

Grzegorz Piwowarek
Editor

NIO