使用Netty構建一個帶註解的Http伺服器框架

高達達發表於2018-05-23

要實現怎樣的效果

一個SpringBoot框架搭建起來的專案釋出介面服務是這樣的

SpringBoot搭建教程點選這裡

@Controller
@RequestMapping("/v1/product")
public class DocController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public WebResult search(@PathVariable("id") Integer id) {
        logger.debug("獲取指定產品接收產品id=>%d", id);
        if (id == null || "".equals(id)) {
            logger.debug("產品id不能為空");
            return WebResult.error(ERRORDetail.RC_0101001);
        }
        return WebResult.success(products.get(id));
    }
}
複製程式碼

我希望我使用Netty構建的Web伺服器也能使用這樣便捷的註解方式去釋出我的介面服務

該怎麼做

系統流程

  • 使用Netty自帶的編解碼、聚合器構建一個帶有Http編解碼功能的伺服器這一點其實非常簡單,Netty提供了對應的Http協議的編解碼以及聚合器,我們只需要在管道初始化的時候載入它們。
public class HttpPipelineInitializer extends ChannelInitializer<Channel> {
    //編解碼處理器名稱
    public final static String CODEC = "codec";
    //HTTP訊息聚合處理器名稱
    public final static String AGGEGATOR = "aggegator";
    //HTTP訊息壓縮處理器名稱
    public final static String COMPRESSOR = "compressor";

    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast(CODEC, new HttpServerCodec());
        pipeline.addLast(AGGEGATOR, new HttpObjectAggregator(512 * 1024));
        pipeline.addLast(COMPRESSOR,new HttpContentCompressor());
        pipeline.addLast(new AllocHandler());
    }
}
複製程式碼
  • 實現RequestMapping註解,用於標識處理器或者控制器對應匹配的介面地址。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String[] value() default {};
}
複製程式碼
  • 提供啟動入口,程式啟動時建立Spring容器,並基於Spring初始化必要元件
  1. 提供程式入口類
public class CettyBootstrap {
    private static final Logger logger = LoggerFactory.getLogger(CettyBootstrap.class);
    private static final String DEFAULT_SPRING_XMLPATH = "classpath:applicantContext.xml";
    private static final String DEFAULT_HTTP_SERVER_BEAN_NAME = "defaultHttpServer";

    public static void create() {
        create(DEFAULT_SPRING_XMLPATH);
    }

    public static void create(String springXmlpath) {
        if (StringUtils.isEmpty(springXmlpath)) {
            springXmlpath = DEFAULT_SPRING_XMLPATH;
        }
        logger.debug("spring框架配置檔案地址為{}", springXmlpath);
        try {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(springXmlpath.split("[,\\s]+"));
            context.start();
            logger.debug("spring框架啟動成功");
            try {
                context.getBean(DEFAULT_HTTP_SERVER_BEAN_NAME, DefaultHttpServer.class);
            } catch (NoSuchBeanDefinitionException ex) {
                logger.warn("未配置HttpServer,採用預設配置啟動");
                context.getAutowireCapableBeanFactory().createBean(DefaultHttpServer.class);
            }
        } catch (BeansException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼
  1. 定義預設實現的HttpServer元件,隨Spring容器啟動時載入基於Netty的Web容器,並使用HandlerMapping元件初始化HttpPipelineInitializer管道,其中HandlerMapping如果未有使用者定義則使用預設的DefaultHandlerMapping實現
public class DefaultHttpServer extends ApplicationObjectSupport {
    private static final Logger logger = LoggerFactory.getLogger(DefaultHttpServer.class);
    private static final String DEFAULT_HTTP_PORT = "8080";
    private static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";


    private String port;

    private HandlerMapping handlerMapping;

    public void setPort(String port) {
        this.port = port;
    }

    @Override
    public void initApplicationContext(ApplicationContext applicationContext) {
        beforeInit(applicationContext);
        initHandlerMapping(applicationContext);
        initServer();
    }

    void initHandlerMapping(ApplicationContext context) {
        try {
            this.handlerMapping = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
        } catch (NoSuchBeanDefinitionException ex) {
            this.handlerMapping = context.getAutowireCapableBeanFactory().createBean(DefaultHandlerMapping.class);
        }
    }

    void initServer() {
        logger.debug("初始化伺服器");
        if (!HttpUtils.isPort(port)) {
            logger.warn("埠號不合法,使用預設埠{}", DEFAULT_HTTP_PORT);
            port = DEFAULT_HTTP_PORT;
        }
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(Integer.parseInt(port)))
                    .childHandler(new HttpPipelineInitializer(handlerMapping));

            ChannelFuture f = b.bind().sync();
            logger.info("服務啟動成功,監聽{}埠", port);
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                workerGroup.shutdownGracefully().sync();
                bossGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    protected void beforeInit(ApplicationContext applicationContext) {

    }

}
複製程式碼
  1. 提供預設的HandlerMapping實現類,負責匹配@RequestMapping註解下的處理函式
public class DefaultHandlerMapping extends ApplicationObjectSupport implements HandlerMapping {
    Logger logger = LoggerFactory.getLogger(DefaultHandlerMapping.class);

    private static Map<String, HttpHandler> httpHandlerMap = new HashMap<String, HttpHandler>();

    @Override
    public void initApplicationContext(ApplicationContext context) throws BeansException {
        logger.debug("初始化處理匹配器");
        Map<String, Object> handles = context.getBeansWithAnnotation(Controller.class);
        try {
            for (Map.Entry<String, Object> entry : handles.entrySet()) {
                logger.debug("載入控制器{}", entry.getKey());
                loadHttpHandler(entry.getValue());
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    void loadHttpHandler(Object value) throws IllegalAccessException, InstantiationException {
        Class clazz = value.getClass();
        Object clazzFromInstance = clazz.newInstance();
        Method[] method = clazz.getDeclaredMethods();
        for (Method m : method) {
            if (m.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping requestMapping = m.getAnnotation(RequestMapping.class);
                for (String url : requestMapping.value()) {
                    HttpHandler httpHandler = httpHandlerMap.get(url);
                    if (httpHandler == null) {
                        logger.info("載入url為{}的處理器{}", url, m.getName());
                        httpHandlerMap.put(url, new HttpHandler(clazzFromInstance, m));
                    } else {
                        logger.warn("url{}存在相同的處理器", url);
                    }
                }
            }
        }
    }

    @Override
    public HttpHandler getHadnler(FullHttpRequest request) {
        return httpHandlerMap.get(request.uri());
    }
}
複製程式碼
  • 當請求進入時通過HandlerMapping元件匹配處理器,如果匹配失敗則返回404
public class AllocHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private HandlerMapping handlerMapping;

    public AllocHandler(HandlerMapping handlerMapping) {
        this.handlerMapping = handlerMapping;
    }

    /*
    異常處理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        super.exceptionCaught(ctx, cause);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception {
        HttpHandler httpHandler = handlerMapping.getHadnler(fullHttpRequest);
        if (httpHandler != null) {
            Object obj = httpHandler.execute(fullHttpRequest);
            if (obj instanceof String) {
                sendMessage(ctx, obj.toString());
            } else {
                sendMessage(ctx, JSONObject.toJSONString(obj));
            }
        } else {
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
        }
    }

    private void sendMessage(ChannelHandlerContext ctx, String msg) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
        response.headers().set("Content-Type", "text/plain");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus httpResponseStatus) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, Unpooled.copiedBuffer(httpResponseStatus.toString(), CharsetUtil.UTF_8));
        response.headers().set("Content-Type", "text/plain");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}
複製程式碼

測試與使用

  • 建立一個TestController
@Controller
public class TestController {

    @RequestMapping("/test")
    public String testHandler(FullHttpRequest fullHttpRequest) {
        return "1234";
    }

    @RequestMapping("/zx")
    public String zx(FullHttpRequest fullHttpRequest) {
        return "zhuxiong";
    }

    @RequestMapping("/obj")
    public Object obj(FullHttpRequest fullHttpRequest) {
        System.out.println("\n\n----------");
        HttpHeaders httpHeaders = fullHttpRequest.headers();
        Set<String> names = httpHeaders.names();
        for (String name : names) {
            System.out.println(name + " : " + httpHeaders.get(name));
        }
        System.out.println("");
        ByteBuf byteBuf = fullHttpRequest.content();
        byte[] byteArray = new byte[byteBuf.capacity()];
        byteBuf.readBytes(byteArray);
        System.out.println(new String(byteArray));
        System.out.println("----------\n\n");

        JSONObject json = new JSONObject();
        json.put("errCode", "00");
        json.put("errMsg", "0000000(成功)");
        json.put("data", null);
        return json;
    }
}
複製程式碼
  • 啟動Spring容器
public class HttpServerTest {
    public static void main(String[] args) throws Exception {
        CettyBootstrap.create();
        // CettyBootstrap.create("classpath:applicationContext.xml");
    }
}
複製程式碼

未來要做的

  • [x] 與Spring框架整合,將核心元件託管給Spring容器統一管理
  • [ ] 提供靜態資源對映
  • [ ] 修改對映策略將請求對映至一個流程(一個處理器多個攔截器)
  • [ ] 支援使用模板語法進行檢視解析

相關文章