- PVSM.RU - https://www.pvsm.ru -
Давным-давно в одной далёкой-далёкой... проекте, понадобилось мне сделать обработку http-запросов на Netty [1]. К сожалению, стандартных удобных механизмов для маппинга http-запросов в Netty [1] не нашлось (да и этот фреймвёрк совсем не для того), поэтому, было решено реализовать собственный механизм.
Если читатель начал беспокоиться о судьбе проекта, то не стоит, с ним всё хорошо, т.к. в дальнейшем было решено переписать веб-сервис на фреймвёрке более заточенном под RESTful сервисы, без использования собственных велосипедов. Но наработки остались, и они могут быть кому-нибудь полезными, поэтому хотелось бы ими поделиться.
Netty [1] — это фреймвёрк, позволяющий разрабатывать высокопроизводительные сетевые приложения. Подробнее о нём можно прочитать на сайте [1] проекта.
Для создания сокет-серверов Netty [1] предоставляет весьма удобный функционал, но для создание REST-серверов данный функционал, на мой взгляд, является не очень удобным.
Для обработки запросов в Netty [1] необходимо отнаследоваться от класса ChannelInboundHandlerAdapter и переопределить метод channelRead.
public class HttpMappingHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
}
}
Для получения необходимой при обработке http-запросов информации объект msg можно привести к HttpRequest.
HttpRequest request = (HttpRequest) msg;
После этого можно получить какую-либо информацию из этого запроса. Например, URL-адрес запроса.
String uri = request.uri();
Тип запроса.
HttpMethod httpMethod = request.method();
И контент.
ByteBuf byteBuf = ((HttpContent) request).content();
Контентом может быть, например, json, переданный в теле POST-запроса. ByteBuf — это класс из библиотеки Netty [1], поэтому json-парсеры вряд ли смогут с ним работать, но его очень просто можно привести к строке.
String content = byteBuf.toString(StandardCharsets.UTF_8);
Вот, в общем-то, и всё. Используя указанные выше методы, можно обрабатывать http-запросы. Правда, обрабатывать всё придётся в одном месте, а именно в методе channelRead. Даже если разнести логику обработки запросов по разным методам и классам, всё равно придётся сопоставлять URL с этими методами где-то в одном месте.
Что ж, как видим, вполне можно реализовать маппинг http-запросов, используя стандартный функционал Netty [1]. Правда, будет это не очень удобно. Хотелось бы как-то полностью разнести обработку http-запросов по разным методам (например, как это сделано в Spring [2]). С помощью рефлексии была предпринята попытка реализовать подобный подход. Получилась из этого библиотека num [3]. С её исходным кодом можно ознакомиться по ссылке [3].
Для использования маппинга запросов с помощью библиотеки num [3] достаточно отнаследоваться от класса AbstractHttpMappingHandler, после чего в этом классе можно будет создавать методы-обработчики запросов. Главное требование к данным методам — это чтобы они возвращали FullHttpResponse или его наследников. Показать, по какому http-запросу будет вызываться данный метод, можно с помощью аннотаций:
@Get@Post@Put@DeleteИмя аннотации показывает то, какой тип запроса будет вызван. Поддерживается четыре типа запросов: GET, POST, PUT и DELETE. В качестве параметра value в аннотации необходимо указать URL-адрес, при обращении на который будет вызываться нужный метод.
Пример того, как будет выглядеть обработчик GET-запроса, который возвращает строку Hello, world!.
public class HelloHttpHandler extends AbstractHttpMappingHandler {
@Get("/test/get")
public DefaultFullHttpResponse test() {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
Unpooled.copiedBuffer("Hello, world!", StandardCharsets.UTF_8));
}
}
Передача параметров из запроса в метод-обработчик осуществляется также с помощью аннотаций. Для этого можно воспользоваться одной из следующих аннотаций:
@PathParam@QueryParam@RequestBodyДля передачи path-параметров используется аннотация @PathParam. При её использовании в качестве параметра value аннотации необходимо указать название параметра. Кроме того, название параметра необходимо указать и в URL запроса.
Пример того, как будет выглядеть обработчик GET-запроса, в который передаётся path-параметр id и который возвращает этот параметр.
public class HelloHttpHandler extends AbstractHttpMappingHandler {
@Get("/test/get/{id}")
public DefaultFullHttpResponse test(@PathParam(value = "id") int id) {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
Unpooled.copiedBuffer(id, StandardCharsets.UTF_8));
}
}
Для передачи query-параметров используется аннотация @QueryParam. При её использовании в качестве параметра value аннотации необходимо указать название параметра. Обязательностью параметра можно управлять с помощью параметра аннотации required.
Пример того, как будет выглядеть обработчик GET-запроса, в который передаётся query-параметр message и который возвращает этот параметр.
public class HelloHttpHandler extends AbstractHttpMappingHandler {
@Get("/test/get")
public DefaultFullHttpResponse test(@QueryParam(value = "message") String message) {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
Unpooled.copiedBuffer(message, StandardCharsets.UTF_8));
}
}
Для передачи тела POST-запросов используется аннотация @RequestBody. Поэтому и использовать её разрешается только в POST-запросах. Предполагается, что в качестве тела запроса будут передаваться данные в формате json. Поэтому для использования @RequestBody необходимо в конструктор класса-обработчика передать реализацию интерфейса JsonParser, которая будет заниматься парсингом данных из тела запроса. Также в библиотеке уже имеется реализация по умолчанию JsonParserDefault. В качестве парсера данная реализация использует jackson.
Пример того, как будет выглядеть обработчик POST-запроса, в котором имеется тело запроса.
public class HelloHttpHandler extends AbstractHttpMappingHandler {
@Post("/test/post")
public DefaultFullHttpResponse test(@RequestBody Message message) {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
Unpooled.copiedBuffer("{id: '" + message.getId() +"', msg: '" + message.getMessage() + "'}",
StandardCharsets.UTF_8));
}
}
Класс Message выглядит следующим образом.
public class Message {
private int id;
private String message;
public Message() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Если при обработке запросов возникнет какой-либо Exception и он не будет перехвачен в коде методов-обработчиков, то вернётся ответ с 500-ым кодом. Для того чтобы написать какую-то свою логику по обработке ошибок, достаточно переопределить в классе-обработчике метод exceptionCaught.
public class HelloHttpHandler extends AbstractHttpMappingHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR));
}
}
Вот, в общем-то, и всё. Надеюсь, это было интересно и будет кому-нибудь полезным.
Код примера http-сервера на Netty [1] с использованием библиотеки num [3] доступен по ссылке [4].
Автор: 1_van
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/305099
Ссылки в тексте:
[1] Netty: https://netty.io/
[2] Spring: https://spring.io/
[3] num: https://github.com/vanbv/num
[4] ссылке: https://github.com/vanbv/num-demo
[5] Источник: https://habr.com/post/435864/?utm_source=habrahabr&utm_medium=rss&utm_campaign=435864
Нажмите здесь для печати.