自己手擼一個 Spring MVC

2021-03-02 碼匠筆記

✋點擊「面試交流」加入交流群✋

「置頂公眾號」,每天推送面試專題

在 SpringBoot 之前,幾乎所有的 Web 應用都是已 web.xml 為入口的,Spring MVC 也不例外,學習過 Servlet 的應該都理解,Spring MVC 其實就是對 Servlet 接口,Servlet 規範的一種實現。Servlet 提供了五個接口,其中兩個接口最為核心,分別是 init 方法和 service 方法。init方法是在伺服器裝入 Servlet 時執行,在 Servlet 的生命周期中,它只執行一次。service方法和客戶端的請求相關,每當一個客戶端發生一次請求,請求一個 HttpServlet 對象,該對象的 service() 方法就會被調用。Spring MVC 的入口是從 web.xml 中配置的

<servlet>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>

開始的,也是 DispatcherServlet 完成了對 Servlet 接口的實現。對於Spring MVC的工作流程,在網上隨便一搜會搜到如下的概括:1. 用戶向服務端發送一次請求,這個請求會先到前端控制器DispatcherServlet。2. DispatcherServlet接收到請求後會調用HandlerMapping處理器映射器。

由此得知,該請求該由哪個Controller來處理。

1. DispatcherServlet調用HandlerAdapter處理器適配器,告訴處理器適配器應該要去執行哪個Controller。2. HandlerAdapter處理器適配器去執行Controller並得到ModelAndView,並層層返回給DispatcherServlet。3. DispatcherServlet將ModelAndView交給ViewReslover視圖解析器解析,然後返回真正的視圖。4. DispatcherServlet將模型數據填充到視圖中。

5. DispatcherServlet將結果響應給用戶。

其中,對於 HandlerMapping 和 HandlerAdapter 在閱讀源碼的過程中可能是不太好理解的。

HandlerMapping是一個接口,這個接口返回的是一個請求訪問時處理器映射器會返回具體的執行鏈(HandlerExecutionChain),其中包括攔截器和映射器,只是找到並不執行。

執行鏈這裡用到了設計模式中的責任鏈模式,每一個責任鏈的負責人只需要把自己的任務處理好就好。

而HandlerMapping為什麼是個接口呢,是因為 Spring MVC提供了三種不同的書寫處理器映射器的方法,只不過我們最常用的是通過註解,通過@Controller,@RequestMapping的方式去寫我們的Controller。

HandlerAdapter:

和 HandlerMapping 一樣,HandlerAdapter 也是個接口。

HandlerAdapter 的意思的是適配器,用到的也是設計模式中的適配器模式,在 Spring MVC 中針對不同的 Handler 需要不同的適配器,例如對於@RequestMapping類型的 Handler 需要 RequestMappingHandlerAdapter 來處理,適配之後通過調用接口的 handle 方法就可以執行對應的方法了。

代碼實現不管是 Spring 還是 Spring MVC,又或是 Mybatis,Spring Data等等。其實在閱讀源碼或者自己實現的過程中會發現,這些提高開發效率的,封裝型的框架,從頭到尾離不開的就是 Java 的反射以及 Java 的動態代理。結合反射以及AOP的思想,根據上面 Servlet 接口和 Spring mvc 流程的介紹以及平時對 Spring MVC 的使用,即使我們不看 Spring MVC 的源碼其實也能把我們經常使用的功能簡單的實現了。這裡對於HandlerMapping 和 HandlerAdapter 我們也不需要設計的如此複雜,只需要實現我們平時最常用的一種就好。流程設計想像一下,我們在開發某個系統,我們寫好了我們的 Spring MVC 控制層的代碼,但是我們沒有引入任何依賴,接下來怎麼讓我們的代碼 Run 起來呢?1. 第一步當然還是要從web.xml入手,和 Spring MVC 一樣,我們需要配置一個我們自己的 DispatcherServlet,這個 Servlet 繼承自 HttpServlet。2. 創建完自己的 Servlet 之後就是重寫上面提到過的兩個核心方法:在 Servlet 的生命周期中,init 只執行一次,對於我們編寫好的代碼,我們需要把所有的urlPath以及我們的控制器和控制器內的方法做一個映射,這樣每次客戶端發起一次請求,調用 service 方法的時候可以通過這個 mapping 映射找到對應它該執行的方法。

(由於功能比較簡單另外沒有實現 Interceptor 攔截器的功能,所以沒有使用 Spring MVC 使用的執行鏈的形式)

3. 找到方法後就該執行了,但是執行前,方法需要的參數我們還沒有填充。

參數分為很多種,有用@RequestParam修飾的基本數據類型,有數組,有Map,有對象等等。這裡 Spring mvc 用到的是設計模式中的策略模式,針對不同類型的參數會有不同的 Resolver。策略模式的使用場景是對於系統中的多個類或是說多個場景,用來區分它們的只是他們的行為不同,像我們要做的參數的解析,數據源都是 HttpServletRequest 的 Attribute,只是我們對於 Attribute 的處理行為不同,我們需要把它填充到不同類型的參數上而已。4. 在對 Method 的參數進行填充後,一切準備就緒了,這時候執行 Method 就可以得到相應的返回值了,返回值就是我們需要的視圖。我們常用的返回類型有兩種,一種是根據路徑直接返回一個指定的視圖,另外一種是我們平時在Spring MVC中用@ResponseBody或者@RestController修飾的直接返回給前端一個JSON形式的串。

這裡很簡單,其實就是根據不同的情況調用 Servlet 給我們提供的方法。

流程圖代碼結構HandleMappingHandleMapping 的功能如上面所說,只是根據請求找到我們對應處理請求的 handler。這個類主要有兩個函數,第一個是初始化,將代碼中所有被@Controller,@RequestMapping修飾的類和方法,以@RequestMapping的值作為key,以 Method 作為 value,初始化一個map。第二個就是根據 key 在剛才初始化的map中獲取對應的 Method。

public class HandleMapping {
    private static final Map<String, HandlerMethod> mappings = new HashMap<>();

    public static void init() {
        Set<Class<?>> controllerSet = ReflectionUtils.getAllClass(Controller.class);
        controllerSet.forEach((controller) -> {
            RequestMapping requestMappingAnnotation = controller.getAnnotation(RequestMapping.class);
            if (requestMappingAnnotation == null) {
                throw new DumpException("controller '" + controller.getName() + "' must have a '@RequestMapping' annotation");
            }
            String parentPath = requestMappingAnnotation.value();
            Method[] methods = controller.getMethods();
            for (Method method : methods) {
                RequestMapping methodRequestMappingAnnotation = method.getAnnotation(RequestMapping.class);
                if (methodRequestMappingAnnotation == null) {
                    continue;
                }
                String path = methodRequestMappingAnnotation.value();
                try {
                    mappings.put(parentPath + path, new HandlerMethod(controller.newInstance(), method));
                } catch (Exception e) {
                    throw new DumpException("init controller failed,can not create instance for controller '" + controller.getName() + "'", e);
                }
            }
        });
    }

    public static HandlerMethod getHandler(String url) {
        HandlerMethod handleMethod = mappings.get(url);
        if (handleMethod == null) {
            throw new DumpException("path '" + url + "' can not find handle");
        }
        return handleMethod;
    }
}

註:HandlerMethod 是對 Method 的一個簡單封裝,除了包含method外,還有當前Class的實例,方便於我們後面直接用這個實例執行這個方法。

HandlerMethodArgumentResolver
public interface HandlerMethodArgumentResolver {

    Boolean support(Parameter parameter);

    Object resolveArgument(HttpServletRequest request, Class<?> requiredType, Parameter parameter);
}

HandleMapping 已經幫我們找到了具體的 Method,但是相關的參數還沒有填充,HandlerMethodArgumentResolver 就是專門用來填充參數的。HandlerMethodArgumentResolver 是個接口,有兩個方法:support 和 resolveArgument。support 用於判斷當前的參數是否支持該Resolver解析填充,resolveArgument 用來做具體的解析填充操作。這裡我只實現了兩種常見的Resolver:一種是基於@RequestParam的基本數據類型的參數解析器 RequestParamResolver,一種是對象類型的參數解析器 RequestModelResolver。這裡和Spring mvc不同,因為我暫時只實現了這兩種,而對於Map,數組等形式的都暫不支持。為了方便識別對象,我規定需要解析參數的對象都需要用@RequestModel來修飾。參數的解析其實就是根據參數的名稱去 HttpServletRequest 對象中調用 getParameter() 來獲取我們想要的參數,對於對象來說就是額外做一次反射。這裡比較複雜的其實是對於參數類型的轉換,因為我們通過 request.getParameter() 拿到的是 String 類型的數值,我們需要轉換成參數本身需要的類型,所以這裡我們需要一個參數轉換的 converter。這個converter雖然寫起來比較麻煩,但是很容易理解,這裡就不贅述了,直接看代碼都可以看懂。HandleMethodAdapter解析完參數就可以執行方法了,HandleMethodAdapter 除了負責參數的填充解析,還有就是負責調用這個方法,並得到具體的返回值。返回值這裡我們強制規定只能返回 String,對於這個 String 類型的返回值,我們分成三種情況來處理:

1. 方法用了@ResponseBody修飾。

對於這種返回值,通過調用response.getWriter().print()來將返回值寫入 response 中。

2. 方法沒用@ResponseBody修飾,返回值含有redirect:前綴。

這代表該請求是一個重定向請求,這裡用response.sendRedirect()將請求重定向。

3. 方法沒用@ResponseBody修飾,返回值也不含有redirect:前綴。

這種返回值代表直接返回一個具體的視圖,直接調用requestDispatcher.forward()即可。DumpServletDispatcher準備完成,在自己的 Servlet(繼承自 HttpServlet) 中完成上述流程即可。其中 HandleMapping 的初始化放在 init() 中執行。

public class DumpServletDispatcher extends HttpServlet {

    @Override
    public final void init() {
        HandleMapping.init();
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String uri = req.getRequestURI();
        HandlerMethod handleMethod = HandleMapping.getHandler(uri);
        HandleMethodAdapter handleMethodAdapter = new HandleMethodAdapter();
        handleMethodAdapter.handle(req, resp, handleMethod);
    }
}

最後以上其實就是對 Spring MVC 中的核心功能或者說我們最常用的功能的實現。其實我在大二的時候寫過類似的框架,自己起了名字叫 Dump。裡面除了包含 Spring MVC 的功能,還包含了 Spring 的 IOC,AOP,還有類似 Spring Data/Hibernate 的 ORM 層的功能。但是當時是在沒有看 Spring 相關的源碼的情況下寫的,裡面很多細節或是設計模式都沒有學習到,只是因為當時學習了 Java 的反射和動態代理覺得可以實現一下就寫了一版。最近看了源碼之後想再好好設計實現一遍,目前還是只完成了上面提到過的這些 MVC 的部分,新的代碼倉庫如下,上面提到過的所有相關代碼也都在裡面,歡迎持續關注。www.github.com/yuanguangxin/Dumphttps://github.com/yuanguangxin/DumpDemo

本文作者「袁廣鑫」,歡迎關注作者的知乎:

https://zhuanlan.zhihu.com/p/139751932 專注於 Java 技術分享,點擊閱讀原文即可關注。

公眾號運營至今,離不開小夥伴們的支持。為了給小夥伴們提供一個互相交流的平臺,特地開通了官方交流群。掃描下方二維碼備註 進群 後獲取進群通道。

相關焦點

  • Spring MVC簡介
    Spring MVC請求處理的整體流程如圖:Spring3.x中定義一個控制器類,必須以@Controller註解標記。當控制器類接收到一個請求時,它會在自己內部尋找一個合適的處理方法來處理請求。使用@RequestMapping註解將方法映射到一些請求上,以便讓該方法處理那些請求。
  • Spring MVC 過時了嗎?
    提出這個問題時我對spring這一套體系還是懵懵懂懂一知半解,現在我已經對該問題有了更全面的認識,我也開發了一些基於ssm的前後端分離的系統。現在jsp似乎已經漸漸淡出大家的視野。web開發朝著前後端分離的方向去了像spring mvc這樣前後端耦合較大的框架是否過時了?首先給出結論:Spring MVC沒有過時,它仍然是當前主流的Java Web開發框架。
  • 使用idea創建spring mvc項目圖文教程 - 凱哥Java
    使用idea創建spring mvc項目圖文教程前言:使用慣了eclipse的朋友,如果剛換成了idea或許有些不習慣。但是使用idea之後,就會love上idea了。本文將通過圖文講解怎麼通過idea創建一個spring mvc項目。
  • Java Web系列:Spring MVC基礎
    (1)前端控制器FrontControllerASP.NET和JSP都是以Page路徑和URL一一對應,Web MVC要通過URL映射Controller和View,就需要一個前端控制器統一接收和解析請求,再根據的URL將請求分發到Controller。
  • Spring-mvc的原理講的不太好?那我手寫來解釋下吧——>面試官
    筆者自己也改過struts項目的BUG。這都是一些很久的老項目了,儘管struts有版本的更新,但是2013年被曝出存在重要的安全漏洞,很多用了struts框架項目的公司都慌了。。。運維工程師很痛苦。Spring-mvc原理先來個草圖看一下,然後大致解釋下,說錯了,幫大家鞏固下它的工作原理。其實看上面的圖基本能曉得了。
  • Spring MVC 異常處理詳解
    如果Web應用程式中配置了ResponseStatusExceptionResolver,那麼我們就可以使用ResponseStatus註解來註解我們自己編寫的異常類,並在Controller中拋出該異常類,之後ResponseStatusExceptionResolver就會自動幫我們處理剩下的工作。
  • Spring Boot 和 Spring 到底有啥區別?
    >org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.0.RELEASE</version></dependency>與Spring不同,Spring Boot只需要一個依賴項來啟動和運行
  • Spring 和 Spring Boot 之間到底有啥區別?
    ;artifactId>spring-webmvc</artifactId><version>5.1.0.RELEASE</version></dependency>與Spring不同,Spring Boot只需要一個依賴項來啟動和運行Web應用程式:<dependency
  • Spring 中那些讓你愛不釋手的代碼技巧
    六 spring mvc攔截器,用過的都說好spring mvc攔截器根spring攔截器相比,它裡面能夠獲取HttpServletRequest和HttpServletResponse 等web對象實例。
  • Spring 和 Spring Boot 最核心的 3 大區別,詳解!
    >spring-webmvc</artifactId>    <version>5.1.0.RELEASE</version></dependency>與Spring不同,Spring Boot只需要一個依賴項來啟動和運行Web應用程式:
  • 五分鐘學Java:一篇文章搞懂spring和springMVC
    相信很多人和我一樣,第一次了解spring都不是做項目的時候用到,而是在網上看到或者是聽到過一個叫做spring的框架,這個框架號稱完爆之前的structs和structs2,吸引了不少人的注意。spring基礎spring的核心就是IOC和AOP了,把這兩點搞懂,你就可以說自己學會了spring(然而並不是)首先來看看IOC,IOC的本質就是把bean的管理交給框架去做,spring自己維護一個bean容器,將所有bean進行統一管理,這樣一來,所有需要用到實例的場景都不需要寫繁瑣且重複的實例化代碼
  • Spring 和 SpringBoot 之間到底有啥區別?
    </groupId><artifactId>spring-webmvc</artifactId><version>5.1.0.RELEASE</version></dependency>與Spring不同,Spring Boot只需要一個依賴項來啟動和運行
  • 高手如何給 Spring MVC 做單元測試?
    在前面的章節我們介紹過 Junit 的使用,也了解過 spring-test,今天我們來了解一個新玩意 -- mock 測試。、什麼是 mock 測試在測試過程中,對於某些不容易構造或者不容易獲取的對象,用一個「虛擬的對象」來創建以便測試的測試方法,就是 「mock 測試」在測試過程中,對於某些不容易構造或者不容易獲取的對象,用一個「虛擬的對象」來創建以便測試的測試方法
  • Spring中這些能升華代碼的技巧,可能會讓你愛不釋手
    六 spring mvc攔截器,用過的都說好spring mvc攔截器根spring攔截器相比,它裡面能夠獲取HttpServletRequest和HttpServletResponse 等web對象實例。
  • 英語教學品牌Eurocentres被德國mvc集團收購
    即將由德國mvc Education&Career集團收購的教學條款,包括在美國、加拿大、澳大利亞、馬爾他、沙烏地阿拉伯開展的活動,以及即將在印度開展的業務。同時,德國mvc集團即將收購的語言教育中心基金會(The Foundation for Language and Educational Centres),將保留對法國設施和日本設施的控制權。
  • Spring 和 SpringBoot 最核心的 3 大區別,詳解!
    Boot只需要一個依賴項來啟動和運行Web應用程式:<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web<
  • 自學MVC看這裡——全網最全ASP.NET MVC 教程匯總
    Asp.Net MVC4入門指南(9):查詢詳細信息和刪除記錄Asp.Net MVC4入門指南(8):給數據模型添加校驗器Asp.Net MVC4入門指南(7):給電影表和模型添加新欄位Asp.Net MVC4入門指南(6):驗證編輯方法和編輯視圖Asp.Net MVC4入門指南(5):從控制器訪問數據模型Asp.Net
  • Spring Boot 無侵入式 實現API接口統一JSON格式返回
    是不是很雞湯, 是不是很勵志, 讓我對前輩們充滿著崇拜, 事實上他對我說的是: 「自己去百度」, 這五個大字, 其實這五個大字已經說明上明的B話了, 通過不斷的百度和Google發現了很多的解決方案.我們都知道使用@ResponseBody註解會把返回Object序列化成JSON字符串,就先從這個入手吧, 大致就是在序列化前把Object賦值給Result<Object>就可以了, 大家可以觀摩org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
  • Spring MVC 接收請求參數所有方式總結!
    那麼,能不能把整個請求參數塞在一個欄位中提交呢?解決辦法還是有的,添加一個org.springframework.core.convert.converter.Converter實現即可:,賦值給名為spring的路徑變量。
  • Java程式設計師必會 springmvc-spring-mybatis框架整合搭建傻瓜教程
    ssm是用於將springmvc-spring-mybatis三個框架整合來進行java開發web項目。本文通過ssm三大框架整合的形式講解springmvc的使用教程,最新的框架版本主流IDE,只要按照圖中步驟能夠保證每個人的框架搭建完成並成功運行。這裡使用maven來構建項目,我們需要創建一個名為ssm的maven項目,打包方式為war包。