Mapping Netty Requests

  • Tutorial

Once upon a time in one far-distant ... project, it took me to do the processing of http requests for Netty . Unfortunately, there was no standard convenient mechanism for mapping http-requests to Netty (and this framework was not for that), therefore, it was decided to implement its own mechanism.


If the reader began to worry about the fate of the project, then you should not, everything is fine with him, because In the future, it was decided to rewrite the web service on a framework more focused on RESTful services, without using our own bicycles. But the developments have remained, and they can be useful to someone, so I would like to share them.

Netty is a framework that allows you to develop high-performance network applications. More information about him can be found on the project website . Netty provides very convenient functionality
for creating socket servers , but in my opinion, this functionality is not very convenient for creating REST servers.


Request processing using standard Netty mechanism


To process requests in Netty, you must inherit from the class ChannelInboundHandlerAdapterand override the method channelRead.


publicclassHttpMappingHandlerextendsChannelInboundHandlerAdapter{
    @OverridepublicvoidchannelRead(ChannelHandlerContext ctx, Object msg){
    }
}

To obtain the necessary information when processing HTTP requests, the object msgcan be brought to HttpRequest.


HttpRequest request = (HttpRequest) msg;

After that, you can get any information from this request. For example, the URL of the request.


String uri = request.uri();

Type of request.


HttpMethod httpMethod = request.method();

And content.


ByteBuf byteBuf = ((HttpContent) request).content();

The content may be, for example, json, transmitted in the body of the POST-query. ByteBuf- This is a class from the Netty library , so json parsers are unlikely to be able to work with it, but it can very easily be brought to a string.


String content = byteBuf.toString(StandardCharsets.UTF_8);

Here, in general, that's all. Using the above methods, you can handle http requests. True, everything will have to be processed in one place, namely in the method channelRead. Even if you spread the logic of processing requests for different methods and classes, you still have to match the URL with these methods somewhere in the same place.


Mapping requests


Well, as you can see, it is quite possible to implement the mapping of http requests using standard Netty functionality . True, it will not be very convenient. I would like to somehow distribute the processing of http-requests for different methods (for example, as is done in Spring ). Using reflection, an attempt was made to implement a similar approach. The library num has turned out from it . Its source code can be found at the link .
To use query mapping using the num library, it is enough to inherit from the class AbstractHttpMappingHandler, after which it will be possible to create query handler methods in this class. The main requirement for these methods is that they returnFullHttpResponseor his heirs. You can show which http-request this method will be called with using annotations:


  • @Get
  • @Post
  • @Put
  • @Delete

The annotation name indicates what type of query will be invoked. It supports four types of queries: GET, POST, PUTand DELETE. As a parameter valuein the annotation, you must specify the URL, when accessing which the required method will be called.


An example of what a GETquery handler will look like that returns a string Hello, world!.


publicclassHelloHttpHandlerextendsAbstractHttpMappingHandler{
      @Get("/test/get")
      public DefaultFullHttpResponse test(){
          returnnew DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer("Hello, world!", StandardCharsets.UTF_8));
      }
}

Request Options


Passing parameters from the request to the handler method is also performed using annotations. To do this, you can use one of the following annotations:


  • @PathParam
  • @QueryParam
  • @RequestBody

@PathParam


An annotation is used to transfer path-parameters @PathParam. When using it as an valueannotation parameter, you must specify the name of the parameter. In addition, the name of the parameter must be specified in the request URL.


An example of how a GETrequest handler will look like , to which the path parameter is passed idand which returns this parameter.


publicclassHelloHttpHandlerextendsAbstractHttpMappingHandler{
      @Get("/test/get/{id}")
      public DefaultFullHttpResponse test(@PathParam(value = "id")int id) {
          returnnew DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer(id, StandardCharsets.UTF_8));
      }
}

@QueryParam


An annotation is used to transfer query parameters @QueryParam. When using it as an valueannotation parameter, you must specify the name of the parameter. Parameter binding can be controlled using the annotation parameter required.


An example of how the GETrequest handler will look like , to which the query parameter is passed messageand which this parameter returns.


publicclassHelloHttpHandlerextendsAbstractHttpMappingHandler{
      @Get("/test/get")
      public DefaultFullHttpResponse test(@QueryParam(value = "message") String message) {
          returnnew DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                  Unpooled.copiedBuffer(message, StandardCharsets.UTF_8));
      }
}

@RequestBody


An POSTabstract is used to transfer the body of the queries @RequestBody. Therefore, it is allowed to use it only in POSTrequests. It is assumed that the data in the json format will be transmitted as the request body. Therefore, for use, @RequestBodyit is necessary to transfer the interface implementation to the handler class constructor JsonParser, which will be engaged in parsing data from the request body. Also in the library there is already a default implementation JsonParserDefault. As a parser, this implementation uses jackson.


An example of what a POSTrequest handler will look like with a request body.


publicclassHelloHttpHandlerextendsAbstractHttpMappingHandler{
      @Post("/test/post")
      public DefaultFullHttpResponse test(@RequestBody Message message){
          returnnew DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK,
                Unpooled.copiedBuffer("{id: '" + message.getId() +"', msg: '" + message.getMessage() + "'}",
                        StandardCharsets.UTF_8));
      }
}

The class Messagelooks like this.


publicclassMessage{
    privateint id;
    private String message;
    publicMessage(){
    }
    publicintgetId(){
        return id;
    }
    publicvoidsetId(int id){
        this.id = id;
    }
    public String getMessage(){
        return message;
    }
    publicvoidsetMessage(String message){
        this.message = message;
    }
}

Error processing


If any request arises during the processing of requests Exceptionand it is not intercepted in the code of the handler methods, the answer with the 500th code will be returned. In order to write some kind of error-handling logic, it is enough to override the method in the handler class exceptionCaught.


publicclassHelloHttpHandlerextendsAbstractHttpMappingHandler{
    @OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception {
        super.exceptionCaught(ctx, cause);
        ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR));
    }
}

Conclusion


Here, in general, that's all. I hope it was interesting and will be useful to someone.




The code of the example http-server on Netty using the num library is available here .


Also popular now: