转载

从零开始实现一个简易的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-api 和 jsp-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。类似的包还有 Gson 和 Jackson 等,这里就不具体比较了,可以挑选一个自己喜欢的。

实现MVC

MVC实现原理

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

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

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

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的效果就是以下几点:

DispatcherServlet

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

创建注解

为了方便实现,先在com.zbw.mvc.annotation包下创建三个注解和一个枚举: RequestMapping 、 RequestParam 、 ResponseBody 、 RequestMethod 。

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. 将生成的 PathInfo 和 ControllerInfo 存到变量 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中的方法并根据结果解析对应的页面。主要流程为:

getRequestParams()
instantiateMethodArgs()
resultResolver()

通过这几个步骤算是凝聚了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);
    }
}

复制代码

在这个类里调用了 ControllerHandler 和 ResultRender 两个类,先根据请求的方法和路径获取对应的 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的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。

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

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

  • 从零开始实现一个简易的Java MVC框架(一)--前言
  • 从零开始实现一个简易的Java MVC框架(二)--实现Bean容器
  • 从零开始实现一个简易的Java MVC框架(三)--实现IOC
  • 从零开始实现一个简易的Java MVC框架(四)--实现AOP
  • 从零开始实现一个简易的Java MVC框架(五)--引入aspectj实现AOP切点
  • 从零开始实现一个简易的Java MVC框架(六)--加强AOP功能
  • 从零开始实现一个简易的Java MVC框架(七)--实现MVC
  • 从零开始实现一个简易的Java MVC框架(八)--制作Starter
  • 从零开始实现一个简易的Java MVC框架(九)--优化MVC代码

源码地址: doodle

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

原文  https://juejin.im/post/5b5920175188251af662301e
正文到此结束
Loading...