从零设计一个mvc框架

从零设计一个mvc框架_第1张图片
image.png

作为一个JavaWeb开发人员,没有经过EJB时代,但是使用过SSH和SSM这些优秀的框架,框架减轻了开发者的开发量,让开发者更有效率的应对业务,但是,作为一个爱折腾的程序员,不应该仅仅局限于对于框架的使用,更应该深入原理,去了解它底层的一些设计思想,这篇博客不是介绍像Spring,SpringMVC这些框架是怎么设计的,而是由理论到实战的讲解以下mvc框架的一些思想,以便于出现问题的时候更好的定位问题,也为学习这些优秀的框架打下一个基础。

目录

  • Web项目目录
  • Servlet讲解
  • Tomcat讲解
  • 原生Servlet的实现方式
  • 基于Filter的mvc框架
  • 反射(方法调用)

一、JavaWeb目录结构

从零设计一个mvc框架_第2张图片
image.png

这是用IDEA创建,Maven构建的JavaWeb项目,先分析下这里的一些目录结构:

  • src:源文件(编译前的)
  • target:目标文件(编译后的)
  • META-INF:是工程自身相关的一些信息,元文件信息,通常由开发工具,环境自动生成
  • web.xml:完成servlet在web容器的注册,web.xml是Web应用程序的部署描述文件,是用来给Web服务器解析并获取Web应用程序相关描述的。
  • classes:用于存放java字节码文件
  • lib:用于存放该工程用到的库,例如servlet-api.jar等等
  • 备注:
    • 不按照sun公司的规范做应用web程序的结构,web容器找不到,比如,xml文件写错了,启动tomcat的时候会报错
    • 凡是客户端能访问的资源(.html,.jpg)必须跟WEB-INF在同一目录。即放在Web根目录下的资源,从客户端是可以通过URL地址直接访问
    • 在WEB-INF目录的classes及lib子目录下,都可以存放Java类文件。在运行时,Servlet容器的类加载器先加载classes目录下的类,再加载lib目录下的JAR文件(Java类库的打包文件)中的类,jar包是许多class文件的集合。因此,如果两个目录下存在同名的类,classes目录下的类具有优先权

二、Servlet概述

  • 什么是Servlet:Servlet 是基于 Java 技术的 web 组件,容器托管的,用于生成动态内容。像其他基于 Java 的组件技术一样, Servlet 也是基于平台无关的 Java 类格式,被编译为平台无关的字节码,可以被基于 Java 技术的 web server 动态加载并运行。容器,有时候也叫做 servlet 引擎,是 web server 为支持 servlet 功能扩展的部分。客户端 通过 Servlet 容器实现的请求/应答模型与 Servlet 交互。
  • 什么是Servlet容器:Servlet容器是web server或application server的一部分,提供基于请求/响应发送模型的网络服务,解码基于 MIME 的请求,并且格式化基于 MIME 的响应。Servlet 容器也包含了管理 Servlet 生命周期。
  • 案例说明:
    1、客户端(如 web 浏览器)发送一个 HTTP 请求到 web 服务器;
    2、Web 服务器接收到请求并且交给 servlet 容器处理,servlet 容器可以运行在与宿主 web 服务器同一个进 程中,也可以是同一主机的不同进程,或者位于不同的主机的 web 服务器中,对请求进行处理。
    3、servlet 容器根据 servlet 配置选择相应的 servlet,并使用代表请求和响应对象的参数进行调用。
    4、servlet通过请求对象得到远程用户,HTTP POST参数和其他有关数据可能作为请求的一部分随请求一 起发送过来。Servlet 执行我们编写的任意的逻辑,然后动态产生响应内容发送回客户端。发送数据到客户 端是通过响应对象完成的。
    5、一旦 servlet 完成请求的处理,servlet 容器必须确保响应正确的刷出,并且将控制权还给宿主 Web 服务 器。

三、Servlet生命周期

  • Servlet 生命周期:Servlet 加载--->实例化--->服务--->销毁。
  • init():在Servlet的生命周期中,仅执行一次init()方法。它是在服务器装入Servlet时执行的,负责初始化Servlet对象。可以配置服务器,以在启动服务器或客户机首次访问Servlet时装入Servlet。无论有多少客户机访问Servlet,都不会重复执行init()。
  • service():它是Servlet的核心,负责响应客户的请求。每当一个客户请求一个HttpServlet对象,该对象的Service()方法就要调用,而且传递给这个方法一个“请求”(ServletRequest)对象和一个“响应”(ServletResponse)对象作为参数。在HttpServlet中已存在Service()方法。默认的服务功能是调用与HTTP请求的方法相应的do功能。
  • destroy(): 仅执行一次,在服务器端停止且卸载Servlet时执行该方法。当Servlet对象退出生命周期时,负责释放占用的资源。一个Servlet在运行service()方法时可能会产生其他的线程,因此需要确认在调用destroy()方法时,这些线程已经终止或完成。

四、Servlet工作原理

Tomcat作为Servlet容器的一种实现,他的工作步骤如下:

从零设计一个mvc框架_第3张图片
image.png

步骤:

  1. Web Client 向Servlet容器(Tomcat)发出Http请求
  2. Servlet容器接收Web Client的请求
  3. Servlet容器创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中。
  4. Servlet容器创建一个HttpResponse对象
  5. Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给 HttpServlet 对象。
  6. HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息。
  7. HttpServlet调用HttpResponse对象的有关方法,生成响应数据。
  8. Servlet容器把HttpServlet的响应结果传给Web Client。
从零设计一个mvc框架_第4张图片
image.png

五、基于原生Servlet的Web项目

  • web.xml中配置Servlet配置信息

    AServlet
    com.sailfish.user.AServlet
  
  
    AServlet
    /AServlet
  
  • 在对应的Servlet类中编写服务
public class AServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("hello doGet()...");
    }

      public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("hello doPost()...");
    }
}

  • Servlet的缺点:
    1.在web.xml中需要配置多行代码,维护起来不方便。
    2.一个servlet的入口只有一个doPost或者doGet方法,如果在一个servlet中写好几个方法,怎么办?
    这样会导致代码结构很乱。
    3.servlet类与servlet容器高度耦合,每个方法中都有两个参数request response。和如果服务器启动,这两个参数没有办法启动。
    4.如果一个servlet类中有很多方法,浏览器对这些方法进行请求,url写起来很麻烦
    5.在servlet中如果要获取页面上表单中的数据,那么在方法中会写很多行

六、基于Filter的MVC框架

web程序是基于 M(模型)V(视图)C(控制器)设计的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从 Java 中分离了出来,所以它允许你的网页中只包含很少的脚本。

  • 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
  • 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Java中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Jsp已经很好的实现了View层中的部分功能。
  • 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。
从零设计一个mvc框架_第5张图片
image.png

思路:

  • 如上图所示,这个框架的基本思路是请求进入应用之后,首先经过filter进行处理,filter根据request的contextPath路由(route)到对应的处理类(controller)的具体方法(method)去执行。
  • mvc中最主要的是c,这里的filter也主要是实现了控制器的功能,上面也介绍了servlet的生命周期,在tomcat启动的时候会调用init方法,filter作为特殊的servlet,也是这样的,在tomcat启动时调用了init方法

主体思路有了,就是思考怎么实现它,上面的三个关键词:route,path,controller,method,这里的mvc框架业主要做的就是这件事情,思路是这样的:

  • 这里要进行路由,就必须要有path,controller和method的对应关系,这里需要一个数据结构来定义它。
public class Route {

    /**
     * 路由path
     */
    private String path;

    /**
     * 执行路由的方法
     */
    private Method action;

    /**
     * 路由所在的控制器
     */
    private Object controller;
}
  • 只是有了数据结构之后,这些对应关系存储在哪里呢?受池化思想的影响,我们可以在应用加载的时候将这部分映射信息首先建立起来(处理类是有限的),使用一个List来进行进行路由器的管理
public class Routers {

    private static final Logger LOGGER = Logger.getLogger(Routers.class.getName());
    
    private List routes = new ArrayList();
    
    public Routers() {
    }
    
    public void addRoute(List routes){
        routes.addAll(routes);
    }
    
    public void addRoute(Route route){
        routes.add(route);
    }
    
    public void removeRoute(Route route){
        routes.remove(route);
    }
    
    public void addRoute(String path, Method action, Object controller){
        Route route = new Route();
        route.setPath(path);
        route.setAction(action);
        route.setController(controller);
        
        routes.add(route);
        LOGGER.info("Add Route:[" + path + "]");
    }

    public List getRoutes() {
        return routes;
    }

    public void setRoutes(List routes) {
        this.routes = routes;
    }
    
}
  • 将路由存储起来之后,最后的目标是要根据request的path进行匹配,这里还需要一个Matcher来进行匹配路由
/**
     * 根据path查找路由
     * @param path  请求地址
     * @return      返回查询到的路由
     */
    public Route findRoute(String path) {
        String cleanPath = parsePath(path);
        List matchRoutes = new ArrayList();
        for (Route route : this.routes) {
            if (matchesPath(route.getPath(), cleanPath)) {
                matchRoutes.add(route);
            }
        }
        // 优先匹配原则
        giveMatch(path, matchRoutes);
        
        return matchRoutes.size() > 0 ? matchRoutes.get(0) : null;
    }
  • 具体的映射信息是在应用中才能确定的,在框架中是没办法知道的,这里定义一个启动类(接口),让子类去实现它
public interface Bootstrap {

    /**
     * 初始化方法
     * @param mario 全局对象
     */
    void init(Mario mario);
    
}
  • 子类实现Bootstrap,将子类的配置信息(Route),数据库配置信息进行加载
public class App implements Bootstrap {

    @Override
    public void init(Mario mario) {
        //实例化控制类(controller)
        Index index = new Index();
        UserController userController = new UserController();
        //添加请求路径和action的映射关系
        mario.addRoute("/", "index", index);
        mario.addRoute("/hello", "hello", index);
        mario.addRoute("/html", "html", index);
        
        mario.addRoute("/users", "users", userController);
        mario.addRoute("/user/add", "show_add", userController);
        mario.addRoute("/user/save", "save", userController);
        mario.addRoute("/user/edit", "edit", userController);
        mario.addRoute("/user/update", "update", userController);
        mario.addRoute("/user/del", "delete", userController);

    }
  • 有这些充足的准备之后就是什么时候来加载了,我们在filter的init方法中将映射关系进行初始化,并将Route添加到RouteMatcher中
@Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOGGER.info(">>MarioFilter-init......");
        //创建mario实例
        Mario mario = Mario.me();
        if(!mario.isInit()){
            
            String className = filterConfig.getInitParameter("bootstrap"); //com.mario.demo.App
            Bootstrap bootstrap = this.getBootstrap(className);
            bootstrap.init(mario);

            /**
             *  应用已经加载完成,mario中包含了:
             *  1、route(路由)
             *  2、资源文件(配置加载器)
             *  3、是否已经加载的标识
             *  4、渲染器(render)
             */
            Routers routers = mario.getRouters();
            if(null != routers){
                routeMatcher.setRoutes(routers.getRoutes());
            }
            servletContext = filterConfig.getServletContext();
            
            mario.setInit(true);
        }
    }
  • Web服务器启动后有请求的话会执行doFilter方法中,调用routeMatcher.findRoute(uri);找到合适的Route,然后执行handle方法进行实际调用
@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        request.setCharacterEncoding(Const.DEFAULT_CHAR_SET);
        response.setCharacterEncoding(Const.DEFAULT_CHAR_SET);
        
        // 请求的uri
        String uri = PathUtil.getRelativePath(request); // users,当前请求的路径
        
        LOGGER.info("Request URI:" + uri);

        //找到合适的路由
        Route route = routeMatcher.findRoute(uri);
        /**
         * Route:
         * 1、path:/users
         * 2、action:public void com.mario.demo.controller.UserController.users(com.junicorn.mario.servlet.wrapper.Request,com.junicorn.mario.servlet.wrapper.Response)
         * 3、controller:userController
         */

        // 如果找到
        if (route != null) {
            // 实际执行方法
            handle(request, response, route);
        } else{
            chain.doFilter(request, response);
        }
    }
  • handler方法详解:利用反射机制调用了对应method
private void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Route route){
        
        // 初始化上下文
        Request request = new Request(httpServletRequest);
        Response response = new Response(httpServletResponse);
        MarioContext.initContext(servletContext, request, response);
        
        Object controller = route.getController();
        // 要执行的路由方法
        Method actionMethod = route.getAction(); //actionMethod:public void com.mario.demo.controller.UserController.users(com.junicorn.mario.servlet.wrapper.Request,com.junicorn.mario.servlet.wrapper.Response)
        // 执行route方法
        executeMethod(controller, actionMethod, request, response);
    }

     /**
     * 执行路由方法
     */
    private Object executeMethod(Object object, Method method, Request request, Response response){
        int len = method.getParameterTypes().length;
        //允许对反射私有方法操作
        method.setAccessible(true);
        if(len > 0){
            Object[] args = getArgs(request, response, method.getParameterTypes());
            return ReflectUtil.invokeMehod(object, method, args);
        } else {
            return ReflectUtil.invokeMehod(object, method);
        }
    }
  • 如果对于反射不太理解的同学可以看下这篇文章: Method.invoke方法详解

七、总结

备注:本片博客使用的案例来源于王爵的项目,他的项目已经足够简洁了,我觉得目前为止我也写不出比他好的

  • 这个基于filter的mvc框架肯定是不能用在生产环境中,但是这里介绍了一种实现框架的思路,对于框架的实现并不是想象那么难,掌握一些基础组件的原理之后结合自己的经验还是可以实现一些东西的。
  • 对于JavaWeb开发者来说,这篇博客可能会让你分清楚哪些是Web服务器做的事情,哪些是框架做的事情,其实有些事情难得就是不知道怎么去分界线
  • 当有人问起tomcat启动时都发生了一些什么事情,也会知道说点什么了
  • 不要重复造轮子,但是只有造了轮子你才能知道轮子怎么走路的

你可能感兴趣的:(从零设计一个mvc框架)