This website requires JavaScript.

从零开始实现一个简易的 Java MVC 框架(七)--实现 MVC

前言

标题是‘从零开始实现一个简易的 Java MVC 框架’,结果写了这么多才到实现 MVC 的时候。.. 只能说前戏确实有点多了。不过这些前戏都是必须的,如果只是简简单单实现一个 MVC 的功能那就没有意思了,要有 Bean 容器、IOC、AOP 和 MVC 才像是一个'框架'嘛。

实现准备

为了实现 mvc 的功能,先要为 pom.xml 添加一些依赖。

<properties>
    ...
    <tomcat.version>8.5.31</tomcat.version>
    <jstl.version>1.2</jstl.version>
    <fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
	...
    <!-- tomcat embed -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <version>${tomcat.version}</version>
    </dependency>

    <!-- JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>${jstl.version}</version>
        <scope>runtime</scope>
    </dependency>

    <!-- FastJson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>
</dependencies>
  • tomcat-embed-jasper这个依赖是引入了一个内置的 tomcat,*spring-boot *默认就是引用这个嵌入式的 tomcat 包实现直接启动服务的。这个包除了加入了一个嵌入式的 tomcat,还引入了java.servlet-apijsp-api这两个包,如果不想用这种嵌入式的 tomcat 的话,可以去除tomcat-embed-jasper然后引入这两个包。

  • jstl用于解析 jsp 表达式的,比如在 jsp 页面编写下面这样c:forEach语句就需要这个包。

    <c:forEach items="${list}" var="user">
    <tr>
          <td>${user.id}</td>
          <td>${user.name}</td>
    </tr>
    </c:forEach>
    
  • fastjson是阿里开发的一个 json 解析包,用于将实体类转换成 json。类似的包还有GsonJackson等,这里就不具体比较了,可以挑选一个自己喜欢的。

实现 MVC

MVC 实现原理

首先我们要了解到 MVC 的实现原理,在使用* spring-boot 编写项目的时候,我们通常都是通过编写一系列的 Controller 来实现一个个链接,这是'现代'的写法。但是在以前 springmvc 甚至是 struts2 *这类 mvc 框架都还没流行的时候,都是通过编写Servlet来实现。

每一个请求都会对应一个Servlet,然后还要在 web.xml 中配置这个Servlet,然后对请求的接收和处理啥的都分布在一大堆的Servlet中,代码十分混杂。

为了让人们编写的时候更专注于业务代码而减少对请求的处理,*springmvc *就通过一个中央的Servlet,处理这些请求,然后再转发到对应的 Controller 中,这样就只有一个Servlet统一处理请求了。下面的一段话来自 spring 的官方文档 https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-servlet

Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification using Java configuration or in web.xml. In turn the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.

这段大致意思就是:*springmvc *通过中心 Servlet(DispatcherServlet) 来实现对控制 controller 的操作。这个Servlet要通过 java 配置或者配置在 web.xml 中,它用于寻找请求的映射(即找到对应的 controller),视图解析(即执行 controller 的结果),异常处理(即对执行过程的异常统一处理)等等

所以实现 MVC 的效果就是以下几点:

  1. 通过一个中央 sevlet 如DispatcherServlet来接收所有请求
  2. 根据请求找到对应的 controller
  3. 执行 controller 获取结果
  4. 对 controller 的结果解析并转到对应视图
  5. 若有异常则统一处理异常

根据上面的步骤,我们先从步骤 2、3、4、5 开始,最后再实现 1 完成 mvc。

创建注解

为了方便实现,先在 com.zbw.mvc.annotation 包下创建三个注解和一个枚举:RequestMappingRequestParamResponseBodyRequestMethod

package com.zbw.mvc.annotation;
import ...

/**
 * http 请求路径
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /**
     * 请求路径
     */
    String value() default "";

    /**
     * 请求方法
     */
    RequestMethod method() default RequestMethod.GET;
}
package com.zbw.mvc.annotation;

/**
 * http 请求类型
 */
public enum RequestMethod {
    GET, POST
}
package com.zbw.mvc.annotation;
import ...

/**
 * 请求的方法参数名
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    /**
     * 方法参数别名
     */
    String value() default "";

    /**
     * 是否必传
     */
    boolean required() default true;
}
package com.zbw.mvc.annotation;
import ...

/**
 * 用于标记返回 json
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

这几个类的作用就不解释了,都是* springmvc *最常见的注解。

创建 ModelAndView

为了能够方便的传递参数到前端,创建一个工具 bean,相当于* spring *中简化版的ModelAndView。这个类创建于 com.zbw.mvc.bean 包下

package com.zbw.mvc.bean;
import ...

/**
 * ModelAndView
 */
public class ModelAndView {

    /**
     * 页面路径
     */
    private String view;

    /**
     * 页面 data 数据
     */
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView setView(String view) {
        this.view = view;
        return this;
    }
    public String getView() {
        return view;
    }
    public ModelAndView addObject(String attributeName, Object attributeValue) {
        model.put(attributeName, attributeValue);
        return this;
    }
    public ModelAndView addAllObjects(Map<String, ?> modelMap) {
        model.putAll(modelMap);
        return this;
    }
    public Map<String, Object> getModel() {
        return model;
    }
}

实现 Controller 分发器

Controller 分发器类似于 Bean 容器,只不过后者是存放 Bean 的而前者是存放 Controller 的,然后根据一些条件可以简单的获取对应的 Controller。

先在 com.zbw.mvc 包下创建一个ControllerInfo类,用于存放 Controller 的一些信息。

package com.zbw.mvc;
import ...

/**
 * ControllerInfo 存储 Controller 相关信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
    /**
     * controller 类
     */
    private Class<?> controllerClass;

    /**
     * 执行的方法
     */
    private Method invokeMethod;

    /**
     * 方法参数别名对应参数类型
     */
    private Map<String, Class<?>> methodParameter;
}

然后再创建一个PathInfo类,用于存放请求路径和请求方法类型

package com.zbw.mvc;
import ...

/**
 * PathInfo 存储 http 相关信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
    /**
     * http 请求方法
     */
    private String httpMethod;

    /**
     * http 请求路径
     */
    private String httpPath;
}

接着创建 Controller 分发器类ControllerHandler

package com.zbw.mvc;
import ...

/**
 * Controller 分发器
 */
@Slf4j
public class ControllerHandler {

    private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();

    private BeanContainer beanContainer;

    public ControllerHandler() {
        beanContainer = BeanContainer.getInstance();
        Set<Class<?>> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
        for (Class<?> clz : classSet) {
            putPathController(clz);
        }
    }

    /**
     * 获取 ControllerInfo
     */
    public ControllerInfo getController(String requestMethod, String requestPath) {
        PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
        return pathControllerMap.get(pathInfo);
    }

    /**
     * 添加信息到 requestControllerMap 中
     */
    private void putPathController(Class<?> clz) {
        RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
        String basePath = controllerRequest.value();
        Method[] controllerMethods = clz.getDeclaredMethods();
        // 1. 遍历 Controller 中的方法
        for (Method method : controllerMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                // 2. 获取这个方法的参数名字和参数类型
                Map<String, Class<?>> params = new HashMap<>();
                for (Parameter methodParam : method.getParameters()) {
                    RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
                    if (null == requestParam) {
                        throw new RuntimeException("必须有 RequestParam 指定的参数名");
                    }
                    params.put(requestParam.value(), methodParam.getType());
                }
                // 3. 获取这个方法上的 RequestMapping 注解
                RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
                String methodPath = methodRequest.value();
                RequestMethod requestMethod = methodRequest.method();
                PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
                if (pathControllerMap.containsKey(pathInfo)) {
                    log.error("url:{} 重复注册", pathInfo.getHttpPath());
                    throw new RuntimeException("url 重复注册");
                }
                // 4. 生成 ControllerInfo 并存入 Map 中
                ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
                this.pathControllerMap.put(pathInfo, controllerInfo);
                log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}",
                        pathInfo.getHttpMethod(), pathInfo.getHttpPath(),
                        controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
            }
        }
    }
}

这个类最复杂的就是构造函数中调用的putPathController()方法,这个方法也是这个类的核心方法,实现了 controller 类中的信息存放到pathControllerMap变量中的功能。大概讲解一些这个类的功能流程:

  1. 在构造方法中获取 Bean 容器BeanContainer的单例实例
  2. 获取并遍历BeanContainer中存放的被RequestMapping注解标记的类
  3. 遍历这个类中的方法,找出被RequestMapping注解标记的方法
  4. 获取这个方法的参数名字和参数类型,生成ControllerInfo
  5. 根据RequestMapping里的value()method()生成PathInfo
  6. 将生成的PathInfoControllerInfo存到变量pathControllerMap
  7. 其他类通过调用getController()方法获取到对应的 controller

以上就是这个类的流程,其中有个注意的点:

步骤 4 的时候,必须规定这个方法的所有参数名字都被RequestParam注解标注,这是因为在 java 中,虽然我们编写代码的时候是有参数名的,比如String name这样的形式,但是被编译成 class 文件后‘name’这个字段就会被擦除,所以必须要通过一个RequestParam来保存名字。

但是大家在* springmvc 中并不用必须每个方法都用注解标记的,这是因为 spring 中借助了asm* ,这种工具可以在编译之前拿到参数名然后保存起来。还有一种方法是在 java8 之后支持了保存参数名,但是必须修改编译器的参数来支持。这两种方法实现起来都比较复杂或者有限制条件,这里就不实现了,大家可以查找资料自己实现

实现结果执行器

接下来实现结果执行器,这个类中实现刚才 mvc 流程中的步骤 3、4、5。

在 com.zbw.mvc 包下创建类ResultRender

package com.zbw.mvc;
import ...

/**
 * 结果执行器
 */
@Slf4j
public class ResultRender {

    private BeanContainer beanContainer;

    public ResultRender() {
        beanContainer = BeanContainer.getInstance();
    }

    /**
     * 执行 Controller 的方法
     */
    public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
        // 1. 获取 HttpServletRequest 所有参数
        Map<String, String> requestParam = getRequestParams(req);
        // 2. 实例化调用方法要传入的参数值
        List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);

        Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
        Method invokeMethod = controllerInfo.getInvokeMethod();
        invokeMethod.setAccessible(true);
        Object result;
        // 3. 通过反射调用方法
        try {
            if (methodParams.size() == 0) {
                result = invokeMethod.invoke(controller);
            } else {
                result = invokeMethod.invoke(controller, methodParams.toArray());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 4. 解析方法的返回值,选择返回页面或者 json
        resultResolver(controllerInfo, result, req, resp);
    }

    /**
     * 获取 http 中的参数
     */
    private Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        //GET 和 POST 方法是这样获取请求参数的
        request.getParameterMap().forEach((paramName, paramsValues) -> {
            if (ValidateUtil.isNotEmpty(paramsValues)) {
                paramMap.put(paramName, paramsValues[0]);
            }
        });
        // TODO: Body、Path、Header 等方式的请求参数获取
        return paramMap;
    }

    /**
     * 实例化方法参数
     */
    private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
        return methodParams.keySet().stream().map(paramName -> {
            Class<?> type = methodParams.get(paramName);
            String requestValue = requestParams.get(paramName);
            Object value;
            if (null == requestValue) {
                value = CastUtil.primitiveNull(type);
            } else {
                value = CastUtil.convert(type, requestValue);
                // TODO: 实现非原生类的参数实例化
            }
            return value;
        }).collect(Collectors.toList());
    }

    /**
     * Controller 方法执行后返回值解析
     */
    private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
        if (null == result) {
            return;
        }
        boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
        if (isJson) {
            // 设置响应头
            resp.setContentType("application/json");
            resp.setCharacterEncoding("UTF-8");
            // 向响应中写入数据
            try (PrintWriter writer = resp.getWriter()) {
                writer.write(JSON.toJSONString(result));
                writer.flush();
            } catch (IOException e) {
                log.error("转发请求失败", e);
                // TODO: 异常统一处理,400 等。..
            }
        } else {
            String path;
            if (result instanceof ModelAndView) {
                ModelAndView mv = (ModelAndView) result;
                path = mv.getView();
                Map<String, Object> model = mv.getModel();
                if (ValidateUtil.isNotEmpty(model)) {
                    for (Map.Entry<String, Object> entry : model.entrySet()) {
                        req.setAttribute(entry.getKey(), entry.getValue());
                    }
                }
            } else if (result instanceof String) {
                path = (String) result;
            } else {
                throw new RuntimeException("返回类型不合法");
            }
            try {
                req.getRequestDispatcher("/templates/" + path).forward(req, resp);
            } catch (Exception e) {
                log.error("转发请求失败", e);
                // TODO: 异常统一处理,400 等。..
            }
        }
    }
}

通过调用类中的invokeController()方法反射调用了 Controller 中的方法并根据结果解析对应的页面。主要流程为:

  1. 调用getRequestParams() 获取 HttpServletRequest 中参数
  2. 调用instantiateMethodArgs() 实例化调用方法要传入的参数值
  3. 通过反射调用目标 controller 的目标方法
  4. 调用resultResolver()解析方法的返回值,选择返回页面或者 json

通过这几个步骤算是凝聚了 MVC 核心步骤了,不过由于篇幅问题,几乎每一步骤得功能都有所精简,如

  • 步骤 1 获取 HttpServletRequest 中参数只获取 get 或者 post 传的参数,实际上还有 Body、Path、Header 等方式的请求参数获取没有实现
  • 步骤 2 实例化调用方法的值只实现了 java 的原生参数,自定义的类的实例化没有实现
  • 步骤 4 异常统一处理也没具体实现

虽然有缺陷,但是一个 MVC 流程是完成了。接下来就要把这些功能组装一下了。

实现 DispatcherServlet

终于到实现开头说的DispatcherServlet了,这个类继承于HttpServlet,所有请求都从这里经过。

在 com.zbw.mvc 下创建DispatcherServlet

package com.zbw.mvc;
import ...

/**
 * DispatcherServlet 所有 http 请求都由此 Servlet 转发
 */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    private ControllerHandler controllerHandler = new ControllerHandler();

    private ResultRender resultRender = new ResultRender();

    /**
     * 执行请求
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置请求编码方式
        req.setCharacterEncoding("UTF-8");
        //获取请求方法和请求路径
        String requestMethod = req.getMethod();
        String requestPath = req.getPathInfo();
        log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
        if (requestPath.endsWith("/")) {
            requestPath = requestPath.substring(0, requestPath.length() - 1);
        }

        ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
        log.info("{}", controllerInfo);
        if (null == controllerInfo) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        resultRender.invokeController(req, resp, controllerInfo);
    }
}

在这个类里调用了ControllerHandlerResultRender两个类,先根据请求的方法和路径获取对应的ControllerInfo,然后再用ControllerInfo解析出对应的视图,然后就能访问到对应的页面或者返回对应的 json 信息了。

然而一直在说的所有请求都从DispatcherServlet经过好像没有体现啊,这是因为要配置 web.xml 才行,现在很多都在使用* spring-boot 的朋友可能不大清楚了,在以前使用 spring*mvc+spring+*mybatis *时代的时候要写很多配置文件,其中一个就是 web.xml,要在里面添加上。通过通配符*让所有请求都走的是 DispatcherServlet。

<servlet>
	<servlet-name>springMVC</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>springMVC</servlet-name>
	<url-pattern>*</url-pattern>
</servlet-mapping>

不过我们无需这样做,为了致敬* spring-boot*,我们会在下一节实现内嵌 Tomcat,并通过启动器启动。

缺陷

可能这一节的代码让大家看起来不是很舒服,这是因为目前这个代码虽然说功能已经是实现了,但是代码结构还需要优化。

首先DispatcherServlet是一个请求分发器,这里面不应该有处理 Http 的逻辑代码的

其次我们把 MVC 步骤的 3、4、5 的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。

还有目前也没实现异常的处理,不能返回异常页面给用户。

这些优化工作会在后期的章节完成的。


源码地址:doodle

原文地址:从零开始实现一个简易的 Java MVC 框架(七)--实现 MVC

0条评论
avatar