Перейти к основному содержимому

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

· 12 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

В этом руководстве мы рассмотрим стандартизацию клиентского API HTTP в Java 11, которая реализует HTTP/2 и веб-сокеты.

Он призван заменить устаревший класс HttpUrlConnection , который присутствовал в JDK с самых первых лет существования Java.

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

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

2. Фон

Изменение было реализовано в рамках JEP 321.

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

  1. Инкубированный HTTP API из Java 9 теперь официально включен в Java SE API. Новые HTTP API можно найти в java.net.HTTP.*
  2. Более новая версия протокола HTTP предназначена для повышения общей производительности отправки запросов клиентом и получения ответов от сервера. Это достигается введением ряда изменений, таких как мультиплексирование потоков, сжатие заголовков и push-обещания.
  3. Начиная с Java 11, API теперь полностью асинхронный (предыдущая реализация HTTP/1.1 блокировала). Асинхронные вызовы реализованы с использованием CompletableFuture . Реализация CompletableFuture заботится о применении каждого этапа после завершения предыдущего, поэтому весь этот поток является асинхронным.
  4. Новый клиентский API HTTP предоставляет стандартный способ выполнения сетевых операций HTTP с поддержкой современных веб-функций, таких как HTTP/2, без необходимости добавления сторонних зависимостей.
  5. Новые API обеспечивают встроенную поддержку HTTP 1.1/2 WebSocket. Основные классы и интерфейс, обеспечивающие основные функциональные возможности, включают:
  • Класс HttpClient , java.net.http.HttpClient
  • Класс HttpRequest , java.net.http.HttpRequest
  • Интерфейс HttpResponse <T>, java.net.http.HttpResponse
  • Интерфейс WebSocket , java.net.http.WebSocket

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

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

  • URLConnection API был разработан с несколькими протоколами, которые в настоящее время больше не работают (FTP, gopher и т. д.).
  • API предшествует HTTP/1.1 и слишком абстрактен.
  • Он работает только в режиме блокировки (т. е. один поток на запрос/ответ).
  • Это очень трудно поддерживать.

3. Обзор HTTP-клиентского API

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

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

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

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

4. HTTP-запрос

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

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

Мы расскажем о самых важных.

Примечание. В JDK 16 появился новый метод HttpRequest.newBuilder(запрос HttpRequest, фильтр BiPredicate<String,String>) , который создает Builder , исходное состояние которого копируется из существующего HttpRequest .

Этот построитель можно использовать для создания HttpRequest , эквивалентного исходному, при этом позволяя изменять состояние запроса до построения, например, удаляя заголовки:

HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Foo-Bar"))

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(тело BodyPublisher)
  • PUT (тело BodyPublisher)
  • УДАЛИТЬ()

Позже мы подробно рассмотрим BodyPublisher .

Теперь давайте просто создадим очень простой пример запроса GET :

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

Последний полезный метод, который мы можем использовать для настройки нашего запроса, — это 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(тело BodyPublisher) , PUT(тело BodyPublisher) и DELETE() .

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

  • StringProcessor — читает тело из строки , созданной с помощью HttpRequest.BodyPublishers.ofString.
  • InputStreamProcessor — считывает тело из InputStream , созданного с помощью HttpRequest.BodyPublishers.ofInputStream.
  • ByteArrayProcessor — читает тело из массива байтов, созданного с помощью HttpRequest.BodyPublishers.ofByteArray
  • FileProcessor — читает тело из файла по заданному пути, созданного с помощью HttpRequest.BodyPublishers.ofFile.

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

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

Примечание. В JDK 16 появился новый метод HttpRequest.BodyPublishers.concat(BodyPublisher…) , который помогает нам создать тело запроса из объединения тел запросов, опубликованных последовательностью издателей. Тело запроса, опубликованное объединяющим публикатором , логически эквивалентно телу запроса, которое было бы опубликовано путем последовательного объединения всех байтов каждого издателя.

5.1. StringBodyPublisher

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

Например, если мы хотим передать простую строку String в качестве тела, мы можем использовать StringBodyPublishers .

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

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

5.2. InputStreamBodyPublisher

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

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

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.BodyPublishers
.ofInputStream(() -> new ByteArrayInputStream(sampleData)))
.build();

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

5.3. ByteArrayПроцессор

Мы также можем использовать 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.BodyPublishers.ofByteArray(sampleData))
.build();

5.4. ФайлПроцессор

Для работы с файлом мы можем использовать предоставленный FileProcessor .

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

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

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

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

6. HTTP-клиент

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

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

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

6.1. Обработка тела ответа

Подобно плавным методам создания издателей, существуют методы, предназначенные для создания обработчиков для распространенных типов тела:

BodyHandlers.ofByteArray
BodyHandlers.ofString
BodyHandlers.ofFile
BodyHandlers.discarding
BodyHandlers.replacing
BodyHandlers.ofLines
BodyHandlers.fromLineSubscriber

Обратите внимание на использование нового класса фабрики BodyHandlers .

До Java 11 нам приходилось делать что-то вроде этого:

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

И теперь мы можем упростить его:

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

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

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

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

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

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

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

В этом случае мы получим код состояния HTTP 3xx, обычно с информацией о новом URI. HttpClient может автоматически перенаправить запрос на новый URI, если мы установим соответствующую политику перенаправления.

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

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

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

6.4. Настройка аутентификатора для подключения

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

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

В большинстве случаев для аутентификации требуется имя пользователя и пароль для подключения к серверу.

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

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

Здесь мы передали значения имени пользователя и пароля в виде открытого текста. Конечно, это должно быть по-другому в производственном сценарии.

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

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

6.5. Отправка запросов — синхронизация против асинхронности

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

  • send(…) — синхронно (блокируется до прихода ответа)
  • sendAsync(…) — асинхронно (не ждет ответа, не блокирует)

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

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

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

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

Итак, теперь мы можем использовать метод sendAsync(. ..) — который возвращает CompletableFeature<HttpResponse>для асинхронной обработки запроса :

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

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

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.BodyHandlers.ofString())
.thenApply(response -> response.body()))
.collect(Collectors.toList());

6.6. Настройка Executor для асинхронных вызовов

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

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

ExecutorService executorService = Executors.newFixedThreadPool(2);

CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
.executor(executorService)
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());

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

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

6.7. Определение CookieHandler

С новым API и компоновщиком установить CookieHandler для нашего соединения очень просто. Мы можем использовать метод компоновщика cookieHandler(CookieHandler cookieHandler) для определения специфичного для клиента CookieHandler .

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

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

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

((CookieManager) httpClient.cookieHandler().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<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
HttpHeaders responseHeaders = response.headers();

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

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

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

Метод 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<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));

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

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

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

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

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

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

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

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

9. Заключение

В этой статье мы рассмотрели API Java 11 HttpClient , который стандартизировал инкубационный HttpClient, представленный в Java 9, с более мощными изменениями.

Полный используемый код можно найти на GitHub .

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