标题是‘从零开始实现一个简易的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的实现原理,在使用 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 最常见的注解。
为了能够方便的传递参数到前端,创建一个工具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分发器类似于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
变量中的功能。大概讲解一些这个类的功能流程:
BeanContainer
的单例实例 BeanContainer
中存放的被 RequestMapping
注解标记的类 RequestMapping
注解标记的方法 ControllerInfo
RequestMapping
里的 value()
和 method()
生成 PathInfo
PathInfo
和 ControllerInfo
存到变量 pathControllerMap
中 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核心步骤了,不过由于篇幅问题,几乎每一步骤得功能都有所精简,如
虽然有缺陷,但是一个MVC流程是完成了。接下来就要把这些功能组装一下了。
终于到实现开头说的 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的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。
还有目前也没实现异常的处理,不能返回异常页面给用户。
这些优化工作会在后期的章节完成的。
源码地址: doodle
原文地址: 从零开始实现一个简易的Java MVC框架(七)--实现MVC