声明

本篇文章除部分引用外,均为原创内容,如有雷同纯属巧合,引用转载请附上原文链接与声明。

阅读条件

读本篇文章需掌握java基础知识,了解Spring MVC请求转发原理,掌握注解的使用方式,HTTP基本知识,Servlet基础,灵活运用反射,阅读自研Spring IOC(一)

注意

本文若包含部分下载内容,本着一站式阅读的想法,本站提供其对应软件的直接下载方式,但是由于带宽原因下载缓慢是必然的,建立读者去相关官网进行下载,若某些软件禁止三方传播,请在主页上通过联系作者的方式将相关项目进行取消。

相关文章
文章大纲
  • 注解&枚举定义
  • RequestHandleMapping
  • DispatchServlet
  • 参数解析工具
  • EmbedTomcat嵌入
  • 全链路整合
  • 使用举例
简述

项目github地址传送门点此 ,该篇文章对应的代码在mvc-1.0分支上
该篇文章简述如何实现一个简易的MVC框架,做到MVC框架里面最重要的请求转发功能,为了实现该功能,需要设计实现转发规则,提供全局唯一DispatchServlet,解析请求参数到指定控制器的指定方法,并内嵌EmbedTomcat以提供。从客户端发出请求,到请求转发获取结果整个过程如下

  • 获取请求路径与请求方法
  • 根据请求路径与请求方法找到对应的RequestHandleMapping
  • 将请求托管给RequestHandleMapping
  • 解析请求提交的参数
  • 将请求转发到RequestHandleMapping所绑定的Controller上的指定方法
  • 获取执行结果
  • 输出结果到客户端
注解&枚举定义
RequestMethod枚举定义

该枚举定义HTTP请求的方法种类,用于在请求分发时查找对应的处理方法,该类定义了常规的HTTP请求方法,如下

public enum RequestMethod {
    GET("GET"),
    HEAD("HEAD"),
    POST("POST"),
    PUT("PUT"),
    DELETE("DELETE"),
    OPTIONS("OPTIONS"),
    TRACE("TRACE");

    @Getter
    private String value;

    RequestMethod(String value) {
        this.value = value;
    }
}
定义请求转发注解@RequestMapping

该注解用于定义请求路径和处理方法的映射方式,与Spring MVC中的@RequestMapping定位一致,该路径通过value获取,映射的请求方法为上文中的RequestMethod,源码如下

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

    /**
     * 映射路径
     *
     * @return
     */
    String value() default "";

    /**
     * 请求方法
     *
     * @return
     */
    RequestMethod method();
}
RequestHandleMapping

该类用于在请求定义请求路径,请求方法,处理方法之间的映射关系,而处理方法实际上是Controller中的某个方法,并且提供了是否匹配请求路径,请求方法是否匹配方法以及处理请求的方法,当请求到来时,只需要找到对应的RequestHandleMaping,则将请求派发到该类中进行执行,该类的定义如下

@Getter
@Setter
public class RequestHandleMapping {

    /**
     * 响应的路径
     */
    private String mapping;
    /**
     * 对应的Controller
     */
    private Object controller;
    /**
     * 对应的处理方法
     */
    private Method method;
    /**
     * 请求方法
     */
    private RequestMethod requestMethod;
    /**
     * 是否匹配该请求
     * @param mapping
     * @param requestMethod
     * @return
     */
    public boolean isMatch(String mapping, String requestMethod) {
        return this.mapping.equals(mapping) && this.requestMethod.getValue().equalsIgnoreCase(requestMethod);
    }

    /**
     * 处理请求
     * @param req
     * @param res
     */
    public void handle(ServletRequest req, ServletResponse res) {
        try {
            Object returnValue = method.invoke(controller, ParamUtil.extractParamFromRequest(req, method));
            res.getWriter().write(returnValue.toString());
        } catch (IllegalAccessException | InvocationTargetException | IOException e) {
            log.error("call method [{}] of controller [{}] fail by reflect", method.getName(), controller.getClass().getName());
        }
    }
}
DispatchServlet

该类是请求的分发器,也是全局唯一一个Servlet,需要注意的是,设计该类时一定要保持无状态设计,因为该类并不能参与到业务中,该类的作用就是迅速转发请求到指定的请求处理方法中,该模式是前端控制器模式的体现。请求转发主要经过以下几个流程

  • 获取请求方法
  • 根据请求方法进一步派发请求到指定的方法中
  • 获取请求路径,根据请求路径和请求方法找到指定的RequestHandleMapping
  • 根据获取的RequestHandleMapping,将请求派发到Controller的指定方法中进行处理
  • 获取处理结果,并将结果输出到请求客户端

首先在pom.xml中新增Servlet支持

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

设计如下

public class DispatchServlet extends HttpServlet {

    private static final String CONTENT_TYPE = "application/json;charset=UTF-8";

    private Set<RequestHandleMapping> requestHandleMappings;

    public DispatchServlet(Set<RequestHandleMapping> requestHandleMappings) {
        this.requestHandleMappings = requestHandleMappings;
    }

    @Override
    public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
        if (req instanceof HttpServletRequest && resp instanceof HttpServletResponse) {
            resp.setContentType(CONTENT_TYPE);
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) resp;
            doDispatch(request, response);
        } else {
            throw new ServletException("non-HTTP request or response");
        }
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) {
        String method = req.getMethod();
        if (method.equals("GET")) {
            doGet(req, resp);
        } else if (method.equals("HEAD")) {
            doHead(req, resp);
        } else if (method.equals("POST")) {
            doPost(req, resp);
        } else if (method.equals("PUT")) {
            doPut(req, resp);
        } else if (method.equals("DELETE")) {
            doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            doTrace(req, resp);
        } else {
            doUnsupportedType(req, resp);
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doHead(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    @Override
    protected void doTrace(HttpServletRequest req, HttpServletResponse res) {
        getRequestMapping(req.getRequestURI(), req.getMethod()).handle(req, res);
    }

    private void doUnsupportedType(HttpServletRequest req, HttpServletResponse resp) {
        try {
            resp.getWriter().write("unsupported method :" + req.getMethod());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private RequestHandleMapping getRequestMapping(String requestUrl, String method) {
        for (RequestHandleMapping requestHandleMapping : requestHandleMappings) {
            if (requestHandleMapping.isMatch(requestUrl, method)) {
                return requestHandleMapping;
            }
        }
        throw new RuntimeException("can't find the right requestHandleMapping for requestUrl:" + requestUrl + ",method :" + method);
    }
}
参数解析工具

在请求派发到指定Controller的指定方法时,需要将客户端提交的参数解析为指定方法所需要的参数类型,实际上这里一般是做的反序列化等操作,但是笔者这里简单实现,统一返回一个指定的String类型字符串即可,设计如下

public class ParamUtil {

    /**
     * 从请求中抽取相关的参数,并组装成调用方法的参数数据进行方法。
     * 这个方法模拟的是spring mvc关于调用前对于参数的前置处理器所做的业务操作
     * 这里为了简化操作,默认返回一个 string参数
     *
     * @param req
     * @param method
     * @return
     */
    public static Object[] extractParamFromRequest(ServletRequest req, Method method) {
        return new String[]{"the default test param"};
    }
}
EmbedTomcat嵌入

为了支持Web service,项目需要引入嵌入式Tomcat,这样在初始化完毕时启动Tomcat,并将Tomcat启动,这样请求才能映射到指定的controller,嵌入EmbedTomcat只需要在pom.xml加入以下依赖

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>8.0.48</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>8.0.48</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-logging-juli</artifactId>
    <version>8.0.48</version>
</dependency>
全链路整合
编写SpringMVCContext

该类提供mvc模块的全部能力,外部只需要调用doMvc方法时,即可完成映射的创建,DispatchServlet初始化,EmbedTomcat启动,即开始对外提供服务,所以该类的doMvc方法有以下三个流程

  • 从ioc容器中获取Controller
  • 根据获取的Controller构建RequestHandleMapping集合
  • 根据获取的RequestHandleMapping构建DispatchServlet
  • 配置EmbedTomcat的初始化参数,比如监听端口,工作空间,监听根路径...等等
  • 将DispatchServlet通过配置到EmbedTomcat中
  • 启动Tomcat

所以设计的SpringMVCContext如下

public class SpringMVCContext {

    private static final int PORT = 80;
    private static final String CONTEXT_PATH = "/";
    private static final String BASE_DIR = "temp";
    private static final String DISPATCH_SERVLET_NAME = "dispatchServlet";
    private static final String URL_PATTERN = "/";

    /**
     * 根据ioc容器创建相关的handlerMapping 完成MVC的操作
     */
    public static void doMvc() {
        Set<RequestHandleMapping> requestHandleMappings = buildRequestHandleMappings();
        DispatchServlet dispatchServlet = buildDispatchServlet(requestHandleMappings);
        startTomcatService(dispatchServlet);
    }

    /**
     * 根据容器中被管理的controller bean组装 requestHandleMapping
     */
    public static Set<RequestHandleMapping> buildRequestHandleMappings() {
        Set<RequestHandleMapping> requestHandleMappings = new HashSet<>();
        Set<?> controllerBeans = BeanContainer.getBeansByAnnotation(Controller.class);
        for (Object controllerBean : controllerBeans) {
            String basePath = controllerBean.getClass().getAnnotation(Controller.class).value();
            Method[] methods = controllerBean.getClass().getDeclaredMethods();
            for (Method method : methods) {
                if (isHandleMappingMethod(method)) {
                    RequestHandleMapping requestHandleMapping = new RequestHandleMapping();
                    requestHandleMapping.setController(controllerBean);
                    requestHandleMapping.setMethod(method);
                    requestHandleMapping.setMapping(getMethodMapping(basePath, method));
                    requestHandleMapping.setRequestMethod(method.getDeclaredAnnotation(RequestMapping.class).method());
                    log.info("generate mapping info: [{}]", requestHandleMapping.getMapping());
                    requestHandleMappings.add(requestHandleMapping);
                }
            }
        }
        return requestHandleMappings;
    }

    public static DispatchServlet buildDispatchServlet(Set<RequestHandleMapping> requestHandleMappings) {
        return new DispatchServlet(requestHandleMappings);
    }

    /**
     * 开启Tomcat服务
     */
    public static void startTomcatService(DispatchServlet dispatchServlet) {
        try {
            Tomcat tomcat = new Tomcat();
            tomcat.setBaseDir(BASE_DIR);
            tomcat.setPort(PORT);
            Context context = tomcat.addContext(CONTEXT_PATH, new File(".").getAbsolutePath());
            tomcat.addServlet(CONTEXT_PATH, DISPATCH_SERVLET_NAME, dispatchServlet);
            context.addServletMappingDecoded(URL_PATTERN, DISPATCH_SERVLET_NAME);
            tomcat.start();
            tomcat.getServer().await();
        } catch (LifecycleException e) {
            log.error("tomcat start fail...");
            throw new RuntimeException(e);
        }
    }

    /**
     * 判断指定方法是否需要对其坐映射
     *
     * @param method
     * @return
     */
    public static boolean isHandleMappingMethod(Method method) {
        return method.isAnnotationPresent(RequestMapping.class);
    }

    public static String getMethodMapping(String basePath, Method method) {
        if (method.isAnnotationPresent(RequestMapping.class)) {
            return formatUrlPath(basePath) + formatUrlPath(method.getDeclaredAnnotation(RequestMapping.class).value());
        }
        throw new RuntimeException("unsupported method for generate handelMapping");
    }

    /**
     * 格式化路径 使其最后创建的最终路径为 /xxx/xxx
     * @param path
     * @return
     */
    public static String formatUrlPath(String path) {
        StringBuilder sb = new StringBuilder();
        if (!Strings.isNullOrEmpty(path)) {
            if (!path.startsWith("/")) {
                sb.append("/");
            }
            if (path.endsWith("/")) {
                sb.append(path, 0, path.length() - 1);
            } else {
                sb.append(path);
            }
        }
        return sb.toString();
    }
}
使用举例

这里使用的例子与自研Spring AOP2.0(三)中的相同,这里仅仅修改Controller的映射声明,如下

@Controller("/a")
@Slf4j
public class ExampleController implements IExampleController {

    @Autowired
    private IExampleService service;

    @Autowired
    private IExampleRepository repository;

    @Override
    @RequestMapping(value = "/b", method = RequestMethod.GET)
    public String show(String param) {
        log.info("the receive param is [{}]", param);
        log.info("ExampleController.show()");
        service.show();
        return "controller show method";
    }

    @Override
    @RequestMapping(value = "/c", method = RequestMethod.POST)
    public String show2(String param) {
        log.info("the receive param is [{}]", param);
        log.info("ExampleController.show()");
        service.show();
        return "controller show method";
    }
}

接下来启动NobitaApplication,然后打开浏览器,请求localhost/a/b,首先可以在控制台看到如下结果,表明之前的ioc,aop功能仍然正常,且Controller.show()方法收到了正确的参数。

mvc-1

在返回浏览器,可以看到浏览器也收到了来自于Controller.show()方法的返回结果,如下

mvc-2