手寫SpringMVC框架 細嗅薔薇 心有猛虎 背景:Spring 想必大家都聽說過,可能現在更多流行的是Spring Boot 和Spring Cloud 框架;但是SpringMVC 作為一款實現了MVC 設計模式的web (表現層) 層框架,其高開發效率和高性能也是現在很多公司仍在採用的框架; ...
手寫SpringMVC框架
細嗅薔薇 心有猛虎
背景:Spring 想必大家都聽說過,可能現在更多流行的是Spring Boot 和Spring Cloud 框架;但是SpringMVC 作為一款實現了MVC 設計模式的web (表現層) 層框架,其高開發效率和高性能也是現在很多公司仍在採用的框架;除此之外,Spring 源碼大師級的代碼規範和設計思想都十分值得學習;退一步說,Spring Boot 框架底層也有很多Spring 的東西,而且面試的時候還會經常被問到SpringMVC 原理,一般人可能也就是只能把SpringMVC 的運行原理背出來罷了,至於問到有沒有瞭解其底層實現(代碼層面),那很可能就歇菜了,但您要是可以手寫SpringMVC 框架就肯定可以令面試官刮目相看,所以手寫SpringMVC 值得一試。
在設計自己的SpringMVC 框架之前,需要瞭解下其運行流程。
一、SpringMVC 運行流程
圖1. SpringMVC 運行流程
1、用戶向伺服器發送請求,請求被Spring 前端控制器DispatcherServlet 捕獲;
2、DispatcherServlet 收到請求後調用HandlerMapping 處理器映射器;
3、處理器映射器對請求URL 進行解析,得到請求資源標識符(URI);然後根據該URI,調用HandlerMapping 獲得該Handler 配置的所有相關的對象(包括Handler 對象以及Handler 對象對應的攔截器),再以HandlerExecutionChain 對象的形式返回給DispatcherServlet;
4、DispatcherServlet 根據獲得的Handler,通過HandlerAdapter 處理器適配器選擇一個合適的HandlerAdapter;(附註:如果成功獲得HandlerAdapter 後,此時將開始執行攔截器的preHandler(...)方法);
5、提取Request 中的模型數據,填充Handler 入參,開始執行Handler(即Controller);【在填充Handler的入參過程中,根據你的配置,Spring 將幫你做一些額外的工作如:HttpMessageConveter:將請求消息(如Json、xml等數據)轉換成一個對象,將對象轉換為指定的響應信息;數據轉換:對請求消息進行數據轉換,如String轉換成Integer、Double等;數據格式化:對請求消息進行數據格式化,如將字元串轉換成格式化數字或格式化日期等;數據驗證:驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResult或Error中 】
6、Controller 執行完成返回ModelAndView 對象;
7、HandlerAdapter 將controller 執行結果ModelAndView 對象返回給DispatcherServlet;
8、DispatcherServlet 將ModelAndView 對象傳給ViewReslover 視圖解析器;
9、ViewReslover 根據返回的ModelAndView,選擇一個適合的ViewResolver (必須是已經註冊到Spring容器中的ViewResolver)返回給DispatcherServlet;
10、DispatcherServlet 對View 進行渲染視圖(即將模型數據填充至視圖中);
11、DispatcherServlet 將渲染結果響應用戶(客戶端)。
二、SpringMVC 框架設計思路
1、讀取配置階段
圖2. SpringMVC 繼承關係
第一步就是配置web.xml,載入自定義的DispatcherServlet。而從圖中可以看出,SpringMVC 本質上是一個Servlet,這個Servlet 繼承自HttpServlet,此外,FrameworkServlet 負責初始SpringMVC的容器,並將Spring 容器設置為父容器;為了讀取web.xml 中的配置,需要用到ServletConfig 這個類,它代表當前Servlet 在web.xml 中的配置信息,然後通過web.xml 中載入我們自己寫的MyDispatcherServlet 和讀取配置文件。
2、初始化階段
初始化階段會在DispatcherServlet 類中,按順序實現下麵幾個步驟:
1、載入配置文件;
2、掃描當前項目下的所有文件;
3、拿到掃描到的類,通過反射機制將其實例化,並且放到ioc 容器中(Map的鍵值對 beanName-bean) beanName預設是首字母小寫;
4、初始化path 與方法的映射;
5、獲取請求傳入的參數並處理參數通過初始化好的handlerMapping 中拿出url 對應的方法名,反射調用。
3、運行階段
運行階段,每一次請求將會調用doGet 或doPost 方法,它會根據url 請求去HandlerMapping 中匹配到對應的Method,然後利用反射機制調用Controller 中的url 對應的方法,並得到結果返回。
三、實現SpringMVC 框架
首先,小老弟SpringMVC 框架只實現自己的@Controller 和@RequestMapping 註解,其它註解功能實現方式類似,實現註解較少所以項目比較簡單,可以看到如下工程文件及目錄截圖。
圖3. 工程文件及目錄
1、創建Java Web 工程
創建Java Web 工程,勾選JavaEE 下方的Web Application 選項,Next。
圖4. 創建Java Web 工程
2、在工程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>
3、創建自定義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 }
4、創建自定義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 }
5、設計用於獲取項目工程下所有的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 }
6、訪問跳轉頁面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!</