Изучение нового HTTP-клиента в Java 9

Изучение нового HTTP-клиента в Java 9 и 11

1. Вступление

В этом руководстве мы рассмотрим новый инкубатор Java 9https://docs.oracle.com/javase/9/docs/api/jdk/incubator/http/HttpClient.html..

До недавнего времени Java предоставляла только APIHttpURLConnection, который является низкоуровневым и не известен своей функциональностью and , удобной для пользователя.

Поэтому часто использовались некоторые широко используемые сторонние библиотеки, такие какApache HttpClient,Jetty иRestTemplate Spring.

2. Начальная настройка

The HTTP Client module is bundled as an incubator module в JDK 9 и поддерживаетHTTP/2 с обратной совместимостью, по-прежнему облегчая HTTP / 1.1.

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

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

3. Обзор HTTP Client API

В отличие отHttpURLConnection, HTTP-клиент предоставляет механизмы синхронных и асинхронных запросов.

API состоит из 3 основных классов:

  • HttpRequest представляет собой запрос, отправляемый черезHttpClient

  • HttpClient ведет себя как контейнер для информации о конфигурации, общей для нескольких запросов

  • HttpResponse представляет результат вызоваHttpRequest

Мы рассмотрим каждый из них более подробно в следующих разделах. Во-первых, давайте сосредоточимся на запросе.

4. HttpRequestс

HttpRequest, в качестве имениsuggests, - это объект, который представляет запрос, который мы хотим отправить. Новые экземпляры могут быть созданы с помощьюHttpRequest.Builder.

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

Мы рассмотрим самые важные из них.

4.1. УстановкаURI

Первое, что мы должны сделать при создании запроса, это предоставить URL.

Мы можем сделать это двумя способами - используя конструктор дляBuilder с параметромURI или вызывая методuri(URI) в экземпляреBuilder:

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

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

Последнее, что нам нужно настроить для создания базового запроса, - это метод HTTP.

4.2. Указание метода HTTP

Мы можем определить метод HTTP, который будет использовать наш запрос, вызвав один из методов изBuilder:

  • ПОЛУЧИТЬ()

  • POST (тело BodyProcessor)

  • PUT (тело BodyProcessor)

  • УДАЛИТЬ (тело BodyProcessor)

Мы подробно рассмотримBodyProcessor позже. Теперь давайте просто создадимa very simple GET request example:

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

Этот запрос имеет все параметры, необходимыеHttpClient. Однако иногда нам нужно добавить дополнительные параметры в наш запрос; Вот некоторые важные из них:

  • версия протокола HTTP

  • заголовки

  • тайм-аут

4.3. Установка версии протокола HTTP

API полностью использует протокол HTTP / 2 и использует его по умолчанию, но мы можем определить, какую версию протокола мы хотим использовать.

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

Здесь важно отметить, что клиент, например, вернется к HTTP / 1.1, если HTTP / 2 не поддерживается.

4.4. Установка заголовков

Если мы хотим добавить дополнительные заголовки к нашему запросу, мы можем использовать предоставленные методы компоновщика.

Мы можем сделать это одним из двух способов:

  • передача всех заголовков в виде пар ключ-значение методуheaders() или

  • используя метод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();

Последний полезный метод, который мы можем использовать для настройки нашего запроса, - этоa timeout().

4.5. Установка тайм-аута

Теперь давайте определим, сколько времени мы хотим ждать ответа.

Если установленное время истекает, будет выброшеноHttpTimeoutException; тайм-аут по умолчанию установлен на бесконечность.

Тайм-аут может быть установлен с помощью объектаDuration - вызывая методtimeout() в экземпляре построителя:

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

5. Установка тела запроса

Мы можем добавить тело к запросу, используя методы конструктора запросов:POST(BodyProcessor body),PUT(BodyProcessor body) иDELETE(BodyProcessor body).

Новый API предоставляет ряд готовых реализацийBodyProcessor, которые упрощают передачу тела запроса:

  • StringProcessor (читает тело изString, созданного с помощьюHttpRequest.BodyProcessor.fromString)

  • InputStreamProcessor (читает тело изInputStream, созданного с помощьюHttpRequest.BodyProcessor.fromInputStream)

  • ByteArrayProcessor (читает тело из массива байтов, созданного с помощьюHttpRequest.BodyProcessor.fromByteArray)

  • FileProcessor (читает тело из файла по заданному пути, созданного с помощьюHttpRequest.BodyProcessor.fromFile)

Если тело нам не нужно, мы можем просто передатьHttpRequest.noBody():

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

5.1. StringBodyProcessorс

Установить тело запроса с любой реализациейBodyProcessor очень просто и интуитивно понятно.

Например, если мы хотим передать простойString как тело, мы можем использоватьStringBodyProcessor.

Как мы уже упоминали, этот объект можно создать фабричным методомfromString(); он принимает в качестве аргумента только объектString и создает из него тело:

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с

Для этогоInputStream нужно передать какSupplier (чтобы сделать его создание ленивым), поэтому он немного отличается от описанного вышеStringBodyProcessor.

Тем не менее, это также довольно просто:

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();

Обратите внимание, как мы использовали здесь простойByteArrayInputStream; это, конечно, может быть любая реализацияInputStream.

5.3. ByteArrayProcessorс

Мы также можем использоватьByteArrayProcessor и передать массив байтов в качестве параметра:

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с

Для работы с файлом мы можем использовать предоставленныйFileProcessor; его фабричный метод принимает путь к файлу в качестве параметра и создает тело из содержимого:

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();

Мы рассмотрели, как создатьHttpRequest и как установить в нем дополнительные параметры.

Пришло время более внимательно изучить классHttpClient, который отвечает за отправку запросов и получение ответов.

6. HttpClientс

Все запросы отправляются с использованиемHttpClient, экземпляр которого можно создать с помощью методаHttpClient.newBuilder() или путем вызоваHttpClient.newHttpClient().

Он предоставляет множество полезных и самоописываемых методов, которые мы можем использовать для обработки нашего запроса / ответа.

Давайте рассмотрим некоторые из них здесь.

6.1. Настройка прокси

Мы можем определить прокси для подключения. Просто вызовите методproxy() в экземпляреBuilder:

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

В нашем примере мы использовали системный прокси по умолчанию.

6.2. Настройка политики перенаправления

Иногда страница, к которой мы хотим получить доступ, перемещалась по другому адресу.

В этом случае мы получим код состояния HTTP 3xx, обычно с информацией о новом URI. HttpClient can redirect the request to the new URI automatically if we set the appropriate redirect policy.с

Мы можем сделать это с помощью методаfollowRedirects() наBuilder:

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

Все политики определены и описаны в enumHttpClient.Redirect.

6.3. УстановкаAuthenticator для подключения

Authenticator - это объект, который согласовывает учетные данные (аутентификация HTTP) для соединения.

Он предоставляет разные схемы аутентификации (например, базовая или дайджест-аутентификация). В большинстве случаев для аутентификации требуется имя пользователя и пароль для подключения к серверу.

Мы можем использовать классPasswordAuthentication, который просто хранит эти значения:

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

В приведенном выше примере мы передали значения имени пользователя и пароля в виде открытого текста; конечно, в сценарии производства это должно быть иначе.

Обратите внимание, что не каждый запрос должен использовать одинаковые имя пользователя и пароль. КлассAuthenticator предоставляет ряд методовgetXXX (например,getRequestingSite()), которые можно использовать для определения того, какие значения должны быть предоставлены.

Теперь мы собираемся изучить одну из наиболее полезных функций новогоHttpClient - асинхронные вызовы сервера.

6.4. Отправить запросы - Синхронизация против асинхронный

Новый HttpClient предоставляет две возможности для отправки запроса на сервер:

  • send(…) – synchronously (блокируется до получения ответа)

  • sendAsync(…) – asynchronously (не ждет ответа, неблокирующий)

До сих пор методsend(. ..), естественно, ожидает ответа:

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

Этот вызов возвращает объектHttpResponse, и мы уверены, что следующая инструкция из нашего потока приложения будет выполнена только тогда, когда ответ уже здесь.

Однако у него много недостатков, особенно когда мы обрабатываем большие объемы данных.

Итак, теперь мы можем использовать методsendAsync(. ..), который возвращаетCompletableFeature<HttpResponse> -to process a request asynchronously:

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

Новый API также может работать с несколькими ответами и передавать тела запросов и ответов:

List 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> futures = targets.stream()
  .map(target -> client
    .sendAsync(
      HttpRequest.newBuilder(target).GET().build(),
      HttpResponse.BodyHandler.asString())
    .thenApply(response -> response.body()))
  .collect(Collectors.toList());

6.5. УстановкаExecutor для асинхронных вызовов

Мы также можем определитьExecutor, который предоставляет потоки для использования в асинхронных вызовах.

Таким образом, мы можем, например, ограничить количество потоков, используемых для обработки запросов:

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());

По умолчаниюHttpClient использует исполнителяjava.util.concurrent.Executors.newCachedThreadPool().

6.6. ОпределениеCookieManager

С новым API и конструктором легко установитьCookieManager для нашего соединения. Мы можем использовать метод построенияcookieManager(CookieManager cookieManager) для определенияCookieManager для конкретного клиента.

Давайте, например, определимCookieManager, который вообще не позволяет принимать файлы cookie:

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

В случае, если нашCookieManager позволяет сохранять файлы cookie, мы можем получить к ним доступ, проверивCookieManager из нашегоHttpClient:

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

Теперь давайте сосредоточимся на последнем классе из Http API -HttpResponse.

7. HttpResponse Объект

КлассHttpResponse представляет ответ от сервера. Он предоставляет ряд полезных методов, но два наиболее важных из них:

  • statusCode() - возвращает код состояния (типint) для ответа (классHttpURLConnection содержит возможные значения)

  • body() - возвращает тело ответа (тип возврата зависит от параметра ответаBodyHandler, переданного методуsend())

У объекта ответа есть другой полезный метод, который мы рассмотрим, напримерuri(),headers(),trailers() иversion().

7.1. URI объекта ответа

Методuri() для объекта ответа возвращаетURI, от которого мы получили ответ.

Иногда он может отличаться отURI в объекте запроса, потому что может произойти перенаправление:

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

7.2. Заголовки из ответа

Мы можем получить заголовки из ответа, вызвав методheaders() для объекта ответа:

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

Он возвращает объектHttpHeaders в качестве возвращаемого типа. Это новый тип, определенный в пакетеjdk.incubator.http, который представляет доступное только для чтения представление заголовков HTTP.

У него есть несколько полезных методов, которые упрощают поиск значения заголовков.

7.3. Получить трейлеры из ответа

HTTP-ответ может содержать дополнительные заголовки, которые включаются после содержимого ответа. Эти заголовки называются заголовками трейлера.

Мы можем получить их, вызвав методtrailers() наHttpResponse:

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

Обратите внимание, что методtrailers() возвращает объектCompletableFuture.

7.4. Версия ответа

Методversion() определяет, какая версия протокола HTTP использовалась для связи с сервером.

Помните, что даже если мы определим, что хотим использовать HTTP / 2, сервер может ответить через HTTP / 1.1.

Версия, в которой ответил сервер, указана в ответе:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://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. HTTP-клиент Java 11

Основным изменением в Java 11 стала стандартизацияHTTP client API that implements HTTP/2 and Web Socket.. Он призван заменить унаследованный классHttpUrlConnection, который присутствует в JDK с самых первых лет Java.

Изменение было реализовано как часть JEP 321.

8.1. Основные изменения в рамках JEP 321

  1. Инкубированный HTTP API из Java 9 теперь официально включен в Java SE API. НовыйHTTP APIs можно найти вjava.net.HTTP. *

  2. Более новая версия протокола HTTP предназначена для повышения общей производительности отправки запросов клиентом и получения ответов от сервера. Это достигается путем внесения ряда изменений, таких как мультиплексирование потоков, сжатие заголовков и push-обещания.

  3. Начиная с Java 11, асинхронные вызовыthe API is now fully asynchronous (the previous HTTP/1.1 implementation was blocking). реализованы с использованиемCompletableFuture. РеализацияCompletableFuture заботится о применении каждого этапа после завершения предыдущего, поэтому весь этот поток является асинхронным.

  4. Новый HTTP-клиент API предоставляет стандартный способ выполнения сетевых операций HTTP с поддержкой современных веб-функций, таких как HTTP / 2, без необходимости добавления сторонних зависимостей.

  5. Новые API предоставляют встроенную поддержку HTTP 1.1 / 2 WebSocket. Основные классы и интерфейс, обеспечивающие основные функциональные возможности, включают в себя:

    • HttpClient class, java.net.http.HttpClient

    • КлассHttpRequest,java.net.http.HttpRequest

    • ИнтерфейсHttpResponse , java.net.http.HttpResponse

    • ИнтерфейсWebSocket,java.net.http.WebSocket

8.2. Проблемы с HTTP-клиентом до Java 11

Существующий APIHttpURLConnection и его реализация имели множество проблем:

  • URLConnectionAPI был разработан с несколькими протоколами, которые больше не работают (FTP, gopher и т. Д.).

  • API предшествует HTTP / 1.1 и является слишком абстрактным.

  • Он работает только в режиме блокировки (т. Е. Один поток на запрос / ответ).

  • Это очень трудно поддерживать.

9. Изменения в Http-клиенте с Java 11

9.1. Введение в статические фабричные классы

Введены новые статические фабричные классыBodyPublishers,BodySubscribers иBodyHandlers , которые включают существующие реализацииBodyPublisher,BodySubscriber иBodyHandler.

Они используются для выполнения полезных общих задач, таких как обработка тела ответа в виде строки или передача тела в файл.

Например, в Pre Java 11 мы должны были сделать что-то вроде этого:

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

Что мы можем теперь упростить как:

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

Кроме того, имя статических методов было стандартизировано для большей ясности.

Например, имена методов, такие какfromXxx, используются, когда мы используем их в качестве адаптеров, или имена, такие какofXxx, когда мы создаем предопределенные обработчики / подписчики.

9.2. Свободные методы для общих типов телосложения

Введены удобные фабричные методы для созданных издателей и обработчиков для обработки распространенных типов телосложения.

Например, У нас есть несколько способов создания издателей из байтов, файлов и строк:

BodyPublishers.ofByteArray
BodyPublishers.ofFile
BodyPublishers.ofString

Точно так же для создания обработчиков из этих общих типов телосложения мы можем использовать:

BodyHandlers.ofByteArray
BodyHandlers.ofString
BodyHandlers.ofFile

9.3. Другие изменения API

1. С этим новым API мы будем использоватьBodyHandlers.discarding() иBodyHandlers.replacing(value) вместоdiscard(Object replacement):

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

2. Новый методofLines() в[.typeNameLabel]#BodyHandlers #is added to обеспечивает потоковую передачу тела ответа в виде потока строк.

3. МетодfromLineSubscriber добавлен в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. Добавлен новыйBodySubscriber.mapping в классBodySubscribers, который можно использовать для сопоставления одного типа тела ответа с другим путем применения данной функции к объекту тела.

5. ВHttpClient.Redirect константы перечисленияSAME_PROTOCOL и политикаSECURE заменяются новым перечислениемNORMAL.

10. Обработка push-обещаний в HTTP / 2

Новый клиент Http поддерживает push-обещания через интерфейсPushPromiseHandler

Это позволяет серверу «проталкивать» контент клиенту к дополнительным ресурсам при запросе основного ресурса, экономя больше времени в обоих направлениях и, как следствие, повышает производительность при отрисовке страницы.

Это действительно функция мультиплексирования HTTP / 2, которая позволяет нам забыть о пакетировании ресурсов. Для каждого ресурса сервер отправляет клиенту специальный запрос, известный как push-обещание.

Полученные push-обещания, если таковые имеются, обрабатываются заданнымPushPromiseHandler. PushPromiseHnadler с нулевым значением отклоняет любые push-обещания.

HttpClient имеет перегруженный методsendAsync, который позволяет нам обрабатывать такие обещания, как показано в приведенном ниже примере.

Давайте сначала создадимPushPromiseHandler:

private static PushPromiseHandler pushPromiseHandler() {
    return (HttpRequest initiatingRequest,
        HttpRequest pushPromiseRequest,
        Function,
        CompletableFuture>> 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());
    };
}

Затем давайте воспользуемся методомsendAsync для обработки этого push-обещания:

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. Заключение

В этой статье мы исследовали APIHttpClient в Java 9, который обеспечивает большую гибкость и мощные функции. Полный код, используемый для API HttpClient в Java 9, доступенover on GitHub.

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

Примечание. В примерах мы использовали образцы конечных точек REST, предоставленныеhttps://postman-echo.com.