手寫SpringMVC框架html
細嗅薔薇 心有猛虎前端
背景:Spring 想必你們都據說過,可能如今更多流行的是Spring Boot 和Spring Cloud 框架;可是SpringMVC 做爲一款實現了MVC 設計模式的web (表現層) 層框架,其高開發效率和高性能也是如今不少公司仍在採用的框架;除此以外,Spring 源碼大師級的代碼規範和設計思想都十分值得學習;退一步說,Spring Boot 框架底層也有不少Spring 的東西,並且面試的時候還會常常被問到SpringMVC 原理,通常人可能也就是隻能把SpringMVC 的運行原理背出來罷了,至於問到有沒有了解其底層實現(代碼層面),那極可能就歇菜了,但您要是能夠手寫SpringMVC 框架就確定能夠令面試官另眼相看,因此手寫SpringMVC 值得一試。java
在設計本身的SpringMVC 框架以前,須要瞭解下其運行流程。web
1、SpringMVC 運行流程面試
圖1. SpringMVC 運行流程spring
一、用戶向服務器發送請求,請求被Spring 前端控制器DispatcherServlet 捕獲;設計模式
二、DispatcherServlet 收到請求後調用HandlerMapping 處理器映射器;tomcat
三、處理器映射器對請求URL 進行解析,獲得請求資源標識符(URI);而後根據該URI,調用HandlerMapping 得到該Handler 配置的全部相關的對象(包括Handler 對象以及Handler 對象對應的攔截器),再以HandlerExecutionChain 對象的形式返回給DispatcherServlet;服務器
四、DispatcherServlet 根據得到的Handler,經過HandlerAdapter 處理器適配器選擇一個合適的HandlerAdapter;(附註:若是成功得到HandlerAdapter 後,此時將開始執行攔截器的preHandler(...)方法);mvc
五、提取Request 中的模型數據,填充Handler 入參,開始執行Handler(即Controller);【在填充Handler的入參過程當中,根據你的配置,Spring 將幫你作一些額外的工做如:HttpMessageConveter:將請求消息(如Json、xml等數據)轉換成一個對象,將對象轉換爲指定的響應信息;數據轉換:對請求消息進行數據轉換,如String轉換成Integer、Double等;數據格式化:對請求消息進行數據格式化,如將字符串轉換成格式化數字或格式化日期等;數據驗證:驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResult或Error中 】
六、Controller 執行完成返回ModelAndView 對象;
七、HandlerAdapter 將controller 執行結果ModelAndView 對象返回給DispatcherServlet;
八、DispatcherServlet 將ModelAndView 對象傳給ViewReslover 視圖解析器;
九、ViewReslover 根據返回的ModelAndView,選擇一個適合的ViewResolver (必須是已經註冊到Spring容器中的ViewResolver)返回給DispatcherServlet;
十、DispatcherServlet 對View 進行渲染視圖(即將模型數據填充至視圖中);
十一、DispatcherServlet 將渲染結果響應用戶(客戶端)。
2、SpringMVC 框架設計思路
一、讀取配置階段
圖2. SpringMVC 繼承關係
第一步就是配置web.xml,加載自定義的DispatcherServlet。而從圖中能夠看出,SpringMVC 本質上是一個Servlet,這個Servlet 繼承自HttpServlet,此外,FrameworkServlet 負責初始SpringMVC的容器,並將Spring 容器設置爲父容器;爲了讀取web.xml 中的配置,須要用到ServletConfig 這個類,它表明當前Servlet 在web.xml 中的配置信息,而後經過web.xml 中加載咱們本身寫的MyDispatcherServlet 和讀取配置文件。
二、初始化階段
初始化階段會在DispatcherServlet 類中,按順序實現下面幾個步驟:
一、加載配置文件;
二、掃描當前項目下的全部文件;
三、拿到掃描到的類,經過反射機制將其實例化,而且放到ioc 容器中(Map的鍵值對 beanName-bean) beanName默認是首字母小寫;
四、初始化path 與方法的映射;
五、獲取請求傳入的參數並處理參數經過初始化好的handlerMapping 中拿出url 對應的方法名,反射調用。
三、運行階段
運行階段,每一次請求將會調用doGet 或doPost 方法,它會根據url 請求去HandlerMapping 中匹配到對應的Method,而後利用反射機制調用Controller 中的url 對應的方法,並獲得結果返回。
3、實現SpringMVC 框架
首先,小老弟SpringMVC 框架只實現本身的@Controller 和@RequestMapping 註解,其它註解功能實現方式相似,實現註解較少因此項目比較簡單,能夠看到以下工程文件及目錄截圖。
圖3. 工程文件及目錄
一、建立Java Web 工程
建立Java Web 工程,勾選JavaEE 下方的Web Application 選項,Next。
圖4. 建立Java Web 工程
二、在工程WEB-INF 下的web.xml 中加入下方配置
1 <?xml version="1.0" encoding="UTF-8"?>
2 <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
5 version="4.0">
6
7 <servlet>
8 <servlet-name>DispatcherServlet</servlet-name>
9 <servlet-class>com.tjt.springmvc.DispatcherServlet</servlet-class>
10 </servlet>
11 <servlet-mapping>
12 <servlet-name>DispatcherServlet</servlet-name>
13 <url-pattern>/</url-pattern>
14 </servlet-mapping>
15
16 </web-app>
三、建立自定義Controller 註解
1 package com.tjt.springmvc; 2
3
4 import java.lang.annotation.*; 5
6
7 /**
8 * @MyController 自定義註解類 9 * 10 * @@Target(ElementType.TYPE) 11 * 表示該註解能夠做用在類上; 12 * 13 * @Retention(RetentionPolicy.RUNTIME) 14 * 表示該註解會在class 字節碼文件中存在,在運行時能夠經過反射獲取到 15 * 16 * @Documented 17 * 標記註解,表示能夠生成文檔 18 */
19 @Target(ElementType.TYPE) 20 @Retention(RetentionPolicy.RUNTIME) 21 @Documented 22 public @interface MyController { 23
24 /**
25 * public class MyController 26 * 把 class 替換成 @interface 該類即成爲註解類 27 */
28
29 /**
30 * 爲Controller 註冊別名 31 * @return
32 */
33 String value() default ""; 34
35 }
四、建立自定義RequestMapping 註解
1 package com.tjt.springmvc; 2
3
4 import java.lang.annotation.*; 5
6
7 /**
8 * @MyRequestMapping 自定義註解類 9 * 10 * @Target({ElementType.METHOD,ElementType.TYPE}) 11 * 表示該註解能夠做用在方法、類上; 12 * 13 * @Retention(RetentionPolicy.RUNTIME) 14 * 表示該註解會在class 字節碼文件中存在,在運行時能夠經過反射獲取到 15 * 16 * @Documented 17 * 標記註解,表示能夠生成文檔 18 */
19 @Target({ElementType.METHOD, ElementType.TYPE}) 20 @Retention(RetentionPolicy.RUNTIME) 21 @Documented 22 public @interface MyRequestMapping { 23
24 /**
25 * public @interface MyRequestMapping 26 * 把 class 替換成 @interface 該類即成爲註解類 27 */
28
29 /**
30 * 表示訪問該方法的url 31 * @return
32 */
33 String value() default ""; 34
35 }
五、設計用於獲取項目工程下全部的class 文件的封裝工具類
1 package com.tjt.springmvc; 2
3
4 import java.io.File; 5 import java.io.FileFilter; 6 import java.net.JarURLConnection; 7 import java.net.URL; 8 import java.net.URLDecoder; 9 import java.util.ArrayList; 10 import java.util.Enumeration; 11 import java.util.List; 12 import java.util.jar.JarEntry; 13 import java.util.jar.JarFile; 14
15 /**
16 * 從項目工程包package 中獲取全部的Class 工具類 17 */
18 public class ClassUtils { 19
20 /**
21 * 靜態常量 22 */
23 private static String FILE_CONSTANT = "file"; 24 private static String UTF8_CONSTANT = "UTF-8"; 25 private static String JAR_CONSTANT = "jar"; 26 private static String POINT_CLASS_CONSTANT = ".class"; 27 private static char POINT_CONSTANT = '.'; 28 private static char LEFT_LINE_CONSTANT = '/'; 29
30
31 /**
32 * 定義私有構造函數來屏蔽隱式公有構造函數 33 */
34 private ClassUtils() { 35 } 36
37
38 /**
39 * 從項目工程包package 中獲取全部的Class 40 * getClasses 41 * 42 * @param packageName 43 * @return
44 */
45 public static List<Class<?>> getClasses(String packageName) throws Exception { 46
47
48 List<Class<?>> classes = new ArrayList<Class<?>>(); // 定義一個class 類的泛型集合
49 boolean recursive = true; // recursive 是否循環迭代
50 String packageDirName = packageName.replace(POINT_CONSTANT, LEFT_LINE_CONSTANT); // 獲取包的名字 並進行替換
51 Enumeration<URL> dirs; // 定義一個枚舉的集合 分別保存該目錄下的全部java 類文件及Jar 包等內容
52 dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); 53 /**
54 * 循環迭代 處理這個目錄下的things 55 */
56 while (dirs.hasMoreElements()) { 57 URL url = dirs.nextElement(); // 獲取下一個元素
58 String protocol = url.getProtocol(); // 獲得協議的名稱 protocol 59 // 若是是
60 /**
61 * 若protocol 是文件形式 62 */
63 if (FILE_CONSTANT.equals(protocol)) { 64 String filePath = URLDecoder.decode(url.getFile(), UTF8_CONSTANT); // 獲取包的物理路徑
65 findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); // 以文件的方式掃描整個包下的文件 並添加到集合中
66 /**
67 * 若protocol 是jar 包文件 68 */
69 } else if (JAR_CONSTANT.equals(protocol)) { 70 JarFile jar; // 定義一個JarFile
71 jar = ((JarURLConnection) url.openConnection()).getJarFile(); // 獲取jar
72 Enumeration<JarEntry> entries = jar.entries(); // 從jar 包中獲取枚舉類
73 /**
74 * 循環迭代從Jar 包中得到的枚舉類 75 */
76 while (entries.hasMoreElements()) { 77 JarEntry entry = entries.nextElement(); // 獲取jar裏的一個實體,如目錄、META-INF等文件
78 String name = entry.getName(); 79 /**
80 * 若實體名是以 / 開頭 81 */
82 if (name.charAt(0) == LEFT_LINE_CONSTANT) { 83 name = name.substring(1); // 獲取後面的字符串
84 } 85 // 若是
86 /**
87 * 若實體名前半部分和定義的包名相同 88 */
89 if (name.startsWith(packageDirName)) { 90 int idx = name.lastIndexOf(LEFT_LINE_CONSTANT); 91 /**
92 * 而且實體名覺得'/' 結尾 93 * 若其以'/' 結尾則是一個包 94 */
95 if (idx != -1) { 96 packageName = name.substring(0, idx).replace(LEFT_LINE_CONSTANT, POINT_CONSTANT); // 獲取包名 並把'/' 替換成'.'
97 } 98 /**
99 * 若實體是一個包 且能夠繼續迭代 100 */
101 if ((idx != -1) || recursive) { 102 if (name.endsWith(POINT_CLASS_CONSTANT) && !entry.isDirectory()) { // 若爲.class 文件 且不是目錄
103 String className = name.substring(packageName.length() + 1, name.length() - 6); // 則去掉.class 後綴並獲取真正的類名
104 classes.add(Class.forName(packageName + '.' + className)); // 把得到到的類名添加到classes
105 } 106 } 107 } 108 } 109 } 110 } 111
112 return classes; 113 } 114
115
116 /**
117 * 以文件的形式來獲取包下的全部Class 118 * findAndAddClassesInPackageByFile 119 * 120 * @param packageName 121 * @param packagePath 122 * @param recursive 123 * @param classes 124 */
125 public static void findAndAddClassesInPackageByFile( 126 String packageName, String packagePath, 127 final boolean recursive, 128 List<Class<?>> classes) throws Exception { 129
130
131 File dir = new File(packagePath); // 獲取此包的目錄並創建一個File
132
133 if (!dir.exists() || !dir.isDirectory()) { // 若dir 不存在或者 也不是目錄就直接返回
134 return; 135 } 136
137 File[] dirfiles = dir.listFiles(new FileFilter() { // 若dir 存在 則獲取包下的全部文件、目錄
138
139 /**
140 * 自定義過濾規則 若是能夠循環(包含子目錄) 或則是以.class 結尾的文件(編譯好的java 字節碼文件) 141 * @param file 142 * @return
143 */
144 @Override 145 public boolean accept(File file) { 146 return (recursive && file.isDirectory()) || (file.getName().endsWith(POINT_CLASS_CONSTANT)); 147 } 148 }); 149
150 /**
151 * 循環全部文件獲取java 類文件並添加到集合中 152 */
153 for (File file : dirfiles) { 154 if (file.isDirectory()) { // 若file 爲目錄 則繼續掃描
155 findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, 156 classes); 157 } else { // 若file 爲java 類文件 則去掉後面的.class 只留下類名
158 String className = file.getName().substring(0, file.getName().length() - 6); 159 classes.add(Class.forName(packageName + '.' + className)); // 把className 添加到集合中去
160
161 } 162 } 163 } 164 }
六、訪問跳轉頁面index.jsp
1 <%--
2 Created by IntelliJ IDEA. 3 User: apple 4 Date: 2019-11-07
5 Time: 13:28
6 To change this template use File | Settings | File Templates. 7 --%>
8 <%--
9 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
10 --%> 11 <html>
12 <head>
13 <title>My Fucking SpringMVC</title>
14 </head>
15 <body>
16 <h2>The Lie We Live!</h2>
17 <H2>My Fucking SpringMVC</H2>
18 </body>
19 </html>
七、自定義DispatcherServlet 設計,繼承HttpServlet,重寫init 方法、doGet、doPost 等方法,以及自定義註解要實現的功能。
1 package com.tjt.springmvc; 2
3
4 import javax.servlet.ServletConfig; 5 import javax.servlet.ServletException; 6 import javax.servlet.http.HttpServlet; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException; 10 import java.lang.reflect.InvocationTargetException; 11 import java.lang.reflect.Method; 12 import java.util.List; 13 import java.util.Map; 14 import java.util.Objects; 15 import java.util.concurrent.ConcurrentHashMap; 16
17
18
19 /**
20 * DispatcherServlet 處理SpringMVC 框架流程 21 * 主要流程: 22 * 一、包掃描獲取包下面全部的類 23 * 二、初始化包下面全部的類 24 * 三、初始化HandlerMapping 方法,將url 和方法對應上 25 * 四、實現HttpServlet 重寫doPost 方法 26 * 27 */
28 public class DispatcherServlet extends HttpServlet { 29
30 /**
31 * 部分靜態常量 32 */
33 private static String PACKAGE_CLASS_NULL_EX = "包掃描後的classes爲null"; 34 private static String HTTP_NOT_EXIST = "sorry http is not exit 404"; 35 private static String METHOD_NOT_EXIST = "sorry method is not exit 404"; 36 private static String POINT_JSP = ".jsp"; 37 private static String LEFT_LINE = "/"; 38
39 /**
40 * 用於存放SpringMVC bean 的容器 41 */
42 private ConcurrentHashMap<String, Object> mvcBeans = new ConcurrentHashMap<>(); 43 private ConcurrentHashMap<String, Object> mvcBeanUrl = new ConcurrentHashMap<>(); 44 private ConcurrentHashMap<String, String> mvcMethodUrl = new ConcurrentHashMap<>(); 45 private static String PROJECT_PACKAGE_PATH = "com.tjt.springmvc"; 46
47
48 /**
49 * 按順序初始化組件 50 * @param config 51 */
52 @Override 53 public void init(ServletConfig config) { 54 String packagePath = PROJECT_PACKAGE_PATH; 55 try { 56 //1.進行報掃描獲取當前包下面全部的類
57 List<Class<?>> classes = comscanPackage(packagePath); 58 //2.初始化springmvcbean
59 initSpringMvcBean(classes); 60 } catch (Exception e) { 61 e.printStackTrace(); 62 } 63 //3.將請求地址和方法進行映射
64 initHandMapping(mvcBeans); 65 } 66
67
68 /**
69 * 調用ClassUtils 工具類獲取工程中全部的class 70 * @param packagePath 71 * @return
72 * @throws Exception 73 */
74 public List<Class<?>> comscanPackage(String packagePath) throws Exception { 75 List<Class<?>> classes = ClassUtils.getClasses(packagePath); 76 return classes; 77 } 78
79 /**
80 * 初始化SpringMVC bean 81 * 82 * @param classes 83 * @throws Exception 84 */
85 public void initSpringMvcBean(List<Class<?>> classes) throws Exception { 86 /**
87 * 若包掃描出的classes 爲空則直接拋異常 88 */
89 if (classes.isEmpty()) { 90 throw new Exception(PACKAGE_CLASS_NULL_EX); 91 } 92
93 /**
94 * 遍歷全部classes 獲取@MyController 註解 95 */
96 for (Class<?> aClass : classes) { 97 //獲取被自定義註解的controller 將其初始化到自定義springmvc 容器中
98 MyController declaredAnnotation = aClass.getDeclaredAnnotation(MyController.class); 99 if (declaredAnnotation != null) { 100 //獲取類的名字
101 String beanid = lowerFirstCapse(aClass.getSimpleName()); 102 //獲取對象
103 Object beanObj = aClass.newInstance(); 104 //放入spring 容器
105 mvcBeans.put(beanid, beanObj); 106 } 107 } 108
109 } 110
111 /**
112 * 初始化HandlerMapping 方法 113 * 114 * @param mvcBeans 115 */
116 public void initHandMapping(ConcurrentHashMap<String, Object> mvcBeans) { 117 /**
118 * 遍歷springmvc 獲取注入的對象值 119 */
120 for (Map.Entry<String, Object> entry : mvcBeans.entrySet()) { 121 Object objValue = entry.getValue(); 122 Class<?> aClass = objValue.getClass(); 123 //獲取當前類 判斷其是否有自定義的requestMapping 註解
124 String mappingUrl = null; 125 MyRequestMapping anRequestMapping = aClass.getDeclaredAnnotation(MyRequestMapping.class); 126 if (anRequestMapping != null) { 127 mappingUrl = anRequestMapping.value(); 128 } 129 //獲取當前類全部方法,判斷方法上是否有註解
130 Method[] declaredMethods = aClass.getDeclaredMethods(); 131 /**
132 * 遍歷註解 133 */
134 for (Method method : declaredMethods) { 135 MyRequestMapping methodDeclaredAnnotation = method.getDeclaredAnnotation(MyRequestMapping.class); 136 if (methodDeclaredAnnotation != null) { 137 String methodUrl = methodDeclaredAnnotation.value(); 138 mvcBeanUrl.put(mappingUrl + methodUrl, objValue); 139 mvcMethodUrl.put(mappingUrl + methodUrl, method.getName()); 140 } 141 } 142
143 } 144
145 } 146
147 /**
148 * @param str 149 * @return 類名首字母小寫 150 */
151 public static String lowerFirstCapse(String str) { 152 char[] chars = str.toCharArray(); 153 chars[0] += 32; 154 return String.valueOf(chars); 155
156 } 157
158 /**
159 * doPost 請求 160 * @param req 161 * @param resp 162 * @throws ServletException 163 * @throws IOException 164 */
165 @Override 166 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 167 try { 168 /**
169 * 處理請求 170 */
171 doServelt(req, resp); 172 } catch (NoSuchMethodException e) { 173 e.printStackTrace(); 174 } catch (InvocationTargetException e) { 175 e.printStackTrace(); 176 } catch (IllegalAccessException e) { 177 e.printStackTrace(); 178 } 179 } 180
181 /**
182 * doServelt 處理請求 183 * @param req 184 * @param resp 185 * @throws IOException 186 * @throws NoSuchMethodException 187 * @throws InvocationTargetException 188 * @throws IllegalAccessException 189 * @throws ServletException 190 */
191 private void doServelt(HttpServletRequest req, HttpServletResponse resp) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ServletException { 192 //獲取請求地址
193 String requestUrl = req.getRequestURI(); 194 //查找地址所對應bean
195 Object object = mvcBeanUrl.get(requestUrl); 196 if (Objects.isNull(object)) { 197 resp.getWriter().println(HTTP_NOT_EXIST); 198 return; 199 } 200 //獲取請求的方法
201 String methodName = mvcMethodUrl.get(requestUrl); 202 if (methodName == null) { 203 resp.getWriter().println(METHOD_NOT_EXIST); 204 return; 205 } 206
207
208 //經過構反射執行方法
209 Class<?> aClass = object.getClass(); 210 Method method = aClass.getMethod(methodName); 211
212 String invoke = (String) method.invoke(object); 213 // 獲取後綴信息
214 String suffix = POINT_JSP; 215 // 頁面目錄地址
216 String prefix = LEFT_LINE; 217 req.getRequestDispatcher(prefix + invoke + suffix).forward(req, resp); 218
219
220
221
222 } 223
224 /**
225 * doGet 請求 226 * @param req 227 * @param resp 228 * @throws ServletException 229 * @throws IOException 230 */
231 @Override 232 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 233 this.doPost(req, resp); 234 } 235
236
237 }
八、測試手寫SpringMVC 框架效果類TestMySpringMVC 。
1 package com.tjt.springmvc; 2
3
4 /**
5 * 手寫SpringMVC 測試類 6 * TestMySpringMVC 7 */
8 @MyController 9 @MyRequestMapping(value = "/tjt") 10 public class TestMySpringMVC { 11
12
13 /**
14 * 測試手寫SpringMVC 框架效果 testMyMVC1 15 * @return
16 */
17 @MyRequestMapping("/mvc") 18 public String testMyMVC1() { 19 System.out.println("he Lie We Live!"); 20 return "index"; 21 } 22
23
24 }
九、配置Tomcat 用於運行Web 項目。
圖5. 配置tomcat
十、運行項目,訪問測試。
一、輸入正常路徑 http://localhost:8080/tjt/mvc 訪問測試效果以下:
圖6. 正常路徑測試效果
二、輸入非法(不存在)路徑 http://localhost:8080/tjt/mvc8 訪問測試效果以下:
圖7. 非法路徑測試效果
三、控制檯打印「The Lie We Live」以下:
圖8. 控制檯打印
測試效果如上則證實成功手寫SpringMVC 框架,恭喜。
細嗅薔薇 心有猛虎