HTTP сервер
Модуль предоставляет тонкий слой абстракции над библиотеками HTTP-сервера для создания обработчиков HTTP-запросов с помощью аннотаций в декларативном стиле, так и в императивном стиле.
Подключение¶
Реализация основанная на Undertow.
Зависимость build.gradle
:
Модуль:
Зависимость build.gradle.kts
:
Модуль:
Конфигурация¶
Пример полной конфигурации, описанной в классе HttpServerConfig
(указаны примеры значений или значения по умолчанию):
httpServer {
publicApiHttpPort = 8080 //(1)!
privateApiHttpPort = 8085 //(2)!
privateApiHttpMetricsPath = "/metrics" //(3)!
privateApiHttpReadinessPath = "/system/readiness" //(4)!
privateApiHttpLivenessPath = "/system/liveness" //(5)!
ignoreTrailingSlash = false //(6)!
ioThreads = 2 //(7)!
blockingThreads = 2 //(8)!
shutdownWait = "30s" //(9)!
telemetry {
logging {
enabled = false //(10)!
stacktrace = true //(11)!
mask = "***" //(12)!
maskQueries = [ ] //(13)!
maskHeaders = [ "authorization" ] //(14)!
pathTemplate = true //(15)!
}
metrics {
enabled = true //(16)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(17)!
}
tracing {
enabled = true //(18)!
}
}
}
- Порт публичного сервера
- Порт приватного сервера
- Путь для получения метрик на приватном сервере
- Путь для получения статуса проб готовности на приватном сервере
- Путь для получения статуса проб жизнеспособности на приватном сервере
- Игнорировать ли слэш в окончании пути, если включен то
/my/path
и/my/path/
будут интерпритироваться одинакого, по умолчанию выключен - Количество потоков сервера, по умолчанию равен кол-во ядер процессора либо минимум
2
- Количество блокирующих потоков, по умолчанию равен кол-во ядер процессора умноженных на 2 либо минимум
2
потока - Время ожидания обработки перед выключением сервера в случае штатного завершения
- Включает логгирование модуля (по умолчанию
false
) - Включает логгирование стэка вызовов в случае исключения
- Маска которая используется для скрытия указанных заголовков и параметров запроса/ответа
- Список параметров запроса которые следует скрывать
- Список заголовков запроса/ответа которые следует скрывать
- Использовать ли всегда шаблон пути запроса при логгировании. По умолчанию используется всегда шаблон пути, за исключением уровня логирования
TRACE
где использует полный путь. - Включает метрики модуля (по умолчанию
true
) - Настройка SLO для DistributionSummary метрики
- Включает трассировку модуля (по умолчанию
true
)
httpServer:
publicApiHttpPort: 8080 #(1)!
privateApiHttpPort: 8085 #(2)!
privateApiHttpMetricsPath: "/metrics" #(3)!
privateApiHttpReadinessPath: "/system/readiness" #(4)!
privateApiHttpLivenessPath: "/system/liveness" #(5)!
ignoreTrailingSlash: false #(6)!
ioThreads: 2 #(7)!
blockingThreads: 2 #(8)!
shutdownWait: "30s" #(9)!
telemetry:
logging:
enabled: false #(10)!
stacktrace: true #(11)!
mask: "***" #(12)!
maskQueries: [ ] #(13)!
maskHeaders: [ "authorization" ] #(14)!
pathTemplate: true #(15)!
metrics:
enabled: true #(16)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(17)!
telemetry:
enabled: true #(18)!
- Порт публичного сервера
- Порт приватного сервера
- Путь для получения метрик на приватном сервере
- Путь для получения статуса проб готовности на приватном сервере
- Путь для получения статуса проб жизнеспособности на приватном сервере
- Игнорировать ли слэш в окончании пути, если включен то
/my/path
и/my/path/
будут интерпритироваться одинакого, по умолчанию выключен - Количество потоков сервера, по умолчанию равен кол-во ядер процессора либо минимум
2
- Количество блокирующих потоков, по умолчанию равен кол-во ядер процессора умноженных на 2 либо минимум
2
потока - Время ожидания обработки перед выключением сервера в случае штатного завершения
- Включает логгирование модуля (по умолчанию
false
) - Включает логгирование стэка вызовов в случае исключения
- Маска которая используется для скрытия указанных заголовков и параметров запроса/ответа
- Список параметров запроса которые следует скрывать
- Список заголовков запроса/ответа которые следует скрывать
- Использовать ли всегда шаблон пути запроса при логгировании. По умолчанию используется всегда шаблон пути, за исключением уровня логирования
TRACE
где использует полный путь. - Включает метрики модуля (по умолчанию
true
) - Настройка SLO для DistributionSummary метрики
- Включает трассировку модуля (по умолчанию
true
)
Контроллер декларативный¶
Для создания контроллера следует использовать @HttpController
аннотацию, а для его регистрации как зависимость @Component
.
Аннотация @HttpRoute
отвечает за указания пути и метода HTTP для конкретного метода обработчика.
@Component //(1)!
@HttpController //(2)!
public final class SomeController {
//(3)!
@HttpRoute(method = HttpMethod.POST, //(4)!
path = "/hello/world") //(5)!
public String helloWorld() {
return "Hello World";
}
}
- Указывает что класс является компонентом и его требуется зарегистрировать в контейнере приложения
- Указывает что класс является контроллером и содержит HTTP-обработчики
- Указывает что метод является обработчиком пути в контроллере
- Указывает тип HTTP метода обработчика
- Указывает путь метода обработчика
@Component //(1)!
@HttpController //(2)!
class SomeController {
//(3)!
@HttpRoute(method = HttpMethod.POST, //(4)!
path = "/hello/world") //(5)!
fun helloWorld(): String {
return "Hello World"
}
}
- Указывает что класс является компонентом и его требуется зарегистрировать в контейнере приложения
- Указывает что класс является контроллером и содержит HTTP-обработчики
- Указывает что метод является обработчиком пути в контроллере
- Указывает тип HTTP метода обработчика
- Указывает путь метода обработчика
Запрос¶
Секция описывает преобразования HTTP-запроса у контроллера. Предлагается использовать специальные аннотации для указания параметров запроса.
Параметр пути¶
@Path
— обозначает значение части пути запроса, сам параметр указывается в {кавычках}
в пути
и имя параметра указывается в value
либо по умолчанию равно имени аргумента метода.
Параметр запроса¶
@Query
— значение параметра запроса, имя параметра указывается в value
либо по умолчанию равно имени аргумента метода.
Заголовок запроса¶
@Header
— значение заголовка запроса, имя параметра указывается в value
либо по умолчанию равно имени аргумента метода.
Тело запроса¶
Для указания тела запроса требуется использовать аргумент метода без специальных аннотации,
по умолчанию поддерживаются такие типы как byte[]
, ByteBuffer
, String
.
Json¶
Для указания, что тело является Json и ему требуется автоматически создать такой читатель и внедрить его,
требуется использовать аннотацию @Json
:
Требуется подключить модуль Json.
Текстовая форма¶
Можно использовать FormUrlEncoded
как тип аргумента тела форма данных.
Бинарная форма¶
Можно использовать FormMultipart
как тип аргумента тела бинарная форма.
Куки¶
@Cookie
— значение Cookie, имя параметра указывается в value
либо по умолчанию равно имени аргумента метода.
Самописный параметр¶
В случае если требуется обрабатывать запрос отличным способом, то можно использовать специальный интерфейс HttpServerRequestMapper
:
@Component
@HttpController
public final class SomeController {
public record UserContext(String userId, String traceId) {}
public static final class RequestMapper implements HttpServerRequestMapper<UserContext> {
@Override
public UserContext apply(HttpServerRequest request) {
return new UserContext(request.headers().getFirst("x-user-id"), request.headers().getFirst("x-trace-id"));
}
}
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
public String get(@Mapping(RequestMapper.class) UserContext context) {
return "Hello World";
}
}
@Component
@HttpController
class MapperRequestController {
data class UserContext(val userId: String, val traceId: String)
class RequestMapper : HttpServerRequestMapper<UserContext> {
override fun apply(request: HttpServerRequest): UserContext {
return UserContext(
request.headers().getFirst("x-user-id"),
request.headers().getFirst("x-trace-id")
)
}
}
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
@Mapping(RequestMapper::class)
operator fun get(@Mapping(RequestMapper::class) context: UserContext): String {
return "Hello World"
}
}
Обязательные параметры¶
По умолчанию все аргументы объявленные в методе являются обязательными (NotNull).
По умолчанию все аргументы объявленные в методе которые не используют Kotlin Nullability синтаксис считаются обязательными (NotNull).
Необязательные параметры¶
В случае если аргумент метода является необязательным, то есть может отсутствовать то,
можно использовать аннотацию @Nullable
:
@Component
@HttpController
public final class SomeController {
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
public String helloWorld(@Nullable @Query("queryName") String queryValue) { //(1)!
return "Hello World";
}
}
- Подойдет любая аннотация
@Nullable
, такие какjavax.annotation.Nullable
/jakarta.annotation.Nullable
/org.jetbrains.annotations.Nullable
/ и т.д.
Предполагается использовать Kotlin Nullability синтаксис и помечать такой параметр как Nullable:
Ответ¶
По умолчанию можно использовать стандартные типы возвращаемых значений,
такие как byte[]
, ByteBuffer
, String
которые будут обработаны со статус кодом 200
и соответствующим заголовком типа ответа
либо HttpServerResponse
где надо будет самостоятельно заполнить всю информацию об HTTP ответе.
@Component
@HttpController
public final class SomeController {
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
public HttpServerResponse helloWorld() {
return HttpServerResponse.of(
200, //(1)!
HttpHeaders.of("headerName", "headerValue"), //(2)!
HttpBody.plaintext(body) //(3)!
);
}
}
- HTTP статус код ответа
- Заголовки ответа
- Тело ответа
@Component
@HttpController
class SomeController {
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
fun helloWorld(): HttpServerResponse {
return HttpServerResponse.of(
200, //(1)!
HttpHeaders.of("headerName", "headerValue"), //(2)!
HttpBody.plaintext(body) //(3)!
)
}
}
- HTTP статус код ответа
- Заголовки ответа
- Тело ответа
Json¶
В случае если предполагается отвечать в формате Json, то требуется использовать аннотацию @Json
над методом:
Требуется подключить модуль Json.
Сущность ответа¶
Если предполагается читать тело и получить также заголовки и статус код ответа,
то предполагается использовать HttpResponseEntity
, это обертка над телом ответа.
Ниже показан пример аналогичный примеру Json вместе с оберткой HttpResponseEntity
:
@Component
@HttpController
public final class SomeController {
public record Response(String greeting) {}
@Json
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
public HttpResponseEntity<Response> helloWorld() {
return HttpResponseEntity.of(200, HttpHeaders.of("myHeader", "12345"), new Response("Hello World"));
}
}
@Component
@HttpController
class SomeController {
data class Response(val greeting: String)
@Json
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
fun helloWorld(): HttpResponseEntity<Response> {
return HttpResponseEntity.of(200, HttpHeaders.of("myHeader", "12345"), Response("Hello World"));
}
}
Ответ исключение¶
Если требуется отвечать ошибкой, то можно использовать HttpServerResponseException
для того чтобы бросать исключение.
Самописное¶
В случае если требуется чтение ответа отличным способом, то можно использовать специальный интерфейс HttpServerResponseMapper
:
@Component
@HttpController
public final class SomeController {
public record HelloWorldResponse(String greeting, String name) {}
public static final class ResponseMapper implements HttpServerResponseMapper<HelloWorldResponse> {
@Override
public HttpServerResponse apply(Context ctx, HttpServerRequest request, HelloWorldResponse result) {
return HttpServerResponse.of(200, HttpBody.plaintext(result.greeting() + " - " + result.name()));
}
}
@Mapping(ResponseMapper.class)
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
public HelloWorldResponse helloWorld() {
return new HelloWorldResponse("Hello World", "Bob");
}
}
@Component
@HttpController
class SomeController {
data class HelloWorldResponse(val greeting: String, val name: String)
class ResponseMapper : HttpServerResponseMapper<HelloWorldResponse> {
fun apply(ctx: Context, request: HttpServerRequest, result: HelloWorldResponse): HttpServerResponse {
return HttpServerResponse.of(200, HttpBody.plaintext(result.greeting + " - " + result.name))
}
}
@Mapping(ResponseMapper::class)
@HttpRoute(method = HttpMethod.POST, path = "/hello/world")
fun helloWorld(): HelloWorldResponse {
return HelloWorldResponse("Hello World", "Bob")
}
}
Сигнатуры¶
Доступные сигнатуры для методов декларативного HTTP обработчика из коробки:
Под T
подразумевается тип возвращаемого значения, либо Void
.
T myMethod()
CompletionStage<T> myMethod()
CompletionStageMono<T> myMethod()
Project Reactor (надо подключить зависимость)
Под T
подразумевается тип возвращаемого значения, либо Unit
.
myMethod(): T
suspend myMethod(): T
Kotlin Coroutine (надо подключить зависимость какimplementation
)
Перехватчики¶
Можно создавать перехватчики для изменения поведения либо создания дополнительного поведения используя класс HttpServerInterceptor
.
Перехватчики можно накладывать:
- На конкретные методы контроллера
- На весь класс контроллер целиком
- На все классы контроллеры одновременно (требуется использовать
@Tag(HttpServerModule.class)
над классом перехватчиком)
@Component
@HttpController
public final class SomeController {
public static final class MethodInterceptor implements HttpServerInterceptor {
@Override
public CompletionStage<HttpServerResponse> intercept(Context context, HttpServerRequest request, InterceptChain chain) throws Exception {
return chain.process(context, request).exceptionally(e -> {
if (e instanceof HttpServerResponseException ex) {
return ex;
}
var body = HttpBody.plaintext(e.getMessage());
if (e instanceof IllegalArgumentException) {
return HttpServerResponse.of(400, body);
} else if (e instanceof TimeoutException) {
return HttpServerResponse.of(408, body);
} else {
return HttpServerResponse.of(500, body);
}
});
}
}
@InterceptWith(MethodInterceptor.class)
@HttpRoute(method = HttpMethod.POST, path = "/intercepted")
public String helloWorld() {
return "Hello World";
}
}
@Component
@HttpController
class SomeController {
class MethodInterceptor : HttpServerInterceptor {
override fun intercept(
context: Context,
request: HttpServerRequest,
chain: HttpServerInterceptor.InterceptChain
): CompletionStage<HttpServerResponse> {
return chain.process(context, request).exceptionally { e ->
val body = HttpBody.plaintext(e.message)
when (e) {
is HttpServerResponseException -> e
is IllegalArgumentException -> HttpServerResponse.of(400, body)
is TimeoutException -> HttpServerResponse.of(408, body)
else -> HttpServerResponse.of(500, body)
}
}
}
}
@InterceptWith(MethodInterceptor::class)
@HttpRoute(method = HttpMethod.POST, path = "/intercepted")
fun helloWorld(): String {
return "Hello World"
}
}
Контроллер императивный¶
Для создания контроллера следует реализовать HttpServerRequestHandler.HandlerFunction
интерфейс,
а затем зарегистрировать его в обработчике HttpServerRequestHandler
.
Ниже показан пример по обработке всех описанных декларативных параметров запроса из примеров выше:
public interface SomeModule {
default HttpServerRequestHandler someHttpHandler() {
return HttpServerRequestHandlerImpl.of(HttpMethod.POST, //(1)!
"/hello/{world}", //(2)!
(context, request) -> {
var path = RequestHandlerUtils.parseStringPathParameter(request, "world");
var query = RequestHandlerUtils.parseOptionalStringQueryParameter(request, "query");
var queries = RequestHandlerUtils.parseOptionalStringListQueryParameter(request, "Queries");
var header = RequestHandlerUtils.parseOptionalStringHeaderParameter(request, "header");
var headers = RequestHandlerUtils.parseOptionalStringListHeaderParameter(request, "Headers");
return CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("Hello World")));
});
}
}
- Указывает тип HTTP метода обработчика
- Указывает путь метода обработчика
interface SomeModule {
fun someHttpHandler(): HttpServerRequestHandler? {
return HttpServerRequestHandlerImpl.of(
HttpMethod.POST, //(1)!
"/hello/{world}" //(2)!
) { context: Context, request: HttpServerRequest ->
val path = RequestHandlerUtils.parseStringPathParameter(request, "world")
val query = RequestHandlerUtils.parseOptionalStringQueryParameter(request, "query")
val queries = RequestHandlerUtils.parseOptionalStringListQueryParameter(request, "Queries")
val header = RequestHandlerUtils.parseOptionalStringHeaderParameter(request, "header")
val headers = RequestHandlerUtils.parseOptionalStringListHeaderParameter(request, "Headers")
CompletableFuture.completedFuture(HttpServerResponse.of(200, HttpBody.plaintext("Hello World")))
}
}
}
- Указывает тип HTTP метода обработчика
- Указывает путь метода обработчика