day05-SpringMVC底層機制簡單實現-01

来源:https://www.cnblogs.com/liyuelian/archive/2023/02/09/17107360.html
-Advertisement-
Play Games

SpringMVC底層機制簡單實現-01 主要完成:核心分發控制器+Controller和Service註入容器+對象自動裝配+控制器方法獲取參數+視圖解析+返回JSON格式數據 1.搭建開發環境 創建 Maven 項目,File-New-Project-Maven 將 pom.xml 文件中的編譯 ...


SpringMVC底層機制簡單實現-01

主要完成:核心分發控制器+Controller和Service註入容器+對象自動裝配+控制器方法獲取參數+視圖解析+返回JSON格式數據

1.搭建開發環境

  1. 創建 Maven 項目,File-New-Project-Maven

    image-20230209164653089 image-20230209164838732 image-20230209165214529
  2. 將 pom.xml 文件中的編譯版本改為1.8

    image-20230209165655198
  3. 在 src 目錄下創建以下目錄:

    java 代碼放在 java 目錄下,相關的資源文件放在 resource 目錄下,對 maven 的 web 項目而言,resource 就是類路徑。前端頁面放在 webapp 下,該目錄對應之前的 web 目錄。test/java 目錄用於存放測試文件,測試需要的資源文件放在 test/resource 目錄下。

    image-20230209170051719
  4. 在 pom.xml 中引入基本的 jar 包

    <dependencies>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
      </dependency>
    
      <!--引入原生servlet依賴的jar包-->
      <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <!--
          1.scope表示引入jar包的作用範圍,
          2.provided表示項目在打包放到生產環境時,不需要打上servlet-api.jar
          3.因為 tomcat本身就有該jar包,使用tomcat的即可,防止版本衝突
        -->
        <scope>provided</scope>
      </dependency>
    
      <!--引入dom4j,用於解析xml-->
      <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
      </dependency>
    
      <!--引入常用的工具類jar包,該jar含有很多常用的類-->
      <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.5</version>
      </dependency>
    </dependencies>
    

2.任務1-開發MyDispatcherServlet

說明:編寫 MyDispatcherServlet,充當原生的 DispatcherServlet(即核心控制器)

2.1分析

image-20230209174244370

2.2代碼實現

  1. 創建 src/main/java/com/li/myspringmvc/servlet/MyDispatcherServlet.java,充當原生的前端控制器。

    package com.li.myspringmvc.servlet;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @author 李
     * @version 1.0
     * 1.MyDispatcherServlet 充當原生的 DispatcherServlet,它的本質就是一個Servlet
     * 因此繼承 HttpServlet
     */
    public class MyDispatcherServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("MyDispatcherServlet doGet() 被調用..");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("MyDispatcherServlet doPost() 被調用..");
        }
    }
    
  2. 創建 src/main/resources/myspringmvc.xml,充當原生的 applicationContext-mvc.xml(即 spring 容器配置文件)

  3. 配置 src/main/webapp/WEB-INF/web.xml

    <!DOCTYPE web-app PUBLIC
            "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
            "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
    <web-app>
        <display-name>Archetype Created Web Application</display-name>
        <servlet>
            <servlet-name>MyDispatcherServlet</servlet-name>
            <servlet-class>com.li.myspringmvc.servlet.MyDispatcherServlet</servlet-class>
            <!--給前端控制器指定配置參數,指定要操作的spring容器文件-->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:myspringmvc.xml</param-value>
            </init-param>
            <!--要求該對象在tomcat啟動時就自動載入-->
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>MyDispatcherServlet</servlet-name>
            <!--作為前端控制器,攔截所有請求-->
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    
    </web-app>
    
  4. 配置 Tomcat,進行測試

  5. 瀏覽器訪問 http://localhost:8080/li_springmvc/aaa

    image-20230209175857755

3.任務2-實現客戶端/瀏覽器可以請求控制層

3.1分析

image-20230209230249962

任務2的總目標是:

實現自己的 @Controller 註解和 @RequestMapping 註解,當瀏覽器訪問指定的 URL 時,由前端控制器,找到 Controller 的某個方法,然後通過 tomcat 將數據返回給瀏覽器。

3.2代碼實現

image-20230209221006109

步驟一:兩個註解和測試Controller

(1)Controller 註解

package com.li.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * @author 李
 * @version 1.0
 * 該註解用於標識一個控制器組件
 * 1.@Target(ElementType.TYPE) 指定自定義註解可修飾的類型
 * 2.@Retention(RetentionPolicy.RUNTIME) 作用範圍,RUNTIME使得可以通過反射獲取自定義註解
 * 3.@Documented 在生成文檔時,可以看到自定義註解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}

(2)RequestMapping 註解

package com.li.myspringmvc.annotation;

import java.lang.annotation.*;

/**
 * @author 李
 * @version 1.0
 * RequestMapping 註解用於指定控制器-方法的映射路徑
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

(3)用於測試的控制器 MonsterController.java

package com.li.controller;

import com.li.myspringmvc.annotation.Controller;
import com.li.myspringmvc.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author 李
 * @version 1.0
 */
@Controller
public class MonsterController {
    //編寫方法,可以列出妖怪列表
    //springmvc支持原生的servlet api,為了看到底層機制,這裡直接放入兩個參數
    @RequestMapping(value = "/monster/list")
    public void listMonster(HttpServletRequest request, HttpServletResponse response) {
        //設置編碼
        response.setContentType("text/html;charset=utf-8");
        //獲取writer,返回提示信息
        try {
            PrintWriter printWriter = response.getWriter();
            printWriter.print("<h1>妖怪列表信息</h1>");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

步驟二:配置容器文件 springmvc.xml,指定掃描的包

指定掃描的包是為了之後使用註解獲取需要反射的類

如果需要添加新的掃描包,在base-package添加包路徑,用逗號表示間隔。

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <!--指定要掃描的包及其子包的java類-->
    <component-scan base-package="com.li.controller,com.li.service"/>
</beans>

步驟三:編寫 XMLParse 工具類,用於解析 springmvc.xml,得到要掃描的包

package com.li.myspringmvc.xml;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * XMLParse用於解析spring配置文件
 */
public class XMLParse {
    public static String getBasePackage(String xmlFile) {
        SAXReader saxReader = new SAXReader();

        //maven的類路徑是在target/li-springmvc/WEB-INF/classes/目錄下
        //通過類的載入路徑-->獲取到spring配置文件[對應的資源流]
        InputStream inputStream =
                XMLParse.class.getClassLoader().getResourceAsStream(xmlFile);
        try {
            //得到配置文件的文檔
            Document document = saxReader.read(inputStream);
            Element rootElement = document.getRootElement();
            Element componentScanElement = rootElement.element("component-scan");
            Attribute attribute = componentScanElement.attribute("base-package");
            String basePackage = attribute.getText();
            return basePackage;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

步驟四:開發MyWebApplicationContext,充當原生Spring容器,得到掃描類的全路徑列表

即把指定目錄包括子目錄下的 java 類的全路徑掃描到 ArrayList 集合中,以便之後反射。是否需要反射,還要取決於類中是否添加了@Controller註解

(1)MyWebApplicationContext.java 實現自定義的 spring 容器,目前先完成掃描工作

package com.li.myspringmvc.context;

import com.li.myspringmvc.xml.XMLParse;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 李
 * @version 1.0
 * MyWebApplicationContext 是我們自定義的spring容器
 */
public class MyWebApplicationContext {
    //屬性classFullPathList用於保存掃描包/子包的類的全路徑
    private List<String> classFullPathList = new ArrayList<>();

    //該方法完成對自己的 spring容器的初始化
    public void init() {
        //返回的是我們在容器文件中配置的base-package的value
        String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
        //這時你的 basePackage是像 com.li.controller,com.li.service 這樣子的
        //通過逗號進行分割包
        String[] basePackages = basePackage.split(",");
        if (basePackages.length > 0) {
            //遍歷這些包
            for (String pack : basePackages) {
                scanPackage(pack);
            }
        }
        System.out.println("掃描後的路徑classFullPathList=" + classFullPathList);
    }

    /**
     * 該方法完成對包的掃描
     * @param pack 表示要掃描的包,如 "com.li.controller"
     */
    public void scanPackage(String pack) {
        //得到包所在的工作路徑[絕對路徑]
        // (1)通過類的載入器,得到指定包的工作路徑[絕對路徑]
        // (2)然後用斜杠代替點=>如 com.li.controller=>com/li/controller
        URL url =
                this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));
        // url=file:/D:/IDEA-workspace/li-springmvc/target/li-springmvc
        // /WEB-INF/classes/com/li/controller/
        //System.out.println("url=" + url);

        //根據得到的路徑,對其進行掃描,把類的全路徑保存到 classFullPathList屬性中
        String path = url.getFile();
        System.out.println("path=" + path);
        //在io中,把目錄也視為一個文件
        File dir = new File(path);
        //遍歷 dir目錄,因為可能會有[多個文件/子目錄]
        File[] files = dir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                //如果是目錄,需要遞歸掃描
                //pack加上下一級的目錄名繼續下一層的掃描
                scanPackage(pack + "." + file.getName());
            } else {
                //這時得到的文件可能是.class文件,也可能是其他文件
                //就算是class文件,還需要考慮是否要註入到容器的問題
                //目前先把所有文件的全路徑都保存到集合中,後面註入對象到spring容器時再考慮過濾
                String classFullPath =
                        pack + "." + file.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }
    }
}

(2)通過 MyDispatcherServlet 前端控制器來調用並初始化 spring 容器

//添加init方法,用於初始化spring容器
@Override
public void init() throws ServletException {
    MyWebApplicationContext myWebApplicationContext = new MyWebApplicationContext();
    myWebApplicationContext.init();
}

(3)啟動tomcat,後臺成功獲取到了路徑,測試成功。

tomcat啟動--載入了MyDispatcherServlet--通過該Servlet的init()生命周期方法初始化自定義的 spring 容器,同時調用自定義 spring 容器的 init 方法去掃描包

image-20230209215714429 image-20230209215753556

步驟五:完善MyWebApplicationContext(自定義 spring 容器),實例化對象到容器中

將掃描到的類,在滿足添加了註解的情況下,通過反射註入到 ioc 容器

(1)部分代碼:在MyWebApplicationContext中添加新屬性 ioc 和新方法 executeInstance。

//定義屬性ioc,用於存放反射生成的 bean對象(單例的)
public ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();

/**
 * 該方法完成對自己的 spring容器的初始化
 */
public void init() {
    //返回的是我們在容器文件中配置的base-package的value
    String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
    //這時你的 basePackage是像 com.li.controller,com.li.service 這樣子的
    //通過逗號進行分割包
    String[] basePackages = basePackage.split(",");
    if (basePackages.length > 0) {
        //遍歷這些包
        for (String pack : basePackages) {
            scanPackage(pack);
        }
    }
    System.out.println("掃描後的路徑classFullPathList=" + classFullPathList);
    //將掃描到的類反射到ioc容器
    executeInstance();
    System.out.println("掃描後的ioc容器=" + ioc);
}

//...

/**
 * 該方法將掃描到的類,在滿足條件的情況下進行反射,並放入到ioc容器中
 */
public void executeInstance() {
    //是否掃描到了類
    if (classFullPathList.size() == 0) {//沒有掃描到類
        return;
    }
    //遍歷 classFullPathList,進行反射
    try {
        for (String classFullPath : classFullPathList) {
            Class<?> clazz = Class.forName(classFullPath);
            //判斷是否要進行反射(即是否添加了註解)
            if (clazz.isAnnotationPresent(Controller.class)) {
                Object instance = clazz.newInstance();
                //獲取該對象的id,預設情況下為類名(首字母小寫)
                String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
                        + clazz.getSimpleName().substring(1);
                String value = clazz.getAnnotation(Controller.class).value();
                if (!"".equals(value)) {//如果註解的value指定了id
                    beanName = value;
                }
                ioc.put(beanName, instance);
            }//如果有其他註解,可以進行擴展
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

(2)啟動tomcat,反射成功。

image-20230209224358698

步驟六:完成請求URL和控制器方法的映射關係


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

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 項目效果 我們今天要實現的是一個路徑規劃的功能,有兩個輸入框。輸入起點終點,然後查詢,得到規劃的路徑,效果如下: 我們會用到以下庫: Axios:用於發送請求,請求高德地圖的地理編碼API Jquery:也用於發送請求 Cesium:地圖 ...
  • 上文創建了一堆 utils、component-info,並實現了新組件模塊相關目錄和文件的創建。本文繼續實現後面的內容。 1 組件樣式文件並導入 在 src/service 目錄中創建 init-scss.ts 文件,該文件導出 initScss 函數。 由於 .vue 類型的組件的樣式就直接寫在 ...
  • JavaScript 詞法環境 本文主要講解JS詞法環境,我們將看到什麼是詞法環境,詞法範圍如何工作,函數內部的名稱如何解析,內部屬性,弄清楚詞法環境利於我們理解閉包。讓我們開始吧... 什麼是詞法環境? 在理解閉包時,最大的混淆來源是術語“辭彙環境”,或者只是“辭彙”這個詞。在電腦科學中術語“詞 ...
  • 演示代碼使用 Vue3 + ts + Vite 編寫,但是也會列出適用於 Vue2 的優化技巧,如果某個優化只適用於 Vue3 或者 Vue2,我會在標題中標出來。 代碼優化 v-for 中使用 key 使用 v-for 更新已經渲染的元素列表時,預設用就地復用策略;列表數據修改的時候,他會根據 k ...
  • 一、使用 performance.now() API 在 JavaScript 中,可以使用 performance.now() API 來評測代碼的運行速度。該 API 返回當前頁面的高精度時間戳,您可以在代碼執行前後調用它來計算代碼執行所需的時間。 例如: let t0 = performanc ...
  • 在19年11月的時候買了一個運動手環,然後時不時會用它來記錄睡眠數據;積累到現在已經有40個月了。現在想要調整作息,分析一下這些數據,來制定合理的作息計劃。 圖1 月平均入睡時間 從圖1可以看出,我最經常的入睡時間是(02:00:00~02:10:00)之間; 現在我想要早睡,逐步調整,第一個目標值 ...
  • 一、前言 在上一節,我們新建了工程,做好了準備。本節在工程中加入B站網頁,屏蔽頁面廣告。 二、工程配置 2.1、基礎配置 配置 程式啟動事件,填入以下代碼 載入網頁("https://m.bilibili.com/index.html") -- 載入網頁 為軟體自帶的函數,後面做函數介紹 -- 鏈接 ...
  • 問題描述 ResultSet 表示 select 語句的查詢結果集。ResultSet 對象具有指向其當前數據行的指針, 最初,指針被置於第一行記錄之前,通過 next() 方法可以將指針移動到下一行記錄。 next() 方法在 ResultSet 對象沒有一行記錄時返回 false ,因此可以在 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...