标题是‘从零开始实现一个简易的Java MVC框架’,结果写了这么多才到实现MVC的时候...只能说前戏确实有点多了。不过这些前戏都是必须的,若是只是简简单单实现一个MVC的功能那就没有意思了,要有Bean容器、IOC、AOP和MVC才像是一个'框架'嘛。html
为了实现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
语句就须要这个包。java
<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
来实现。git
每个请求都会对应一个Servlet
,而后还要在web.xml中配置这个Servlet
,而后对请求的接收和处理啥的都分布在一大堆的Servlet
中,代码十分混杂。github
为了让人们编写的时候更专一于业务代码而减小对请求的处理,springmvc就经过一个中央的Servlet
,处理这些请求,而后再转发到对应的Controller中,这样就只有一个Servlet
统一处理请求了。下面的一段话来自spring的官方文档https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-servletweb
Spring MVC, like many other web frameworks, is designed around the front controller pattern where a centralServlet
, theDispatcherServlet
, 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 anyServlet
, needs to be declared and mapped according to the Servlet specification using Java configuration or inweb.xml
. In turn theDispatcherServlet
uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.spring
这段大体意思就是:springmvc经过中心Servlet(DispatcherServlet)来实现对控制controller的操做。这个Servlet
要经过java配置或者配置在web.xml中,它用于寻找请求的映射(即找到对应的controller),视图解析(即执行controller的结果),异常处理(即对执行过程的异常统一处理)等等apache
因此实现MVC的效果就是如下几点:json
DispatcherServlet
来接收全部请求根据上面的步骤,咱们先从步骤二、三、四、5开始,最后再实现1完成mvc。api
为了方便实现,先在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流程中的步骤三、四、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()
获取HttpServletRequest中参数 instantiateMethodArgs()
实例化调用方法要传入的参数值resultResolver()
解析方法的返回值,选择返回页面或者json经过这几个步骤算是凝聚了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的朋友可能不大清楚了,在之前使用springmvc+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步骤的三、四、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