HttpClient
Начиная с Java 11 в JDK есть современный HTTP-клиент java.net.http. Внешние библиотеки для простых задач не нужны. Он поддерживает HTTP/1.1, HTTP/2, асинхронные запросы и WebSocket.
Основные классы:
| Класс | Назначение |
|---|---|
HttpClient | клиент, переиспользуется для многих запросов |
HttpRequest | описание запроса |
HttpResponse<T> | ответ с телом типа T |
BodyHandlers | как читать тело ответа |
BodyPublishers | как отправлять тело запроса |
GET-запрос
import java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;public class GetDemo { public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/todos/1")) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.headers().firstValue("content-type").orElse("нет")); System.out.println(response.body()); }}Пример ответа:
200application/json; charset=utf-8{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}send блокирует поток до получения ответа и может бросить IOException и InterruptedException.
Настройка клиента
import java.net.http.HttpClient;import java.time.Duration;public class ClientConfig { static final HttpClient CLIENT = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NORMAL) .build();}Создавайте один клиент на приложение: он управляет пулом соединений. Таймаут на ответ задается у запроса через .timeout(Duration).
POST с JSON
import java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.time.Duration;public class PostDemo { public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); String json = """ { "title": "Купить хлеб", "body": "Сегодня до 18:00", "userId": 1 } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/posts")) .timeout(Duration.ofSeconds(10)) .header("Content-Type", "application/json; charset=UTF-8") .header("Accept", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); }}Другие методы: PUT(...), DELETE(), method("PATCH", publisher).
Форма и параметры запроса
Параметры в адресе нужно кодировать:
import java.net.URI;import java.net.URLEncoder;import java.net.http.HttpRequest;import java.nio.charset.StandardCharsets;public class QueryParams { public static void main(String[] args) { String query = URLEncoder.encode("java курс", StandardCharsets.UTF_8); URI uri = URI.create("https://example.com/search?q=" + query + "&page=2"); System.out.println(uri); String form = "login=" + URLEncoder.encode("anna", StandardCharsets.UTF_8) + "&password=" + URLEncoder.encode("p@ss word", StandardCharsets.UTF_8); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://example.com/login")) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(form)) .build(); System.out.println(request.method()); }}Авторизация и заголовки
import java.net.URI;import java.net.http.HttpRequest;import java.nio.charset.StandardCharsets;import java.util.Base64;public class AuthDemo { public static void main(String[] args) { HttpRequest bearer = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/profile")) .header("Authorization", "Bearer eyJ...токен") .header("User-Agent", "JavaCourse/1.0") .build(); String credentials = Base64.getEncoder() .encodeToString("user:password".getBytes(StandardCharsets.UTF_8)); HttpRequest basic = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/profile")) .header("Authorization", "Basic " + credentials) .build(); System.out.println(bearer.headers().map()); System.out.println(basic.headers().map()); }}Не храните токены и пароли в исходном коде. Читайте их из переменных окружения: System.getenv("API_TOKEN").
Проверка статуса
Ответ с кодом 404 или 500 для HttpClient не считается исключением: проверять нужно самому.
| Код | Смысл |
|---|---|
| 200 | успех |
| 201 | создано |
| 204 | успех, тела нет |
| 301, 302 | перенаправление |
| 400 | неверный запрос |
| 401 | не авторизован |
| 403 | доступ запрещен |
| 404 | не найдено |
| 429 | слишком много запросов |
| 500 | ошибка сервера |
import java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;public class StatusCheck { static String fetch(HttpClient client, String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); int status = response.statusCode(); if (status >= 200 && status < 300) { return response.body(); } if (status == 404) { throw new IOException("Ресурс не найден: " + url); } throw new IOException("Ошибка HTTP " + status + " для " + url); } public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); System.out.println(fetch(client, "https://jsonplaceholder.typicode.com/todos/1")); try { fetch(client, "https://jsonplaceholder.typicode.com/todos/99999"); } catch (IOException e) { System.out.println(e.getMessage()); } }}Типичные исключения
HttpConnectTimeoutException— не удалось подключиться за заданное время;HttpTimeoutException— сервер не ответил за времяtimeout;ConnectException— сервер недоступен;UnknownHostException— неизвестное имя хоста, обычно проблема DNS;SSLHandshakeException— проблема с сертификатом.
Все они являются наследниками IOException.
Асинхронные запросы
Метод sendAsync не блокирует поток и возвращает CompletableFuture:
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.List;import java.util.concurrent.CompletableFuture;public class AsyncDemo { public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); List<String> urls = List.of( "https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/2", "https://jsonplaceholder.typicode.com/todos/3" ); List<CompletableFuture<String>> futures = urls.stream() .map(url -> HttpRequest.newBuilder().uri(URI.create(url)).build()) .map(req -> client.sendAsync(req, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .exceptionally(e -> "ошибка: " + e.getMessage())) .toList(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); futures.forEach(f -> System.out.println(f.join().replaceAll("\\s+", " "))); }}Три запроса выполняются параллельно, а не последовательно.
Другие типы тела ответа
import java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.file.Path;import java.util.stream.Stream;public class BodyHandlersDemo { public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/todos")) .build(); HttpResponse<Path> file = client.send(request, HttpResponse.BodyHandlers.ofFile(Path.of("todos.json"))); System.out.println(file.body()); HttpResponse<byte[]> bytes = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); System.out.println(bytes.body().length); HttpResponse<Stream<String>> lines = client.send(request, HttpResponse.BodyHandlers.ofLines()); System.out.println(lines.body().count()); HttpResponse<Void> discard = client.send(request, HttpResponse.BodyHandlers.discarding()); System.out.println(discard.statusCode()); }}Для больших файлов используйте ofFile или ofInputStream, чтобы не загружать тело в память целиком.
Отправка файла:
HttpRequest upload = HttpRequest.newBuilder() .uri(URI.create("https://example.com/upload")) .header("Content-Type", "application/octet-stream") .POST(HttpRequest.BodyPublishers.ofFile(Path.of("data.bin"))) .build();Работа с JSON
В JDK нет встроенного JSON-парсера. Для простых случаев хватает ручной работы со строками, но на практике подключают библиотеку: Gson или Jackson. Пример с Gson (зависимость подключается через Maven или Gradle, как показано в главе про сборку):
import com.google.gson.Gson;import com.google.gson.reflect.TypeToken;import java.io.IOException;import java.lang.reflect.Type;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.List;public class JsonDemo { record Todo(int userId, int id, String title, boolean completed) { } public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); Gson gson = new Gson(); HttpRequest one = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/todos/1")) .build(); String body = client.send(one, HttpResponse.BodyHandlers.ofString()).body(); Todo todo = gson.fromJson(body, Todo.class); System.out.println(todo.title()); HttpRequest all = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/todos?_limit=5")) .build(); String listBody = client.send(all, HttpResponse.BodyHandlers.ofString()).body(); Type listType = new TypeToken<List<Todo>>() { }.getType(); List<Todo> todos = gson.fromJson(listBody, listType); todos.forEach(t -> System.out.println(t.id() + ": " + t.title())); String out = gson.toJson(new Todo(1, 201, "Новая задача", false)); System.out.println(out); }}Повтор запроса при ошибке
import java.io.IOException;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.time.Duration;public class Retry { static HttpResponse<String> sendWithRetry(HttpClient client, HttpRequest request, int attempts) throws IOException, InterruptedException { IOException last = null; for (int attempt = 1; attempt <= attempts; attempt++) { try { HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 500) { return response; } last = new IOException("HTTP " + response.statusCode()); } catch (IOException e) { last = e; } Thread.sleep(Duration.ofMillis(200L * attempt * attempt)); } throw last; } public static void main(String[] args) throws IOException, InterruptedException { HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://jsonplaceholder.typicode.com/todos/1")) .timeout(Duration.ofSeconds(5)) .build(); System.out.println(sendWithRetry(client, request, 3).statusCode()); }}Повторять стоит только безопасные для повтора запросы (GET и идемпотентные). Время ожидания между попытками увеличивают: это называется экспоненциальной задержкой.
Простой HTTP-сервер
В JDK есть небольшой встроенный сервер com.sun.net.httpserver (модуль jdk.httpserver). Подходит для учебных примеров и тестов:
import com.sun.net.httpserver.HttpExchange;import com.sun.net.httpserver.HttpServer;import java.io.IOException;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;public class SimpleServer { public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/hello", exchange -> { String name = "мир"; String query = exchange.getRequestURI().getQuery(); if (query != null && query.startsWith("name=")) { name = query.substring(5); } respond(exchange, 200, "Привет, " + name + "!"); }); server.createContext("/echo", exchange -> { if (!"POST".equals(exchange.getRequestMethod())) { respond(exchange, 405, "Только POST"); return; } String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); respond(exchange, 200, "Вы отправили: " + body); }); server.start(); System.out.println("Сервер запущен: http://localhost:8080/hello"); } static void respond(HttpExchange exchange, int status, String text) throws IOException { byte[] bytes = text.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8"); exchange.sendResponseHeaders(status, bytes.length); try (OutputStream out = exchange.getResponseBody()) { out.write(bytes); } }}Проверка из консоли:
curl "http://localhost:8080/hello?name=Anna"curl -X POST -d "тест" http://localhost:8080/echoДля реальных веб-приложений применяют фреймворки: Spring Boot, Javalin, Micronaut, Quarkus.
Работа с URL
import java.net.URI;public class UriDemo { public static void main(String[] args) { URI uri = URI.create("https://user@example.com:8443/path/to/page?x=1&y=2#top"); System.out.println(uri.getScheme()); System.out.println(uri.getHost()); System.out.println(uri.getPort()); System.out.println(uri.getPath()); System.out.println(uri.getQuery()); System.out.println(uri.getFragment()); System.out.println(uri.resolve("../other")); }}Практические советы
- Создавайте один
HttpClientи переиспользуйте его. - Всегда задавайте таймауты соединения и ответа.
- Проверяйте код статуса, а не только отсутствие исключения.
- Кодируйте параметры через
URLEncoder. - Не логируйте заголовки авторизации и тела с личными данными.
- Для разбора JSON используйте проверенную библиотеку.
- Учитывайте ограничения API: лимиты запросов (код 429) и заголовок
Retry-After.
Итоговый проект
Напишите консольную утилиту «Погода»:
- Принимайте название города аргументом командной строки.
- Запрашивайте данные у открытого API (например, Open-Meteo, он не требует ключа).
- Разберите JSON-ответ с помощью Gson или Jackson.
- Выведите температуру и скорость ветра.
- Обработайте ошибки сети, неверный город и статусы 4xx и 5xx.
- Соберите проект через Maven или Gradle в исполняемый jar и покройте разбор ответа тестами JUnit.
Выполнив эту задачу, вы пройдете почти весь курс: работа со строками, коллекциями, исключениями, сетью, JSON, тестами и сборкой.
