自己写个Spring MVC

Spring MVC的工作流程

在 SpringBoot 之前,几乎所有的 Web 应用都是已 web.xml 为入口的,Spring MVC 也不例外,学习过 Servlet 的应该都理解,Spring MVC 其实就是对 Servlet 接口,Servlet 规范的一种实现。Servlet 提供了五个接口,其中两个接口最为核心,分别是 init 方法和 service 方法。

  1. init方法:init方法是在服务器装入 Servlet 时执行,在 Servlet 的生命周期中,它只执行一次。如它的名字,它做的就是初始化。
  2. service方法:service方法和客户端的请求相关,每当一个客户端发生一次请求,请求一个 HttpServlet 对象,该对象的 service() 方法就会被调用。

Spring MVC 的入口是从 web.xml 中配置的

<servlet>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>

开始的,也是 DispatcherServlet 完成了对 Servlet 接口的实现。对于Spring MVC的工作流程,在网上随便一搜会搜到如下的概括:

  1. 用户向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet。
  2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理。
  3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller。
  4. HandlerAdapter处理器适配器去执行Controller并得到ModelAndView,并层层返回给DispatcherServlet。
  5. DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图。
  6. DispatcherServlet将模型数据填充到视图中。
  7. DispatcherServlet将结果响应给用户。

其中,对于 HandlerMapping 和 HandlerAdapter 在阅读源码的过程中可能是不太好理解的。

  1. HandlerMapping:HandlerMapping是一个接口,这个接口返回的是一个请求访问时处理器映射器会返回具体的执行链(HandlerExecutionChain),其中包括拦截器和映射器,只是找到并不执行。执行链这里用到了设计模式中的责任链模式,每一个责任链的负责人只需要把自己的任务处理好就好。而HandlerMapping为什么是个接口呢,是因为 Spring MVC提供了三种不同的书写处理器映射器的方法,只不过我们最常用的是通过注解,通过@Controller@RequestMapping的方式去写我们的Controller。
  2. HandlerAdapter:和 HandlerMapping 一样,HandlerAdapter 也是个接口。HandlerAdapter 的意思的是适配器,用到的也是设计模式中的适配器模式,在 Spring MVC 中针对不同的 Handler 需要不同的适配器,例如对于@RequestMapping类型的 Handler 需要 RequestMappingHandlerAdapter 来处理,适配之后通过调用接口的 handle 方法就可以执行对应的方法了。

代码实现

不管是 Spring 还是 Spring MVC,又或是 Mybatis,Spring Data等等。其实在阅读源码或者自己实现的过程中会发现,这些提高开发效率的,封装型的框架,从头到尾离不开的就是 Java 的反射以及 Java 的动态代理。

结合反射以及AOP的思想,根据上面 Servlet 接口和 Spring mvc 流程的介绍以及平时对 Spring MVC 的使用,即使我们不看 Spring MVC 的源码其实也能把我们经常使用的功能简单的实现了。这里对于HandlerMapping 和 HandlerAdapter 我们也不需要设计的如此复杂,只需要实现我们平时最常用的一种就好。

流程设计

想象一下,我们在开发某个系统,我们写好了我们的 Spring MVC 控制层的代码,但是我们没有引入任何依赖,接下来怎么让我们的代码 Run 起来呢?

  1. 第一步当然还是要从web.xml入手,和 Spring MVC 一样,我们需要配置一个我们自己的 DispatcherServlet,这个 Servlet 继承自 HttpServlet。
  2. 创建完自己的 Servlet 之后就是重写上面提到过的两个核心方法:init和service。在 Servlet 的生命周期中,init 只执行一次,对于我们编写好的代码,我们需要把所有的urlPath以及我们的控制器和控制器内的方法做一个映射,这样每次客户端发起一次请求,调用 service 方法的时候可以通过这个 mapping 映射找到对应它该执行的方法。(由于功能比较简单另外没有实现 Interceptor 拦截器的功能,所以没有使用 Spring MVC 使用的执行链的形式)
  3. 找到方法后就该执行了,但是执行前,方法需要的参数我们还没有填充。参数分为很多种,有用@RequestParam修饰的基本数据类型,有数组,有Map,有对象等等。这里 Spring mvc 用到的是设计模式中的策略模式,针对不同类型的参数会有不同的 Resolver。策略模式的使用场景是对于系统中的多个类或是说多个场景,用来区分它们的只是他们的行为不同,像我们要做的参数的解析,数据源都是 HttpServletRequest 的 Attribute,只是我们对于 Attribute 的处理行为不同,我们需要把它填充到不同类型的参数上而已。
  4. 在对 Method 的参数进行填充后,一切准备就绪了,这时候执行 Method 就可以得到相应的返回值了,返回值就是我们需要的视图。我们常用的返回类型有两种,一种是根据路径直接返回一个指定的视图,另外一种是我们平时在Spring MVC中用@ResponseBody或者@RestController修饰的直接返回给前端一个JSON形式的串。这里很简单,其实就是根据不同的情况调用 Servlet 给我们提供的方法。

流程图

代码结构

HandleMapping

HandleMapping 的功能如上面所说,只是根据请求找到我们对应处理请求的 handler。这个类主要有两个函数,第一个是初始化,将代码中所有被@Controller@RequestMapping修饰的类和方法,以@RequestMapping的值作为key,以 Method 作为 value,初始化一个map。第二个就是根据 key 在刚才初始化的map中获取对应的 Method。

public class HandleMapping {
    private static final Map<String, HandlerMethod> mappings = new HashMap<>();

    public static void init() {
        Set<Class<?>> controllerSet = ReflectionUtils.getAllClass(Controller.class);
        controllerSet.forEach((controller) -> {
            RequestMapping requestMappingAnnotation = controller.getAnnotation(RequestMapping.class);
            if (requestMappingAnnotation == null) {
                throw new DumpException("controller '" + controller.getName() + "' must have a '@RequestMapping' annotation");
            }
            String parentPath = requestMappingAnnotation.value();
            Method[] methods = controller.getMethods();
            for (Method method : methods) {
                RequestMapping methodRequestMappingAnnotation = method.getAnnotation(RequestMapping.class);
                if (methodRequestMappingAnnotation == null) {
                    continue;
                }
                String path = methodRequestMappingAnnotation.value();
                try {
                    mappings.put(parentPath + path, new HandlerMethod(controller.newInstance(), method));
                } catch (Exception e) {
                    throw new DumpException("init controller failed,can not create instance for controller '" + controller.getName() + "'", e);
                }
            }
        });
    }

    public static HandlerMethod getHandler(String url) {
        HandlerMethod handleMethod = mappings.get(url);
        if (handleMethod == null) {
            throw new DumpException("path '" + url + "' can not find handle");
        }
        return handleMethod;
    }
}

注:HandlerMethod 是对 Method 的一个简单封装,除了包含method外,还有当前Class的实例,方便于我们后面直接用这个实例执行这个方法。

HandlerMethodArgumentResolver

public interface HandlerMethodArgumentResolver {

    Boolean support(Parameter parameter);

    Object resolveArgument(HttpServletRequest request, Class<?> requiredType, Parameter parameter);
}

HandleMapping 已经帮我们找到了具体的 Method,但是相关的参数还没有填充,HandlerMethodArgumentResolver 就是专门用来填充参数的。HandlerMethodArgumentResolver 是个接口,有两个方法:support 和 resolveArgument。support 用于判断当前的参数是否支持该Resolver解析填充,resolveArgument 用来做具体的解析填充操作。这里我只实现了两种常见的Resolver:一种是基于@RequestParam的基本数据类型的参数解析器 RequestParamResolver,一种是对象类型的参数解析器 RequestModelResolver。这里和Spring mvc不同,因为我暂时只实现了这两种,而对于Map,数组等形式的都暂不支持。为了方便识别对象,我规定需要解析参数的对象都需要用@RequestModel来修饰。

参数的解析其实就是根据参数的名称去 HttpServletRequest 对象中调用 getParameter() 来获取我们想要的参数,对于对象来说就是额外做一次反射。这里比较复杂的其实是对于参数类型的转换,因为我们通过 request.getParameter() 拿到的是 String 类型的数值,我们需要转换成参数本身需要的类型,所以这里我们需要一个参数转换的 converter。这个converter虽然写起来比较麻烦,但是很容易理解,这里就不赘述了,直接看代码都可以看懂。

HandleMethodAdapter

解析完参数就可以执行方法了,HandleMethodAdapter 除了负责参数的填充解析,还有就是负责调用这个方法,并得到具体的返回值。返回值这里我们强制规定只能返回 String,对于这个 String 类型的返回值,我们分成三种情况来处理:

  1. 方法用了@ResponseBody修饰。对于这种返回值,通过调用response.getWriter().print()来将返回值写入 response 中。
  2. 方法没用@ResponseBody修饰,返回值含有redirect:前缀。这代表该请求是一个重定向请求,这里用response.sendRedirect()将请求重定向。
  3. 方法没用@ResponseBody修饰,返回值也不含有redirect:前缀。这种返回值代表直接返回一个具体的视图,直接调用requestDispatcher.forward()即可。

DumpServletDispatcher

准备完成,在自己的 Servlet(继承自 HttpServlet) 中完成上述流程即可。其中 HandleMapping 的初始化放在 init() 中执行。

public class DumpServletDispatcher extends HttpServlet {

    @Override
    public final void init() {
        HandleMapping.init();
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String uri = req.getRequestURI();
        HandlerMethod handleMethod = HandleMapping.getHandler(uri);
        HandleMethodAdapter handleMethodAdapter = new HandleMethodAdapter();
        handleMethodAdapter.handle(req, resp, handleMethod);
    }
}

最后

以上其实就是对 Spring MVC 中的核心功能或者说我们最常用的功能的实现。其实我在大二的时候写过类似的框架,自己起了名字叫 Dump。里面除了包含 Spring MVC 的功能,还包含了 Spring 的 IOC,AOP,还有类似 Spring Data/Hibernate 的 ORM 层的功能。但是当时是在没有看 Spring 相关的源码的情况下写的,里面很多细节或是设计模式都没有学习到,只是因为当时学习了 Java 的反射和动态代理觉得可以实现一下就写了一版。最近看了源码之后想再好好设计实现一遍,目前还是只完成了上面提到过的这些 MVC 的部分,新的代码仓库如下,上面提到过的所有相关代码也都在里面,欢迎持续关注。

Dump:

编辑于 2020-06-04 02:09