阅读 52

教你使用 Netty 实现一个 MVC 框架

NettyMVC#

上面介绍 Netty 能做是什么时我们说过,相比于 SpringMVC 等框架,Netty 没提供路由等功能,这也契合和 Netty 的设计思路,它更贴近底层。下面我们在 Netty Http 的基础上加入路由和 IOC 等功能,来实现一个 MVC 框架。

NettyMVC 是一个 MVC 框架,它实现了 IOC 容器的相关功能。

  1. 支持 @Controller,@RequestParam,@RequestMapping 等 MVC 注解。
  2. 支持 @Service,@Repositry,@Autowired 等 IOC 注解。
  3. URI 路由解析,参数映射。
  4. Request 中支持多种参数类型,包括基本数据类型,List,Array,Map等等。 结构图

教你使用 Netty 实现一个 MVC 框架

如何使用在项目中引入 netty-mvc-core 模块。在 Maven 项目中的 resources 文件夹下创建 applicationContext.xml, 用来配置 IOC 的包扫描路径。

Copy <?xml version="1.0" encoding="UTF-8"?>
 <beans>
     <package-scan component-scan="org.test.demo" />
 </beans>
复制代码

@Controller 对应控制层注解,@Service 对应服务层注解,@Respostry 对应持有层注解,@Autowired 做自动注入,@RequestMapping 做路由,@RequestParam 做参数映射。

Copy  @Controller
  @RequestMapping("/user")
  public class UserController {

      @Autowired
      private UserService userService;

      @RequestMapping("/getUser")
      public FullHttpResponse getUserById(FullHttpRequest request,@RequestParam("userId") int id,@RequestParam("name") String name){

          String res = userService.getUser(id);
          return HttpUtil.constructText(res);
      }

  }
@Service("userService")
  public class UserServiceImpl implements UserService {

      @Autowired("userDao")
      private UserDao userDao;

      @Override
      public String getUser(int id) {
          return userDao.get(id);
      }
  }
@Repository
  public class UserDao {

      public String get(int id){
          if(id == 1){
              return "paul";
          }else{
              return "wang";
          }
      }
  }
复制代码

代码实现既然我们的 MVC 框架是基于 Netty Http 的,我们首先实现 Netty Http 的相关功能。服务启动类,单例:

Copy    package com.paul.http;
    import com.paul.ioc.bean.AnnotationApplicationContext;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;

    public class NettyHttpServer {

        private static NettyHttpServer instance = new NettyHttpServer();

        private NettyHttpServer(){};

        public static NettyHttpServer getInstance(){
            return instance;
        }

        private final int port = 8012;

        public void start(AnnotationApplicationContext applicationContext) throws InterruptedException {
            //定义两个线程组,事件循环组,可以类比与 Tomcat 就是死循环,不断接收客户端的连接
            // boss 线程组不断从客户端接受连接,但不处理,由 worker 线程组对连接进行真正的处理
            // 一个线程组其实也能完成,推荐使用两个
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                // 服务端启动器
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                //group 方法有两个,一个接收一个参数,另一个接收两个参数
                // childhandler 是我们自己写的请求处理器
                serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                        .childHandler(new ServerInitializer(applicationContext));
                //绑定端口
                ChannelFuture future = serverBootstrap.bind(port).sync();
                System.out.println("服务端启动,监听端口:8012");
                //channel 关闭的监听
                future.channel().closeFuture().sync();
            }finally {
                //优雅的关闭
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }

        }
    }
复制代码
Copy对应的 Initializer:
复制代码
Copy        public class ServerInitializer extends ChannelInitializer<SocketChannel> {

        private AnnotationApplicationContext applicationContext;

        public ServerInitializer(AnnotationApplicationContext applicationContext){
            this.applicationContext = applicationContext;
        }
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            //管道,管道里面可以有很多 handler,handler 可以理解为一层层过滤的网
            ChannelPipeline pipeline = socketChannel.pipeline();
            //HttpServerCodec 是 HttpRequestDecoder 和 HttpReponseEncoder 的组合,编码和解码的 handler
            pipeline.addLast("httpServerCodec", new HttpServerCodec());
            pipeline.addLast(new HttpObjectAggregator(10*1024*1024));
            pipeline.addLast("handler", new DispatcherHandler(applicationContext));
        }
    }
    ```
    上面这部分代码除了 DispatcherHandler 没有什么特殊的。通过名字就能知道       
    DispatcherHandler 是我们的核心路由控制类。在看这个类之前我们先定义几个注解,用于 
    IOC 和请求路径的 mapping。
    ```java
    /**
     * 自定义自动注入的注解
     * @author swang18
     *
     */
    @Target({ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Autowired {
        String value() default "";
    }
复制代码
Copy@Controller, @Service, @Respostry 与 SpringMVC 注解含义相同,分别对应控制层,业
务层和持久层。

@RequestMapping 用作请求路径的 mapping,@RequestParam 用作参数的映射。此处代
码都一样,就不一一列举了。
复制代码
Copy    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RequestMapping {
        String value() default "";
    }
复制代码

首先我们实现 IOC 相关功能,将对应类的实例放入容器中保存起来。我们通过解析 applicationContext.xml 的包名,解析包里面有对应注解的类,将这些类的实例加入到容器中,有 @Autowired 注解的地方要把实例注入进来。

说到容器,很多人可能有点疑惑,到底用什么来实现容器,其实用最简单的 Map 就可以保存名字和对应的实例。

先看 xml 解析类,将 component-scan 中的包名解析出来:

Copy    public class XmlUtil {

        public String handlerXMLForScanPackage(String configuration){
            System.out.println(System.getProperty("user.dir"));//user.dir指定了当前的路径
            System.out.println(configuration);
            InputStream ins = this.getClass().getClassLoader().getResourceAsStream(configuration);
            SAXReader reader = new SAXReader();
            try{
                Document document = reader.read(ins);
                Element root = document.getRootElement();
                Element ele = root.element("package-scan");
                String res = ele.attributeValue("component-scan");
                return res;
            }catch(Exception e){
                e.printStackTrace();
            }
            return null;
        }

    }
复制代码
Copy容器的实现,我们在 IOC 容器启动时,也做了 MVC 注解的扫描,将路径和方法存入 Map 中,并且启动了 Netty 服务器:
复制代码
Copy    public abstract class BeanFactory {

        public Object getBean(String beanName){
            return doGetBean(beanName);
        }

        //交给子类,容器实现类去完成
        protected abstract Object doGetBean(String beanName);

    }
复制代码
Copy    public abstract class ApplicationContext extends BeanFactory {

        protected String configuration;
        protected XmlUtil xmlUtil;

        public ApplicationContext(String configuration){
            this.configuration = configuration;
            this.xmlUtil = new XmlUtil();
        }
    }
复制代码
Copy    public class AnnotationApplicationContext extends ApplicationContext {

        //保存类路径的缓存
        private List<String> classCache = Collections.synchronizedList(new ArrayList<String>());

        //保存需要注入的类的缓存
        private List<Class<?>> beanDefinition = Collections.synchronizedList(new ArrayList<Class<?>>());

        //保存类实例的容器
        public Map<String,Object> beanFactory = new ConcurrentHashMap<>();

        // 完整路径和 方法的 mapping
        public Map<String,Object> handleMapping = new ConcurrentHashMap<>();

        // 类路径和controller 的 mapping
        public Map<String,Object> controllerMapping = new ConcurrentHashMap<>();

        public AnnotationApplicationContext(String configuration) {
            super(configuration);
            String path  = xmlUtil.handlerXMLForScanPackage(configuration);

            //执行包的扫描操作
            scanPackage(path);
            //注册bean
            registerBean();
            //把对象创建出来,忽略依赖关系
            doCreateBean();
            //执行容器管理实例对象运行期间的依赖装配
            diBean();
            //mvc 相关注解扫描
            mappingMVC();
            //启动 netty 服务器
            NettyHttpServer instance = NettyHttpServer.getInstance();
            try {
                instance.start(this);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        /*
         * MVC 注解和路径扫描
         */
        private void mappingMVC() {
            //上一步已经完成了 Controller,service,respostry,autowired 等注解的扫描和注入
            //遍历容器,将 requestmapping 注解的路径和对应的方法以及 contoller 实例对应起来
            for(Map.Entry<String,Object> entry:beanFactory.entrySet()){
                Object instance = entry.getValue();
                Class<?> clazz = instance.getClass();
                if(clazz.isAnnotationPresent(Controller.class)){
                    RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
                    String classPath = requestMapping.value();
                    Method[] methods = clazz.getMethods();
                    for(Method method:methods){
                        if(method.isAnnotationPresent(RequestMapping.class)){
                            RequestMapping requestMapping2 = method.getAnnotation(RequestMapping.class);
                            String methodPath = requestMapping2.value();
                            String requestPath = classPath + methodPath;
                            handleMapping.put(requestPath,method);
                            controllerMapping.put(requestPath,instance);
                        }else{
                            continue;
                        }

                    }
                }else{
                    continue;
                }
            }
        }


        @Override
        protected Object doGetBean(String beanName) {
            return beanFactory.get(beanName);
        }

        /**
         * 扫描包下面所有的 .class 文件的类路径到上面的List中
         *
         */
        private void scanPackage(final String path) {
            System.out.println("path:"+path);
            URL url = this.getClass().getClassLoader().getResource(path.replaceAll("\\.", "/"));
            System.out.println("scanPackage:" + url.getPath());
            try {
                File file = new File(url.toURI());

                file.listFiles(new FileFilter(){
                    //pathname 表示当前目录下的所有文件
                    @Override
                    public boolean accept(File pathname) {
                        //递归查找文件
                        if(pathname.isDirectory()){
                            scanPackage(path+"."+pathname.getName());
                        }else{
                            if(pathname.getName().endsWith(".class")){
                                String classPath = path + "." + pathname.getName().replace(".class","");
                                System.out.println("addClassPath:" +classPath );
                                classCache.add(classPath);
                            }
                        }
                        return true;
                    }

                });
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }


        }


        /**
         * 根据类路径获得 class 对象
         */
        private void registerBean() {
            if(classCache.isEmpty()){
                return;
            }


            for(String path:classCache){
                try {
                    //使用反射,通过类路径获取class 对象
                    Class<?> clazz = Class.forName(path);
                    //找出需要被容器管理的类,比如,@Component,@org.test.demo.Controller,@org.test.demo.Service,@Repository
                    if(clazz.isAnnotationPresent(Repository.class)||clazz.isAnnotationPresent(Service.class)
                            ||clazz.isAnnotationPresent(Controller.class)|| clazz.isAnnotationPresent(Component.class)){
                        beanDefinition.add(clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

        }


        /**
         *
         * 根据类对象,创建实例
         */
        private void doCreateBean() {
            if(beanDefinition.isEmpty()){
                return;
            }


            for(Class clazz:beanDefinition){
                try {
                    Object instance = clazz.newInstance();
                    //将首字母小写的类名作为默认的 bean 的名字
                    String aliasName = lowerClass(clazz.getSimpleName());
                    //先判断@ 注解里面是否给了 Bean 名字,有的话,这个就作为 Bean 的名字
                    if(clazz.isAnnotationPresent(Repository.class)){
                        Repository repository = (Repository) clazz.getAnnotation(Repository.class);
                        if(!"".equals(repository.value())){
                            aliasName = repository.value();
                        }
                    }
                    if(clazz.isAnnotationPresent(Service.class)){
                        Service service = (Service) clazz.getAnnotation(Service.class);
                        if(!"".equals(service.value())){
                            aliasName = service.value();
                        }
                    }
                    if(clazz.isAnnotationPresent(Controller.class)){
                        Controller controller = (Controller) clazz.getAnnotation(Controller.class);
                        if(!"".equals(controller.value())){
                            aliasName = controller.value();
                        }
                    }
                    if(clazz.isAnnotationPresent(Component.class)){
                        Component component = (Component) clazz.getAnnotation(Component.class);
                        if(!"".equals(component.value())){
                            aliasName = component.value();
                        }
                    }
                    if(beanFactory.get(aliasName)== null){
                        beanFactory.put(aliasName, instance);
                    }

                    //判断当前类是否实现了接口
                    Class<?>[] interfaces = clazz.getInterfaces();
                    if(interfaces == null){
                        continue;
                    }
                    //把当前接口的路径作为key存储到容器中
                    for(Class<?> interf:interfaces){
                        beanFactory.put(interf.getName(), instance);
                    }



                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }


            for (Map.Entry<String, Object> entry : beanFactory.entrySet()) {
                System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
            }


        }

        /**
         * 对创建好的对象进行依赖注入
         */
        private void diBean() {
            if(beanFactory.isEmpty()){
                return;
            }

            for(Class<?> clazz:beanDefinition){
                String aliasName = lowerClass(clazz.getSimpleName());
                //先判断@ 注解里面是否给了 Bean 名字,有的话,这个就作为 Bean 的名字
                if(clazz.isAnnotationPresent(Repository.class)){
                    Repository repository = clazz.getAnnotation(Repository.class);
                    if(!"".equals(repository.value())){
                        aliasName = repository.value();
                    }
                }
                if(clazz.isAnnotationPresent(Service.class)){
                    Service service = clazz.getAnnotation(Service.class);
                    if(!"".equals(service.value())){
                        aliasName = service.value();
                    }
                }
                if(clazz.isAnnotationPresent(Controller.class)){
                    Controller controller = clazz.getAnnotation(Controller.class);
                    if(!"".equals(controller.value())){
                        aliasName = controller.value();
                    }
                }
                if(clazz.isAnnotationPresent(Component.class)){
                    Component component = clazz.getAnnotation(Component.class);
                    if(!"".equals(component.value())){
                        aliasName = component.value();
                    }
                }

                //根据别名获取到被装配的 bean 的实例
                Object instance = beanFactory.get(aliasName);
                try{
                    //从类中获取参数,判断是否有 @Autowired 注解
                    Field[] fields = clazz.getDeclaredFields();
                    for(Field f:fields){
                        if(f.isAnnotationPresent(Autowired.class)){
                            //开启字段的访问权限
                            f.setAccessible(true);
                            Autowired autoWired = f.getAnnotation(Autowired.class);
                            if(!"".equals(autoWired.value())){
                                //注解里写了别名
                                f.set(instance, beanFactory.get(autoWired.value()));

                            }else{
                                //按类型名称
                                String fieldName = f.getType().getName();
                                f.set(instance, beanFactory.get(fieldName));
                            }
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }

            }

        }


        private String lowerClass(String simpleName) {
            char[] chars = simpleName.toCharArray();
            chars[0] += 32;
            String res = String.valueOf(chars);
            return res;
        }


    }
复制代码
Copy核心路由控制 handler:
复制代码
Copy    public class DispatcherHandler extends SimpleChannelInboundHandler {


        private static final String CONNECTION_KEEP_ALIVE = "keep-alive";
        private static final String CONNECTION_CLOSE = "close";
        private AnnotationApplicationContext annotationApplicationContext;
        private FullHttpRequest request;
        private FullHttpResponse response;
        private Channel channel;

        public DispatcherHandler(AnnotationApplicationContext annotationApplicationContext){
            this.annotationApplicationContext = annotationApplicationContext;
        }

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
            if(o instanceof FullHttpRequest) {
                channel = channelHandlerContext.channel();
                request = (FullHttpRequest) o;
                String uri = request.uri();   //   /paul-mvc/com.paul.controller/method-com.paul.controller
                System.out.println("uri: " + uri);
                if(uri.contains("?")){
                    int index = uri.indexOf("?");
                    uri = uri.substring(0,index);
                }
                Method m = (Method) annotationApplicationContext.handleMapping.get(uri);
                if (null == m) {
                    response = com.paul.http.HttpUtil.getNotFoundResponse();
                    writeResponse(true);
                    return;
                }
                //从容器里拿到controller 实例
                Object instance = annotationApplicationContext.controllerMapping.get(uri);
                Object[] args = handle(request, response, m);
                for (Object a : args) {
                    System.out.println("Object:" + a);
                }
                try {
                    response = (FullHttpResponse) m.invoke(instance, args);
                    writeResponse(false);
                } catch (Exception e) {
                    e.printStackTrace();
                    response = HttpUtil.getErroResponse();
                    writeResponse(true);
                }
            }

        }


        private static Object[] handle(FullHttpRequest req, FullHttpResponse resp,Method method) throws IOException, IllegalAccessException, InstantiationException {
            Map<String, List<String>> parameters = RequestParseUtil.getParamMap(req);
            //拿到当前执行的方法有哪些参数
            Class<?>[] paramClazzs = method.getParameterTypes();
            //根据参数的个数,new 一个参数的数据
            Object[] args = new Object[paramClazzs.length];

            int args_i = 0;
            int index = 0;
            for(Class<?> paramClazz:paramClazzs){
                if(FullHttpRequest.class.isAssignableFrom(paramClazz)){
                    args[args_i++] = req;
                }
                if(FullHttpResponse.class.isAssignableFrom(paramClazz)){
                    args[args_i++] = resp;
                }

                //判断requestParam  注解
                Annotation[] paramAns = method.getParameterAnnotations()[index];
                if(paramAns.length > 0){
                    for(Annotation paramAn:paramAns){
                        if(RequestParam.class.isAssignableFrom(paramAn.getClass())){
                            RequestParam rp = (RequestParam) paramAn;
                            args[args_i++] = RequestParseUtil.getParamValue(parameters, paramClazz, rp, method, index);
                        }
                    }
                }
                index ++;
            }


            return  args;
        }

        private void writeResponse(boolean forceClose){
            boolean close = isClose();
            if(!close && !forceClose){
                response.headers().add("Content-Length", String.valueOf(response.content().readableBytes()));
            }
            ChannelFuture future = channel.writeAndFlush(response);
            if(close || forceClose){
                future.addListener(ChannelFutureListener.CLOSE);
            }
        }
        private boolean isClose(){
            if(request.headers().contains("Connection", CONNECTION_CLOSE, true) ||
                    (request.protocolVersion().equals(HttpVersion.HTTP_1_0) &&
                            !request.headers().contains("Connection", CONNECTION_KEEP_ALIVE, true)))
                return true;
            return false;
        }
    }
复制代码

这样我们就通过 Netty Http 实现了一个 MVC 框架,当然这个框架还有待改进的地方。

目前方法参数与 request 匹配时必须使用 RequestParam 注解。对于没有实现接口的类如果注入时,@Autowired 注解必须指定实例名称。 以上两个问题因为目前无法获取参数名(不是参数类型)