手寫SpringMVC 框架

来源:https://www.cnblogs.com/taojietaoge/archive/2019/11/10/11817163.html
-Advertisement-
Play Games

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

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!</	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 攔截器 在開始創建攔截器之前,一定要瞭解 $q和延期承諾api 出於全局錯誤處理,身份驗證或請求的任何同步或非同步預處理或響應的後處理目的,希望能夠在將請求移交給伺服器之前攔截請求,併在將請求移交給伺服器之前將響應攔截發起這些請求的應用程式代碼-攔截器利用promise api滿足同步和非同步預處理的需 ...
  • 本來用的是網易雲的外鏈,後來發現APlayer就換成這個播放器組件了 在 頁腳 HTML 代碼 中插入以下代碼就行了 1 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/APlayer.min ...
  • css,對包含有子元素的元素進行flex後,會影響原有的佈局。如何後續處理 ...
  • 今天再寫項目的時候, 有一個手動添加行的功能,使用的是jqgrid的addRowData方法添加數據。但是在我們切換標簽頁的時候,再次添加行,調用這個方法的時候,報錯了。錯誤信息如下 然後經過自己的反覆測試發現是這樣的,當我們切換到第二個標簽頁的時候,頁面上是沒有數據的,調用addRowData的方 ...
  • setInterval定時器解決request非同步: view溢出橫向滑動顯示: 跳轉傳參+返回傳參: ...
  • 由 楊柳依 創建於2019年11月3日,最近更新於2019年11月8日 參考資料: "大話設計模式" | "圖解設計模式" | "菜鳥教程—設計模式" UML類圖 【矩形框】代表一個類(Class)。類圖分三層: 第一層顯示類的名稱,如果是抽象類,則就用斜體顯示; 第二層是類的特性,通常就是欄位和屬 ...
  • 雖然之前已經學了2個月python,但仍然感覺學的很亂,沒有系統性;或者說自學的沒有條例,只是追求進度,沒有保證知識點的全面與準確。 從今天開始,從python的基礎變數開始重新整理知識點,梳理忽略的內容。願所學即所會,所會即能用。 1、變數名遵循的規則 只能包含字母、數字和下劃線。需要以字母或下劃 ...
  • 一、概述二、Struts2 快速入門程式2.1 開發流程比較2.2 引入依賴2.2 創建jsp頁面2.3 在web.xml中配置前端控制器2.4 創建struts.xml配置文件2.4 創建一個HelloAction類2.5 在struts.xml文件中配置HelloAction2.6 在index... ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...