自己手寫一個SpringMVC 框架

来源:https://www.cnblogs.com/tk55/archive/2018/03/21/8618633.html
-Advertisement-
Play Games

一、瞭解SpringMVC運行流程及九大組件 1、SpringMVC 的運行流程 · 用戶發送請求至前端控制器DispatcherServlet · DispatcherServlet收到請求調用HandlerMapping處理器映射器。 · 處理器映射器根據請求url找到具體的處理器,生成處理器對 ...


 

 

一、瞭解SpringMVC運行流程及九大組件

 

  1、SpringMVC 的運行流程

 

 

  1. · 用戶發送請求至前端控制器DispatcherServlet

  2. · DispatcherServlet收到請求調用HandlerMapping處理器映射器。

  3. · 處理器映射器根據請求url找到具體的處理器,生成處理器對象及處理器攔截器(如果有則生成)一併返回給DispatcherServlet。

  4. · DispatcherServlet通過HandlerAdapter處理器適配器調用處理器

  5. · 執行處理器(Controller,也叫後端控制器)。

  6. · Controller執行完成返回ModelAndView

  7. · HandlerAdapter將controller執行結果● ModelAndView返回給DispatcherServlet

  8. · DispatcherServlet將ModelAndView傳給● ViewReslover視圖解析器

  9. · ViewReslover解析後返回具體View

  10. · DispatcherServlet對View進行渲染視圖(即將模型數據填充至視圖中)。

  11. · DispatcherServlet響應用戶。

從上面可以看出,DispatcherServlet有接受請求,響應結果,轉發等作用。有了DispatcherServlet之後,可以減少組件之間的耦合度。

 

2、SpringMVC 的九大組件

protected void initStrategies(ApplicationContext context) {
//用於處理上傳請求。處理方法是將普通的request包裝成MultipartHttpServletRequest,後者可以直接調用getFile方法獲取File.
initMultipartResolver(context);

//SpringMVC主要有兩個地方用到了Locale:一是ViewResolver視圖解析的時候;二是用到國際化資源或者主題的時候。
initLocaleResolver(context); 

//用於解析主題。SpringMVC中一個主題對應一個properties文件,裡面存放著跟當前主題相關的所有資源、
//如圖片、css樣式等。SpringMVC的主題也支持國際化, 
initThemeResolver(context);

//用來查找Handler的。
initHandlerMappings(context);

//從名字上看,它就是一個適配器。Servlet需要的處理方法的結構卻是固定的,都是以request和response為參數的方法。
//如何讓固定的Servlet處理方法調用靈活的Handler來進行處理呢?這就是HandlerAdapter要做的事情
initHandlerAdapters(context);

//其它組件都是用來幹活的。在幹活的過程中難免會出現問題,出問題後怎麼辦呢?
//這就需要有一個專門的角色對異常情況進行處理,在SpringMVC中就是HandlerExceptionResolver。
initHandlerExceptionResolvers(context);

//有的Handler處理完後並沒有設置View也沒有設置ViewName,這時就需要從request獲取ViewName了,
//如何從request中獲取ViewName就是RequestToViewNameTranslator要做的事情了。
initRequestToViewNameTranslator(context);

//ViewResolver用來將String類型的視圖名和Locale解析為View類型的視圖。
//View是用來渲染頁面的,也就是將程式返回的參數填入模板里,生成html(也可能是其它類型)文件。
initViewResolvers(context);

//用來管理FlashMap的,FlashMap主要用在redirect重定向中傳遞參數。
initFlashMapManager(context); 
}
View Code

 

 

 

二、自己實現 SpringMVC 功能分析

 本篇文章只實現 SpringMVC 的配置載入、實例化掃描的包、HandlerMapping 的 url 映射到對應的Controller 的 method 上、異常的攔截和動態調用後返回結果輸出給瀏覽器的功能。其餘 SpringMVC 功能讀者可以嘗試自己實現。

 

 

1、讀取配置

 

從圖中可以看出,SpringMVC本質上是一個Servlet,這個 Servlet 繼承自 HttpServlet。

 

FrameworkServlet負責初始化SpringMVC的容器,並將Spring容器設置為父容器。因為本文只是實現SpringMVC,對於Spring容器不做過多講解(有興趣同學可以看看博主另一篇文章:向spring大佬低頭--大量源碼流出解析)。

 

為了讀取web.xml中的配置,我們用到ServletConfig這個類,它代表當前Servlet在web.xml中的配置信息。通過web.xml中載入我們自己寫的MyDispatcherServlet和讀取配置文件。

 

2、初始化階段

在上文中,我們知道了DispatcherServlet的initStrategies方法會初始化9大組件,但是本文將實現一些SpringMVC的最基本的組件而不是全部,按順序包括:

 

· 載入配置文件

· 掃描用戶配置包下麵所有的類

· 拿到掃描到的類,通過反射機制,實例化。並且放到ioc容器中(Map的鍵值對  beanName-bean) beanName預設是首字母小寫

· 初始化HandlerMapping,這裡其實就是把url和method對應起來放在一個k-v的Map中,在運行階段取出

 

3、運行階段

每一次請求將會調用doGet或doPost方法,所以統一運行階段都放在doDispatch方法里處理,它會根據url請求去HandlerMapping中匹配到對應的Method,然後利用反射機制調用Controller中的url對應的方法,並得到結果返回。按順序包括以下功能:

· 異常的攔截

· 獲取請求傳入的參數並處理參數

· 通過初始化好的handlerMapping中拿出url對應的方法名,反射調用

 

 三、手寫 SpringMVC 框架

 

工程文件及目錄:

 

首先,新建一個maven項目,在pom.xml中導入以下依賴。為了方便,博主直接導入了Springboot的web包,裡面有我們需要的所有web開發的東西:

 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.liugh</groupId>
  <artifactId>liughMVC</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  
 <dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>1.4.3.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencies>
</dependencyManagement>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <java.version>1.8</java.version>
</properties>
<dependencies>
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>
View Code

接著,我們在WEB-INF下創建一個web.xml,如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
    <servlet-name>MySpringMVC</servlet-name>
    <servlet-class>com.liugh.servlet.MyDispatcherServlet</servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>application.properties</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>MySpringMVC</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>
View Code

application.properties文件中只是配置要掃描的包到SpringMVC容器中。

scanPackage=com.liugh.core

創建自己的Controller註解,它只能標註在類上面:

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
/**
     * 表示給controller註冊別名
     * @return
     */
    String value() default "";

}
View Code

RequestMapping註解,可以在類和方法上:

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
/**
     * 表示訪問該方法的url
     * @return
     */
    String value() default "";

}
View Code

RequestParam註解,只能註解在參數上

package com.liugh.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestParam {
/**
     * 表示參數的別名,必填
     * @return
     */
    String value();

}
View Code

然後創建MyDispatcherServlet這個類,去繼承HttpServlet,重寫init方法、doGet、doPost方法,以及加上我們第二步分析時要實現的功能:

package com.liugh.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.liugh.annotation.MyController;
import com.liugh.annotation.MyRequestMapping;

public class MyDispatcherServlet extends HttpServlet{
private Properties properties = new Properties();
private List<String> classNames = new ArrayList<>();
private Map<String, Object> ioc = new HashMap<>();
private Map<String, Method> handlerMapping = new  HashMap<>();
private Map<String, Object> controllerMap  =new HashMap<>();

@Override
public void init(ServletConfig config) throws ServletException {
    //1.載入配置文件
    doLoadConfig(config.getInitParameter("contextConfigLocation"));
    //2.初始化所有相關聯的類,掃描用戶設定的包下麵所有的類
    doScanner(properties.getProperty("scanPackage"));
    //3.拿到掃描到的類,通過反射機制,實例化,並且放到ioc容器中(k-v  beanName-bean) beanName預設是首字母小寫
    doInstance();
    //4.初始化HandlerMapping(將url和method對應上)
    initHandlerMapping();
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req,resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    try {
        //處理請求
        doDispatch(req,resp);
    } catch (Exception e) {
        resp.getWriter().write("500!! Server Exception");
}

}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    if(handlerMapping.isEmpty()){
        return;
    }
    String url =req.getRequestURI();
    String contextPath = req.getContextPath();
    url=url.replace(contextPath, "").replaceAll("/+", "/");
    if(!this.handlerMapping.containsKey(url)){
    resp.getWriter().write("404 NOT FOUND!");
    return;
}
Method method =this.handlerMapping.get(url);
//獲取方法的參數列表
Class<?>[] parameterTypes = method.getParameterTypes();
//獲取請求的參數
Map<String, String[]> parameterMap = req.getParameterMap();
//保存參數值
Object [] paramValues= new Object[parameterTypes.length];
//方法的參數列表
        for (int i = 0; i<parameterTypes.length; i++){  
            //根據參數名稱,做某些處理  
            String requestParam = parameterTypes[i].getSimpleName();  
            
            
            if (requestParam.equals("HttpServletRequest")){  
                //參數類型已明確,這邊強轉類型  
            paramValues[i]=req;
                continue;  
            }  
            if (requestParam.equals("HttpServletResponse")){  
            paramValues[i]=resp;
                continue;  
            }
            if(requestParam.equals("String")){
            for (Entry<String, String[]> param : parameterMap.entrySet()) {
         String value =Arrays.toString(param.getValue()).replaceAll("\[|\]", "").replaceAll(",\s", ",");
         paramValues[i]=value;
         }
            }
        }  
//利用反射機制來調用
    try {
       method.invoke(this.controllerMap.get(url), paramValues);//obj是method所對應的實例 在ioc容器中
    } catch (Exception e) {
        e.printStackTrace();
    }
}



private void  doLoadConfig(String location){
//把web.xml中的contextConfigLocation對應value值的文件載入到留裡面
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
try {
//用Properties文件載入文件里的內容
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}finally {
//關流
if(null!=resourceAsStream){
try {
resourceAsStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void doScanner(String packageName) {
//把所有的.替換成/
URL url  =this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\.", "/"));
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
if(file.isDirectory()){
//遞歸讀取包
doScanner(packageName+"."+file.getName());
}else{
String className =packageName +"." +file.getName().replace(".class", "");
classNames.add(className);
}
}
}
private void doInstance() {
if (classNames.isEmpty()) {
return;
}
for (String className : classNames) {
try {
//把類搞出來,反射來實例化(只有加@MyController需要實例化)
Class<?> clazz =Class.forName(className);
   if(clazz.isAnnotationPresent(MyController.class)){
ioc.put(toLowerFirstWord(clazz.getSimpleName()),clazz.newInstance());
}else{
continue;
}
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}


private void initHandlerMapping(){
if(ioc.isEmpty()){
return;
}
try {
for (Entry<String, Object> entry: ioc.entrySet()) {
Class<? extends Object> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(MyController.class)){
continue;
}
//拼url時,是controller頭的url拼上方法上的url
String baseUrl ="";
if(clazz.isAnnotationPresent(MyRequestMapping.class)){
MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
baseUrl=annotation.value();
}
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if(!method.isAnnotationPresent(MyRequestMapping.class)){
continue;
}
MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);
String url = annotation.value();
url =(baseUrl+"/"+url).replaceAll("/+", "/");
handlerMapping.put(url,method);
controllerMap.put(url,clazz.newInstance());
System.out.println(url+","+method);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}


/**
* 把字元串的首字母小寫
* @param name
* @return
*/
private String toLowerFirstWord(String name){
char[] charArray = name.toCharArray();
charArray[0] += 32;
return String.valueOf(charArray);
}
}
View Code

這裡我們就開發完了自己的SpringMVC,現在我們測試一下:

package com.liugh.core.controller;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.liugh.annotation.MyController;
import com.liugh.annotation.MyRequestMapping;
import com.liugh.annotation.MyRequestParam;

@MyController
@MyRequestMapping("/test")
public class TestController {

@MyRequestMapping("/doTest")
    public void test1(HttpServletRequest request, HttpServletResponse response,
    @MyRequestParam("param") String param){
 System.out.println(param);
    try {
            response.getWriter().write( "doTest method success! param:"+param);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
 
@MyRequestMapping("/doTest2")
    public void test2(HttpServletRequest request, HttpServletResponse response){
        try {
            response.getWriter().println("doTest2 method success!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
View Code

訪問http://localhost:8080/liughMVC/test/doTest?param=liugh如下:

 

訪問一個不存在的試試:

 

 

 ----【轉】

 


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

-Advertisement-
Play Games
更多相關文章
  • ~~媽媽,我終於會寫LCT了!~~ LCT太神奇了,它和Splay能完美結合。在$O(nlogn)$的時間內解決動態樹問題。 BZOJ2049 [Sdoi2008]Cave 洞穴勘測 丟板子,懶得解釋。用維護一下聯通塊就行了,因為數據水,並查集也可以水過。 AC代碼: c++ include inc ...
  • 1 package java.util; 2 3 import sun.misc.SharedSecrets; 4 5 import java.util.function.Consumer; 6 import java.util.function.Predicate; 7 import java.u... ...
  • 搜onlyoffice document server的github上的issue,會得到這2個地址https://github.com/ONLYOFFICE/DocumentServer/issues/67https://github.com/ONLYOFFICE/DocumentServer/i ...
  • 為什麼我們可以在Startup這個 “孤零零的” 類中配置依賴註入和管道? 它是什麼時候被實例化並且調用的? 參數中的IServiceCollection services是怎麼來的? 處理管道是怎麼構建起來的? 啟動過程中,系統“默默的”做了哪些準備工作? 上一篇文章講了ASP.NET Core中 ...
  • 由於自身專業水平的欠佳,我對於XAML控制項的學習並不深刻,只在簡單瞭解過後產生了一二想法,也許十分荒謬,就減省地談談。以下五種控制項,是我在學習後,並不十分看好或有所疑慮的。 在瀏覽XAML Controls Gallery上各種控制項的簡單介紹時,這個控制項引起我註意,它通過坐標軸控制每個內容的分佈,是 ...
  • interface 學習 使用interface定義介面, 介面不儲存數據,所以沒有欄位,但是可以有屬性, 實現介面的類必須包括介面的所有方法和屬性,否則無法編譯。 公共介面中的所有方法都會自動成為公共方法,因為介面就是用來定義實現該介面的類應該具有的公共方法和屬性。 不能實例化介面,不過可以引用接 ...
  • 顧名思義,HTML輔助方法(HTML Helper)就是用來輔助產生HTML之用,在開發View的時候一定會面對許多HTML標簽,處理這些HTML的工作非常繁瑣,為了降低View的複雜度,可以使用HTML輔助方法幫助你產生一些HTML標簽或內容,因這些HTML標簽都有固定標準的寫法,所以將其包裝成H ...
  • 一、前言 最近忙裡偷閑,做了一個部署資料庫及IIS網站站點的WPF應用程式工具。 二、內容 此工具的目的是: 最終樣式:(Check按鈕的作用是防止與本機已有的站點或程式池有衝突) View: View的後臺文件: ViewModel: ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...