Вивчення нового клієнта HTTP в Java

1. Вступ

У цьому підручнику ми дослідимо новий інкубаційний Java HttpClient .

До недавнього часу Java надавала лише API HttpURLConnection - який є низьким рівнем і не відомий тим, що є багатофункціональнимізручний для користувача.

Тому широко використовувались деякі широко використовувані сторонні бібліотеки, такі як Apache HttpClient, Jetty та Spring's RestTemplate.

2. Початкове налаштування

Модуль клієнта HTTP входить до складу модуля інкубатора в JDK 9 і підтримує HTTP / 2 із зворотною сумісністю, що все ще сприяє HTTP / 1.1.

Для його використання нам потрібно визначити наш модуль, використовуючи файл module-info.java, який також вказує необхідний модуль для запуску нашого додатку:

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

3. Огляд клієнтського API HTTP

На відміну від HttpURLConnection, клієнт HTTP забезпечує синхронні та асинхронні механізми запитів.

API складається з 3 основних класів:

  • HttpRequest - представляє запит, який потрібно надіслати через HttpClient
  • HttpClient - поводиться як контейнер для інформації про конфігурацію, загальну для декількох запитів
  • HttpResponse - представляє результат виклику HttpRequest

Ми розглянемо кожен із них більш детально в наступних розділах. Спочатку зупинимося на запиті.

4. HttpRequest

HttpRequest, як випливає з назви, є об'єктом, який представляє запит, який ми хочемо надіслати. Нові екземпляри можна створювати за допомогою HttpRequest.Builder.

Ми можемо отримати його, зателефонувавши HttpRequest.newBuilder () . Клас Builder надає купу методів, які ми можемо використовувати для налаштування нашого запиту.

Ми розглянемо найважливіші.

4.1. Встановлення URI

Перше, що нам потрібно зробити під час створення запиту, - це надати URL-адресу.

Ми можемо зробити це двома способами - використовуючи конструктор для Builder з параметром URI або викликаючи метод uri (URI) у екземплярі Builder :

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

Останнє, що нам потрібно налаштувати для створення базового запиту, це метод HTTP.

4.2. Вказівка ​​методу HTTP

Ми можемо визначити метод HTTP, який використовуватиме наш запит, викликаючи один із методів з Builder :

  • ОТРИМАТИ ()
  • POST (BodyProcessor body)
  • PUT (BodyProcessor body)
  • ВИДАЛИТИ (BodyProcessor body)

Детальніше ми розглянемо BodyProcessor . Тепер давайте просто створимо дуже простий приклад запиту GET :

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

Цей запит має всі параметри, які вимагає HttpClient . Однак іноді нам потрібно додати додаткові параметри до нашого запиту; ось деякі важливі з них:

  • версія протоколу HTTP
  • заголовки
  • тайм-аут

4.3. Встановлення версії протоколу HTTP

API повністю використовує протокол HTTP / 2 і використовує його за замовчуванням, але ми можемо визначити, яку версію протоколу ми хочемо використовувати.

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

Тут важливо зазначити, що клієнт повернеться, наприклад, до HTTP / 1.1, якщо HTTP / 2 не підтримується.

4.4. Встановлення заголовків

Якщо ми хочемо додати додаткові заголовки до нашого запиту, ми можемо скористатися наданими методами побудови.

Ми можемо зробити це одним із двох способів:

  • передача всіх заголовків як пар ключ-значення в метод headers () або by
  • використовуючи метод header () для одного заголовка ключ-значення:
HttpRequest request = HttpRequest.newBuilder() .uri(new URI("//postman-echo.com/get")) .headers("key1", "value1", "key2", "value2") .GET() .build(); HttpRequest request2 = HttpRequest.newBuilder() .uri(new URI("//postman-echo.com/get")) .header("key1", "value1") .header("key2", "value2") .GET() .build(); 

Останнім корисним методом, який ми можемо використовувати для налаштування нашого запиту, є тайм-аут () .

4.5. Встановлення часу очікування

Давайте тепер визначимо час, який ми хочемо чекати на відповідь.

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("//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 the 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("//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 a 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("//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("//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("//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("//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 at 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 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 the appropriate redirect policy.

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

HttpResponse 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 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 the response, non-blocking)

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

HttpResponse 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 CompletableFeatureto process a request asynchronously:

CompletableFuture
    
      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 targets = Arrays.asList( new URI("//postman-echo.com/get?foo1=bar1"), new URI("//postman-echo.com/get?foo2=bar2")); HttpClient client = HttpClient.newHttpClient(); List
    
      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
    
      response1 = HttpClient.newBuilder() .executor(executorService) .build() .sendAsync(request, HttpResponse.BodyHandler.asString()); CompletableFuture
     
       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("//stackoverflow.com")); assertThat(response.uri() .toString(), equalTo("//stackoverflow.com/"));

7.2. Headers from Response

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

HttpResponse 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 response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandler.asString()); CompletableFuture 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 the server answered is specified in the response:

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

8. Java 11 Http Client

The major change in Java 11 was the standardization of HTTP client API that implements HTTP/2 and Web Socket. It aims to replace the legacy HttpUrlConnection class which has been present in the JDK since the very early years of Java.

The change was implemented as a part of JEP 321.

8.1. Major Changes as Part of JEP 321

  1. The incubated HTTP API from Java 9 is now officially incorporated into the Java SE API. The new HTTP APIs can be found in java.net.HTTP.*
  2. The newer version of the HTTP protocol is designed to improve the overall performance of sending requests by a client and receiving responses from the server. This is achieved by introducing a number of changes such as stream multiplexing, header compression and push promises.
  3. As of Java 11, the API is now fully asynchronous (the previous HTTP/1.1 implementation was blocking). Asynchronous calls are implemented using CompletableFuture.The CompletableFuture implementation takes care of applying each stage once the previous one has finished, so this whole flow is asynchronous.
  4. The new HTTP client API provides a standard way to perform HTTP network operations with support for modern Web features such as HTTP/2, without the need to add third-party dependencies.
  5. The new APIs provide native support for HTTP 1.1/2 WebSocket. 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

8.2. Problems With the Pre Java 11 HTTP Client

The existing HttpURLConnection API and its implementation had numerous problems:

  • URLConnection API was designed with multiple protocols that are now no longer functioning (FTP, gopher, etc.).
  • The API predates HTTP/1.1 and is too abstract.
  • It works in blocking mode only (i.e., one thread per request/response).
  • It is very hard to maintain.

9. Changes in Http Client with Java 11

9.1. Introduction of Static Factory Classes

New static factory classes BodyPublishers, BodySubscribers, and BodyHandlers are introduced that include existing implementations of BodyPublisher, BodySubscriber and BodyHandler.

These are used to perform useful common tasks, such as handling the response body as a String or streaming the body to a File.

For e.g. in Pre Java 11 we had to do something like this:

HttpResponse response = client.send(request, HttpResponse.BodyHandler.asString());

Which we can now simplify as:

HttpResponse response = client.send(request, BodyHandlers.ofString());

Also, the name of static methods has been standardized for more clarity.

For e.g. methods names like fromXxx are used when we are using them as adapters or names like ofXxx when we are creating pre-defined handlers/subscribers.

9.2. Fluent Methods for Common Body Types

Convenient factory methods for created publishers and handlers for handling common body types have been introduced.

For e.g. we have below fluent methods for creating publishers from bytes, files and strings:

BodyPublishers.ofByteArray BodyPublishers.ofFile BodyPublishers.ofString

Similarly, for creating handlers from these common body types we can use:

BodyHandlers.ofByteArray BodyHandlers.ofString BodyHandlers.ofFile

9.3. Other API Changes

1. With this new API, we will be using BodyHandlers.discarding() and BodyHandlers.replacing(value) instead of discard(Object replacement):

HttpResponse response1 = HttpClient.newHttpClient() .send(request, BodyHandlers.discarding());
HttpResponse response1 = HttpClient.newHttpClient() .send(request, BodyHandlers.replacing(value));

2. New method ofLines() in BodyHandlers is added to handle streaming of the response body as a Stream of lines.

3. fromLineSubscriber method is added in BodyHandlers class that can be used as an adapter between a BodySubscriber and a text-based Flow.Subscriber that parses text line by line.

4. Added a new BodySubscriber.mapping in BodySubscribers class that can be used for mapping from one response body type to another by applying the given function to the body object.

5. In HttpClient.Redirect, enum constants SAME_PROTOCOL and SECURE policy are replaced with a new enum NORMAL.

10. Handling Push Promises in HTTP/2

New Http client supports push promises through PushPromiseHandler interface.

It allows the server to “push” content to the client additional resources while requesting the primary resource, saving more roundtrip and as a result, improves performance in page rendering.

It is really the multiplexing feature of HTTP/2 that allows us to forget about resource bundling. For each resource, the server sends a special request, known as a push promise to the client.

Push promises received, if any, are handled by the given PushPromiseHandler. A null valued PushPromiseHnadler rejects any push promises.

The HttpClient has an overloaded sendAsync method that allows us to handle such promises, as shown in the below example.

Let's first create a PushPromiseHandler:

private static PushPromiseHandler pushPromiseHandler() { return (HttpRequest initiatingRequest, HttpRequest pushPromiseRequest, Function
    
     > acceptor) -> { acceptor.apply(BodyHandlers.ofString()) .thenAccept(resp -> { System.out.println(" Pushed response: " + resp.uri() + ", headers: " + resp.headers()); }); System.out.println("Promise request: " + pushPromiseRequest.uri()); System.out.println("Promise request: " + pushPromiseRequest.headers()); }; }
    

Next, let's use sendAsync method to handle this push promise:

httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), pushPromiseHandler()) .thenAccept(pageResponse -> { System.out.println("Page response status code: " + pageResponse.statusCode()); System.out.println("Page response headers: " + pageResponse.headers()); String responseBody = pageResponse.body(); System.out.println(responseBody); }) .join(); 

11. Conclusion

In this article, we explored Java 9's HttpClient API which provides a lot of flexibility and powerful features. The complete code used for Java 9's HttpClient API is available over on GitHub.

Ми також дослідили нові зміни в Java 11 HttpClient, які стандартизували інкубаційний HttpClient, введений в Java 9, з більш потужними змінами. Фрагменти коду, що використовуються для Java 11 Http Client, також доступні через Github.

Примітка: У прикладах ми використовували зразки кінцевих точок REST, наданих //postman-echo.com.