手寫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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...