转载

从零开始实现一个简易的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的官方文档 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包下创建三个注解和一个枚举: 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中的方法并根据结果解析对应的页面。主要流程为:

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);
    }
}

复制代码

在这个类里调用了 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的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。

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

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

  • 从零开始实现一个简易的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...