使用Netty构建Http容器

使用Netty构建一个SpringBoot风格的Web框架

要实现怎样的效果

一个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容器统一管理
  • [ ] 提供静态资源映射
  • [ ] 修改映射策略将请求映射至一个流程(一个处理器多个拦截器)
  • [ ] 支持使用模板语法进行视图解析

推荐阅读