HTTP-клиент
Программам часто нужны данные из интернета: курс валют, погода, ответ другого сервиса. Их получают по протоколу HTTP: клиент отправляет серверу запрос (request), сервер возвращает ответ (response) — код статуса и текст. Для этого в Java есть встроенный клиент HttpClient (Java 11) из модуля java.net.http. Он не входит в java.base, поэтому даже в компактном файле его импортируют: import java.net.http.*;.
Сервер для опытов
Чтобы примеры работали без интернета, каждый из них сам запускает маленький сервер на вашем компьютере. Для этого в JDK есть простой HttpServer. Он отвечает текстом «Сервер получил: …» и повторяет запрос, а на адрес /missing отвечает кодом 404. Порт 0 означает «любой свободный» — настоящий номер узнают через server.getAddress().getPort(). Код сервера — в методе startServer() в конце каждого примера; разбирать его не обязательно, главное в уроке — клиент.
GET-запрос
Работа с клиентом — три шага: создать HttpClient, собрать HttpRequest с адресом и отправить его методом send. Второй аргумент send — обработчик тела (body handler): он говорит, во что превратить ответ. BodyHandlers.ofString() читает тело как строку:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/users/1";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.statusCode());
IO.println(response.body());
server.stop(0);
}
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Результат:
200
Сервер получил: GET /users/1Для настоящего сайта меняется только адрес, например https://api.example.com/users/1. По умолчанию запрос — GET. Код 200 означает «всё хорошо». Есть и другие обработчики: ofLines() даёт поток строк, ofFile(path) сохраняет ответ в файл, discarding() тело отбрасывает.
Коды ответа и заголовки
Код статуса — трёхзначное число: 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента (404 — «не найдено»), 5xx — ошибка сервера. Кроме тела, ответ несёт заголовки — пары «имя: значение» с дополнительными сведениями. headers().firstValue(...) возвращает Optional:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String base = "http://localhost:" + server.getAddress().getPort();
HttpClient client = HttpClient.newHttpClient();
for (String path : List.of("/users/1", "/missing")) {
HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(path + " -> " + response.statusCode());
}
HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/users/1")).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.headers().firstValue("Content-Type").orElse("нет"));
server.stop(0);
}
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Результат:
/users/1 -> 200
/missing -> 404
text/plain; charset=utf-8POST с JSON
Чтобы отправить данные, запрос строят с методом POST и телом (body). BodyPublishers.ofString превращает строку в тело запроса. Данные чаще всего передают в формате JSON; здесь он записан обычной строкой, а заголовок Content-Type сообщает серверу формат:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/users";
String json = """
{"name": "Анна", "age": 28}""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
IO.println(response.statusCode());
IO.println(response.body());
server.stop(0);
}
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Результат:
200
Сервер получил: POST /users {"name": "Анна", "age": 28}timeout ограничивает ожидание ответа: если сервер молчит дольше пяти секунд, send выбросит HttpTimeoutException. Так же устроены PUT(...) и DELETE(). Разбирать JSON из ответа удобнее библиотеками Jackson или Gson — в стандартной библиотеке Java парсера JSON нет.
Асинхронные запросы
send блокирует поток, пока не придёт ответ. sendAsync возвращает сразу — CompletableFuture, в котором ответ появится позже. Так можно отправить несколько запросов одновременно и дождаться всех:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String base = "http://localhost:" + server.getAddress().getPort();
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = new ArrayList<>();
for (String path : List.of("/news", "/weather", "/rates")) {
HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
futures.add(client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body));
}
for (CompletableFuture<String> future : futures) {
IO.println(future.join());
}
server.stop(0);
}
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Результат:
Сервер получил: GET /news
Сервер получил: GET /weather
Сервер получил: GET /ratesЗапросы выполняются параллельно, а join() по очереди забирает готовые результаты — в том порядке, в каком будущие значения лежат в списке. Подробнее о CompletableFuture — в уроке CompletableFuture. Один HttpClient можно и нужно переиспользовать для многих запросов: он держит соединения открытыми.
Частая ошибка
send не выбрасывает исключение, если сервер ответил ошибкой: 404 или 500 — тоже нормальный ответ. Код, который сразу берёт body(), примет текст ошибки за данные:
import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
void main() throws Exception {
HttpServer server = startServer();
String url = "http://localhost:" + server.getAddress().getPort() + "/missing";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
IO.println("Данные: " + response.body());
} else {
IO.println("Ошибка, код " + response.statusCode());
}
server.stop(0);
}
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes());
String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
byte[] bytes = reply.strip().getBytes();
int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.close();
});
server.start();
return server;
}Результат:
Ошибка, код 404Всегда проверяйте statusCode(), прежде чем разбирать тело. Исключение (IOException, например ConnectException) бывает, только когда ответа нет вовсе: сервер недоступен или нет сети.