手寫SpringMVC 框架

手寫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:將請求消息(如Jsonxml等數據)轉換成一個對象,將對象轉換爲指定的響應信息數據轉換:對請求消息進行數據轉換,如String轉換成IntegerDouble等;數據格式化:對請求消息進行數據格式化,如將字符串轉換成格式化數字或格式化日期等;數據驗證:驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResultError

六、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 框架,恭喜。

 

 

細嗅薔薇 心有猛虎

相關文章
相關標籤/搜索