JavaRush /Курсы /Java Server /HttpServer и

HttpServer и HttpExchange: путь → handler

Java Server
22 уровень , 1 лекция
Открыта

1. Карта объектов JDK HttpServer

Когда впервые видишь server-side код, мозг новичка часто ожидает магию: «наверное, есть один startServer() и дальше всё само». В HttpServer магии почти нет — и это как раз полезно. Здесь есть несколько базовых классов, которые и составляют всю картину: адрес, сервер, контекст (путь) и обработчик, а также объект “обмена” запрос-ответ.

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

Давайте соберём короткую “карту” в одну табличку — так проще держать в голове, кто за что отвечает, и не путать «сервер» с «запросом» (это очень частая путаница на старте).

Сущность Что это За что отвечает в нашей server-фазе
InetSocketAddress адрес (host + port) где именно сервер “слушает”
HttpServer объект сервера держит сокет, принимает запросы, знает про контексты
HttpContext контекст пути связывает строку пути (например, /health) и обработчик
HttpHandler обработчик ваш код, который будет вызван при запросе
HttpExchange обмен (request+response) объект на один запрос, из него читаем вход и пишем выход

Чтобы это почувствовать не как справочник, а как “поток”, полезно увидеть схему:

flowchart TD
  Client["HTTP-клиент Postman / curl / браузер"] -->|"GET /health"| Server["HttpServer"]
  Server --> Context["HttpContext: /health"]
  Context --> Handler["HttpHandler: handle(exchange)"]
  Handler --> Exchange["HttpExchange: request + response"]
  Handler -->|"status + headers + body"| Client

Теперь важно не смешать два шага. Сначала нужен живой server skeleton: адрес, HttpServer, старт процесса. Сам внешний контракт /health имеет смысл навешивать уже поверх работающего skeleton.

На практике здесь важно уверенно отвечать на два вопроса. Первый — «какой объект живёт долго и является самим сервером?» (это HttpServer). Второй — «какой объект создаётся на каждый запрос и через него мы общаемся с клиентом?» (это HttpExchange). Всё остальное — связующие детали, которые не дают нам утонуть в хаосе.

2. HttpServer: сервер и диспетчер

Интуитивно хочется думать, что “сервер” — это тот код, который отвечает на запрос. Но в JDK HttpServer сервер — это скорее «диспетчер»: он слушает порт, принимает подключения и, увидев путь запроса, решает, какому обработчику (handler) передать работу. А вот обработчик — это уже ваш код, где вы читаете запрос и формируете ответ.

Важный момент: HttpServer — это объект, который создаётся один раз при старте server-mode и живёт всё время, пока ваш процесс работает. Он не создаётся на каждый запрос. Если вы когда-нибудь увидите код «на каждый запрос создаём новый HttpServer», знайте: где-то рядом плачет один сетевой порт.

Минимально сервер создаётся через фабричный метод HttpServer.create(...). Обратите внимание: здесь мы пока только создаём объект. Запуск (start()) — это следующий шаг и следующая лекция (где мы аккуратно встроим это в режим server и конфигурацию).

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

// Адрес, на котором сервер будет слушать входящие подключения
InetSocketAddress address = new InetSocketAddress("localhost", 8080);

// Создаём сервер, но пока НЕ запускаем его (start() будет позже)
HttpServer server = HttpServer.create(address, 0);

Во втором аргументе create(address, 0) слово backlog выглядит как заклинание из древнего гримуара. На практике это размер очереди входящих соединений “на уровне ОС”. Для учебного локального сервера мы честно ставим 0, чтобы использовать значение по умолчанию и не устраивать себе курс «Сетевой администратор за 3 дня».

Дальше у HttpServer появляется ключевая обязанность: зарегистрировать контексты, то есть связать кусочки пути с обработчиками. И вот тут мы плавно переходим к слову “context”.

3. InetSocketAddress: host + port без строковой магии

Если вы когда-нибудь делали URL через конкатенацию строк, а потом ловили баг из серии «лишний слеш сломал всё», то вы уже эмоционально готовы полюбить InetSocketAddress. Он решает простую задачу: зафиксировать адрес прослушивания как структуру данных, а не как “строчку в стиле ‘http://…’”.

Для server-mode нас интересуют два параметра: host и port. С host’ом важно не перемудрить. localhost обычно достаточно, когда вы тестируете сервер на своей машине и не хотите, чтобы он был доступен из локальной сети. Если поставить что-то вроде 0.0.0.0, сервер начнёт слушать на всех интерфейсах — иногда это удобно, но для учебного проекта чаще означает «я случайно открыл дверь в подъезд и теперь удивляюсь, почему там люди».

Вот маленький пример, показывающий, что InetSocketAddress — это именно структура, из которой можно прочитать параметры:

import java.net.InetSocketAddress;

// Создаём адрес как структуру (не как "склеенную строку")
InetSocketAddress address = new InetSocketAddress("localhost", 8080);

// Читаем параметры обратно — удобно для логов и диагностики
String host = address.getHostString();
int port = address.getPort();

И ещё один важный трюк: порт 0. Если указать 0, операционная система выберет свободный порт сама. Это иногда используется в тестах, но для нас сейчас скорее источник путаницы: вы запустили сервер, а он слушает “непонятно где”. Поэтому в учебном server-mode мы почти всегда хотим явный порт (и в следующей лекции мы будем брать его из конфигурации).

4. HttpContext: привязка пути к обработчику

В HttpServer нет аннотаций @GetMapping, нет “контроллеров”, нет “роутера из коробки” — и именно поэтому контекст выглядит так прозрачно. Контекст — это связка: “вот этот путь” → “вот этот обработчик”. И задаётся она методом createContext(...).

Важная практическая деталь: контекст в HttpServer — это не полноценный роутинг, а скорее привязка по префиксу пути. То есть, грубо говоря, "/health" — это “ветка” URL-дерева. И сервер умеет выбрать наиболее подходящую ветку.

Вот как выглядит регистрация контекста в самом простом виде:

import com.sun.net.httpserver.HttpServer;

// Регистрируем обработчик на путь /health
server.createContext("/health", exchange -> {
    // Здесь будет код, который обработает КАЖДЫЙ запрос на /health
    // (и GET, и POST — метод нужно проверять отдельно)
});

Здесь стоит запомнить три вещи.

Во-первых, контекст привязывается по пути, а не по “полному URL”. Никаких http://localhost:8080/health тут быть не должно — это задача клиента, а не сервера.

Во-вторых, HttpServer не выбирает обработчик по HTTP-методу. Если вы привязали /health, то в этот обработчик прилетит и GET, и POST, и что угодно. Отличать методы — это отдельная тема следующего дня, и мы её не будем “прятать под ковёр” в этой лекции.

В-третьих, контексты могут быть более общими и более конкретными, и сервер старается выбрать более конкретный. Например, если когда-нибудь у вас будут одновременно контексты "/api" и "/api/v1/reading-list", то запрос на "/api/v1/reading-list" должен попасть в более специфичный обработчик. Это не магия, а просто здравый смысл, встроенный в сервер: сначала ищем самый точный match.

5. HttpHandler: лямбда или класс

Когда начинаешь, лямбда выглядит очень вкусно: минимум кода, максимум результата. Но как только обработчик перестаёт быть “две строчки”, лямбда превращается в монстра, которого никто не хочет читать. Хорошая новость: HttpHandler — обычный интерфейс, и вы можете сделать обработчик отдельным классом, а не держать всё внутри createContext(...).

Сам HttpHandler выглядит концептуально так: есть метод handle(exchange), и exchange — это тот самый объект “обмена”, который представляет один входящий HTTP-запрос и будущий ответ.

Давайте посмотрим на класс-обработчик в минимальной форме. Он пока ничего не делает (ответ мы начнём писать в лекции про /health), но он уже полезен как структура: есть место, где будет жить логика конкретного endpoint-а.

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;

// Отдельный класс удобен, когда появятся зависимости и логика станет больше "двух строк"
public class HealthHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // exchange — это объект на один конкретный запрос (в нём и request, и response)
        // TODO: в следующей лекции сформируем ответ /health
    }
}

Для одного технического /health на этом шаге этого может быть даже многовато: лямбды в LocalApiServer уже достаточно. Отдельный класс особенно окупается там, где у пути появляются зависимости и логика больше двух строк.

Теперь сравним с лямбдой. Лямбда хороша, когда вы хотите быстро увидеть механику: “путь → обработчик → обмен”. Для этого — идеально.

// Быстрая форма: лямбда как HttpHandler
server.createContext("/health", exchange -> {
    // Считываем метод (GET/POST/...)
    String method = exchange.getRequestMethod();

    // Считываем путь (/health)
    String path = exchange.getRequestURI().getPath();
});

Почему я обычно предлагаю переходить к отдельным классам довольно рано? Потому что в реальном проекте обработчик почти всегда имеет зависимости: логгер, конфигурацию, ObjectMapper для JSON, возможно, сервисы. В лямбду вы начнёте “захватывать” внешние переменные, потом передавать пять аргументов, потом переносить код в отдельный метод, а потом всё равно окажетесь в отдельном классе — только уже с лёгким запахом рефакторинга по всему коду.

6. HttpExchange: запрос и ответ

HttpExchange — это сердце всей механики. Это объект, который создаётся сервером на каждый входящий запрос и передаётся в ваш HttpHandler. Через него вы можете прочитать метод, URI, заголовки, тело запроса, а также сформировать статус ответа, заголовки ответа и записать тело ответа.

И тут важно психологически перестроиться: HttpExchange — это не “запрос”. Это “обмен”. То есть в нём живёт и вход, и выход. Иногда это кажется странным (“почему не два объекта?”), но на учебном уровне это даже удобно: вы не теряете связь “что пришло” → “что мы отправили”.

Начнём с самого простого: метод и путь. Именно эти две штуки вы почти всегда хотите увидеть в логах (и это нам очень пригодится, когда будем проверять server-mode).

import com.sun.net.httpserver.HttpExchange;

// Метод запроса: GET / POST / PUT / ...
String method = exchange.getRequestMethod();

// Путь запроса: например, /health
String path = exchange.getRequestURI().getPath();

Дальше часто нужно прочитать заголовки запроса. В HttpExchange они доступны как exchange.getRequestHeaders(). Это не “красивый DTO”, а довольно прямолинейная структура. Но для старта достаточно одного приёма: getFirst(...), чтобы забрать одно значение.

import com.sun.net.httpserver.Headers;

// Заголовки запроса (request headers)
Headers headers = exchange.getRequestHeaders();

// Например, читаем Accept (может быть null — это нормально)
String accept = headers.getFirst("Accept");

accept здесь может оказаться null, и это нормально: клиент не обязан присылать Accept. Наша цель сейчас не валидация и не идеальный контракт, а понимание механики.

У HttpExchange есть и входной поток, который содержит body запроса:

import java.io.InputStream;

// Тело запроса — это поток байтов (потом мы будем декодировать/парсить, например JSON)
InputStream bodyStream = exchange.getRequestBody();

Сразу важная оговорка: то, что body — это InputStream, означает “это байты, а не JSON”. До превращения в DTO через Jackson ещё куча шагов. Мы их будем делать позже, в следующем дне, и это будет отдельная история “как не сойти с ума без Spring MVC binding”.

И наконец, у HttpExchange есть “ответная часть”. Даже если вы ещё не пишете body, вы уже должны понимать, что ответ живёт здесь: response headers и response body stream.

import java.io.OutputStream;

// Заголовки ответа (response headers) задаём ДО отправки тела
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");

// Поток, в который пишем тело ответа
OutputStream out = exchange.getResponseBody();

Пока не закрепляйте это как “готовый шаблон”. Сейчас нам важно лишь понять: один и тот же exchange — это место, где вы всё делаете. А в следующей лекции мы соберём минимальный правильный ответ для /health (со статусом и JSON).

7. Структура в ReadLater Starter

Когда добавляешь сервер в проект, есть соблазн сделать всё в одном месте: в ReadLaterApplication создать сервер, там же зарегистрировать пути, там же написать ответы. Так действительно быстрее… ровно до момента, когда вы захотите второй endpoint. Потом появляется третий, и внезапно ReadLaterApplication превращается в «главный файл Вселенной», который никто не открывает без лёгкой тревожности.

Нам помогает структура, которую мы уже заложили раньше: package-by-feature плюс явный composition root. Сам запуск сервера и технический /health пока честнее держать в app.server, потому что это не доменная фича reading list, а инфраструктурный вход в локальный API. ServerConfig остаётся в config, потому что это настройки. А вот доменные handlers для путей reading list уже логично держать рядом с feature, например в readinglist.http.

Небольшая иллюстрация “куда целиться” (без попытки строить мини-фреймворк) может выглядеть так:

com.example.readlater
|-- app
|   `-- server
|       `-- LocalApiServer.java
|-- config
|   `-- ServerConfig.java
`-- readinglist
    `-- http

Пока путь один, createContext("/health", ...) даже полезно держать прямо в LocalApiServer: так виден сам механизм без лишних слоёв. Когда путей станет много, регистрацию контекстов уже можно вынести в отдельный класс вроде ApiRoutes, но сейчас это было бы скорее усложнением, чем выигрышем.

8. Типичные ошибки при знакомстве с HttpServer и HttpExchange

Ошибка №1: путать “сервер” и “обработчик”.
Очень легко начать думать, что HttpServer — это что-то вроде “контроллера”, и пытаться писать логику ответа прямо рядом с create(...). Но HttpServer — это долгоживущий объект, который принимает запросы и раздаёт их обработчикам. Логика ответа должна жить в HttpHandler, потому что именно он получает HttpExchange конкретного запроса.

Ошибка №2: воспринимать createContext(...) как полноценный роутинг, как в Spring.
createContext("/api/v1/reading-list/{id}", ...) не сработает так, как вы ожидаете после опыта со Spring MVC. В JDK HttpServer контекст — это привязка по пути (скорее префикс), без шаблонов и без автоматического извлечения переменных. Если держать это в голове сразу, вы меньше злитесь на технологию и больше понимаете, за что именно Spring потом будет “брать деньги”.

Ошибка №3: строить URL строками и тащить “полный URL” в server-side код.
Серверу не нужно знать http://localhost:8080/health как строку. Ему нужен host и port для InetSocketAddress, а пути регистрируются как "/health". Как только вы начинаете смешивать “адрес прослушивания” и “пути”, в голове образуется каша, а в коде — конкатенации, которые потом сложно чинить.

Ошибка №4: игнорировать то, что HttpExchange — это request и response в одном объекте.
Иногда новички читают метод/путь, а потом ищут “где ответ” в другом месте — и не находят. В HttpServer всё делается через exchange: вы прочитали из него вход, и через него же выставляете статус, заголовки и пишете body. Когда это щёлкнет, дальнейшие темы (JSON, статусы, корректные заголовки) ложатся гораздо спокойнее.

Ошибка №5: пытаться “сделать красиво”, построив самописный мини-Spring.
Как только появляется createContext, у многих просыпается внутренний архитектор: хочется сделать роутер, аннотации, рефлексию и “автоматическое связывание”. В рамках курса это прямо вредно: вы потеряете главную ценность — прозрачность. Здесь наша цель не “сделать навсегда”, а увидеть механику руками и подготовить мозг к Spring MVC без ощущения магии.

1
Задача
Java Server, 22 уровень, 1 лекция
Недоступна
Минимальный сервер с context `/ping`
Минимальный сервер с context `/ping`
1
Задача
Java Server, 22 уровень, 1 лекция
Недоступна
Отдельный класс-обработчик для пути `/info`
Отдельный класс-обработчик для пути `/info`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ