標題是‘從零開始實現一個簡易的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