面試官:Spring MVC 的處理流程是怎樣的?

提起 Spring MVC,你的第一印象是什麼?一個簡化 Web 開發的輕量級框架?實際上,現代開發過程中,開發流程與開發效率的不斷提高,同時伴隨着 Restful 與 Json 相結合的方式的興起,使得多個設備跨平臺的相互調用與訪問變得簡單了許多,所以 Spring MVC 簡化 Web 開發的使命也自然而然的變爲了簡化服務端開發。那麼今天我們就拋開繁雜的代碼,從宏觀的角度來看一看 Spring MVC 對於處理請求,簡化服務端開發的解決方案是如何實現的。

1、曾經的王者——Servlet

在筆者剛接觸到使用 Java 進行 Web 開發的時候,Spring MVC 遠沒有今天這麼流行,君不見曾經的王者 Servlet 繁盛一時的場面。現在回想起來,使用 Servlet 進行開發雖然不像現在這麼容易,好多的事情需要自己做,但是 Servlet 使得開發的邏輯變得十分清晰,尤其是在 Servlet 與 jsp 很好的承擔了各自的角色之後,再加上 mvc 分層思想的流行。編寫 Web 應用程序在那時是一件快樂而又簡單的事情。

實際上 Servlet 做的事情並不是很多,筆者覺得 Servlet 想要完成的就是統一請求的接受、處理與響應的流程。

網絡編程中繞不開的一個東東想必不用說大家也猜得到,那就是 Socket。但是網絡需要傳輸的話是很複雜的,首先需要遵循一定的協議,現在我們一般使用 Http 與 Https 傳輸數據,而 Socket 就是在一些網絡協議之上,屏蔽了底層協議的細節,爲使用者提供一個統一的 api。但是 Servlet 認爲 Socket 做的還不夠,或者說我們還要進行相應的處理。於是 Servlet(就 HttpServlet 來說),他將網絡中的請求報文進行封裝轉化成爲了 Request 表示,在 Http 通信過程之中就是 HttpServletRequest,而將服務端處理請求後返回的響應統一的封裝爲了 HttpServletResponse 對象。

這樣做的好處是什麼呢?

我們作爲開發者,不必再去做一些處理網絡請求與響應的繁瑣之事,而只需要關注於我們的業務邏輯開發。

大家有沒有發現,每一次框架效率的提升很多時候都是在將最最重要的業務邏輯與其他任務儘可能完全的分離開,使我們總可以全身心的投入到業務邏輯的開發之中,Spring AOP 是不是就是一個很好的佐證呢!

那麼 Servlet 如何使用呢?

沒有 Servlet 使用經歷的同學可以聽我簡單的說一說:

  1. 首先我們通常要編寫一個自己的 Servlet 然後繼承自 HttpServlet, 然後重寫其 doGet() 與 doPost() 方法。這兩個方法都會將 HttpServletRequest 與 HttpServletResponse 作爲參數傳遞進去,然後我們從 Request 中提取前端傳來的參數,在相應的 doXXX 方法內調用事先編寫好的 Service 接口,Dao 接口即可將數據準備好放置到 Response 中並跳轉到指定的頁面即可,跳轉的方式可以選擇轉發或者重定向。

  2. Servlet 使用的是模板方法的設計模式,在 Servlet 頂層將會調用 service 方法,該方法會構造 HttpServletRequest 與 HttpServletResponse 對象作爲參數調用子類重寫的 doXXX() 方法。然後返回請求。

  3. 最後我們需要將我們編寫的自定義 Servlet 註冊到 web.xml 中,在 web.xml 中配置 servlet-mapping 來爲該 servlet 指定處理哪些請求。

Servlet 的使用就是這麼簡單!事實上,在很長的一段時間內他的流行也得益於他的簡單易用易上手。

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">
    <servlet>
            <servlet-name>ShoppingServlet</servlet-name>
            <servlet-class>com.myTest.ShoppingServlet</servlet-class>
    </servlet>

    <servlet-mapping>
            <servlet-name>ShoppingServlet</servlet-name>
            <url-pattern>/shop/ShoppingServlet</url-pattern>
    </servlet-mapping>
</web-app>

2、想要更進一步

當我們使用 Servlet 來進行業務邏輯開發的時候,時常會感覺到爽歪歪,但是爽歪歪的同時也感覺到有那麼一點點不適。不適的地方主要有以下幾點:

帶着這些思考,能不能進一步的來抽離業務邏輯的開發呢?

在早期的時候筆者也曾進行一些嘗試,其大概思路就是編寫一個 BaseServlet,然後我們自己定義的 Servlet 繼承自 BaseServlet,前端的請求需要指定 Servlet 的哪個方法進行處理,這樣請求的時候將需要帶上一個 method 參數,例如這樣:

http://localhost:8080/myProject/MyServlet?method=getInfo

在 BaseServlet 中將提取該參數信息,並使用反射的方法調用子類的該方法,子類方法統一返回 String 類型的結果,代表要返回的邏輯視圖名,也就是要跳轉的路徑,然後父類拿到結果,使用重定向或者轉發進行跳轉。

說到這裏,有小夥伴肯定不耐煩了,明明是講 Spring MVC 的,到現在連個 Spring MVC 的影都還沒見,全是在講 Servlet。先彆着急,理解這些對我們理解 Spring MVC 有很大的幫助,請往下看

說到這裏,其實是想說,如果我們想要在 Servlet 上更進一步,想要進一步的將業務邏輯與其他工作相分離,那麼就需要在 Servlet 之上,構建一個事無鉅細,任勞任怨,神通過大,...(額想不起來。。。) 的超級 Servlet,來爲我們做這些工作,我們暫且把這個 Servlet 叫做超級牛逼 Servlet。而開發 Spring 的那些人是啥大佬,我們能想到這些,他們能想不到?於是他們動手開發了這個超級牛逼 Servlet,並正式命名爲 DispatcherServlet。

3、Spring MVC——兩級控制器方式

接下來我們就要正式的開始 Spring MVC 之旅了,通過前面的瞭解,我們知道 Spring MVC 把那個超級牛逼 Servlet 叫做 DispatcherServlet,這個 Servlet 可以說爲簡化我們的開發操碎了心,我們稱之爲_**前端控制器**_。現在我們不禁思考,前面我們寫的 BaseServlet 對應現在的超級牛逼 Servlet(DispatcherServlet)。那麼定義我們業務邏輯的自定義 Servlet 叫啥呢?Spring MVC 管定義我們的業務邏輯處理的類叫做 Handler,只不過他不再是一個 Servlet 了,而是一個普普通通的類,這也很好理解,畢竟 DispatcherServlet 做了太多,而且那麼牛逼,完全可以像對待 Servlet 一樣對待一個普通的類,而這個 Handler 就叫做_**次級控制器**_。

這裏可能有小夥伴持反對意見了,有的書上說了 Spring MVC 的次級控制器叫 Controller,不是 Handler。

其實 Spring MVC 的次級控制器確實是叫 Handler,只不過 Hander 是一個抽象的,而 Spring MVC 選擇使用 Controller 來實現 Handler,講到這裏,你覺得我們能不能自定義一個 Handler 實現,叫做 Lellortnoc 呢?答案當然是可以的!就好像 List 是一個抽象的接口,而 List 的實現有 ArrayList,LinkedList 一樣。

4、DispatcherServlet——前端控制器

DispatcherServlet 是整個 Spring MVC 的核心,超級牛逼 Servlet 這個榮譽稱號他是名副其實。DispatcherServlet 和其家族成員兄弟一起完成了很多的工作,包括請求參數的自動綁定,參數的自動校驗,請求 url 的自動匹配,邏輯視圖名到真實頁面的跳轉,數據獲取與數據渲染顯示的分離等等。。。在此過程中他更像是一個指揮家,有條不紊的指揮着請求不斷的向前處理,並最終完成服務端的響應數據。

想要了解具體 DispatcherServlet 都是怎麼指揮的,那就繼續往下看吧!推薦:250 期面試題彙總

5、HandlerMapper——請求映射專家

想想我們在使用 Servlet 編寫代碼的時候,請求的映射工作是交給了 web.xml。但是現在 Spring MVC 採用了兩級控制器的方式,就必須解決這個棘手的問題。

首先 DispatcherServlet 也是一個 Servlet,那麼我們也應該在 web.xml 中配置其處理的請求路徑。那麼應該配置什麼路徑呢?我們說 DispatcherServlet 被稱爲超級牛逼 Serlvet,我們希望它能處理所有的請求,那麼就可以讓 DispatcherServlet 接受所有請求的處理。像下面這樣配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
 <servlet>
          <servlet-name>Spring MVC</servlet-name>
          <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
          <!-- 表示啓動容器時初始化該servlet -->
          <init-param>
              <param-name>contextConfigLocation</param-name>
              <param-value>classpath:Spring-servlet.xml</param-value>
          </init-param>
          <load-on-startup>1</load-on-startup>
     </servlet>
     <servlet-mapping>
          <servlet-name>Spring MVC</servlet-name>
          <url-pattern>/*</url-pattern>
     </servlet-mapping>

</web-app>

現在所有的請求都被映射到了 DispatcherServlet,那麼 DispatcherServlet 現在就有責任將請求分發至具體的次級控制器,如何找到或者說如何保存請求到具體的次級控制器的這種映射關係呢?DispatcherServlet 選擇請求他的好兄弟 HandlerMapping。

在 HandlerMapping 中,保存了特定的請求 url 應該被哪一個 Handler(也就是通常的 Controller) 所處理。HandlerMapping 根據映射策略的不同,大概有下面幾種映射查找方式:

  1. org.springframework.web.servlet.handler.SimpleUrlHandlerMapping 通過配置請求路徑和 Controller 映射建立關係,找到相應的 Controller

  2. org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping 通過 Controller 的類名找到請求的 Controller。

  3. org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping 通過定義的 beanName 進行查找要請求的 Controller

  4. org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping 通過註解 @RequestMapping(“/userlist”) 來查找對應的 Controller。

想必現在最常用的就是第四種了吧,直接在對應的 Controller 上以及其內部的方法之上加上相應的註解,就可以配置好請求的映射,簡直是香香的。推薦:250 期面試題彙總

6、Handler 的攔路虎——HandlerInterceptor

聊到這裏,你以爲 DispatcherServlet 把請求的 url 交給 HandlerMapping, HandlerMapping 根據請求查出對應的 Controller 來交給 DispatcherServlet, 然後 DispatcherServlet 交給 Controller 執行就完事了?那就 To young to native 了,這其中還有一些小插曲。比如我們不能啥請求不管三七二十一都交給 Handler 執行吧,最起碼要過濾一下不合理的請求,比如跳轉頁面的時候檢查 Session, 如果用戶沒登錄跳轉到登錄界面啊,以及一些程序的異常以統一的方式跳轉等等,都需要對請求進行攔截。

如果對 Servlet 瞭解的同學是不是有一點似曾相識的感覺?沒錯,Servlet 中的 Filter 也可以完成請求攔截與過濾的功能,不過既然 Spring MVC 是兩級控制器結構,那麼 HandlerInterceptor 就與 Filter 有一些細微的差別,其最主要的差別,筆者認爲 HandlerInterceptor 提供了更細粒度的攔截。畢竟 Filter 攔截的對象是 Serlvet,而 HandlerInterceptor 攔截的則是 Handler(Controller)。用一張圖可以生動的表現出來。

HandlerInterceptor.jpg

從圖中我們可以看出 HandlerInteceptor 可以配置多個,其中任何一個返回 false 的話,請求都將被攔截,直接返回。

7、次級控制器——Handler

前端控制器我們已經很熟悉了,而次級控制器也就是 Handler,是我們真正執行業務邏輯的類。通常在 Spring MVC 中,這個 Handler 就是我們很熟悉的 Controller。我們調用封裝好的業務邏輯接口就是在這裏進行處理的。可以說 Spring MVC 已經將業務邏輯與其他不相關的繁雜工作分離的較爲徹底了。這樣,我們就在 Handler(Controller) 中專心的編寫我們的業務邏輯吧!

8、Handler 與 HandlerInterceptor 的橋樑——HandlerExecutionChain

前面講到 DispatherServlet 求助 HandlerMapping 進行 url 與次級控制器的映射,但是 DispatherServlet 在將 url 交給特定的 HandlerMapping 之後,HandlerMapping 在進行了一頓猛如虎的操作之後,返回給 DispaterServlet 的卻不是一個可執行的 Handler(Controller), 而是一個 HandlerExecutionChain 對象。那麼 HandlerMapping 究竟爲什麼要返回給這樣的一個對象而不是返回 Handler 對象呢?

其實在看上面圖的時候,你有沒有納悶,HandlerInterceptor 與 Handler 是怎樣聯繫在一起的呢?答案就是 HandlerExecutionChain。它就是若干的 HandlerInterceptor 與 Handler 的組合。那麼是怎麼組合的呢?

這裏就涉及到設計模式中的責任鏈設計模式,HandlerExecutionChain 將 HandlerInterceptor 與 Handler 串成一個執行鏈的形式,首先請求會被第一個 HandlerInterceptor 攔截,如果返回 false, 那麼直接短路請求,如果返回 true, 那麼再交給第二個 HandlerInterceptor 處理,直到所有的 HandlerInterceptor 都檢查通過,請求才到達 Handler(Controller),交由 Handler 正式的處理請求。執行完成之後再逐層的返回。

而 DispatcherServlet 拿到的就是這樣一個串聯好的 HandlerExecutionChain,然後順序的執行請求。

9、解耦的關鍵——ModelAndView

到這裏,請求終於來到了對應的 Handler。我們希望的是 Handler 只處理負責的業務邏輯即可,而一些 url 的跳轉等無需 Handler 負責。那麼 DispatcherServlet 就使用了 ModelAndView 保存我們的數據和想要跳轉的路徑。

我們調用業務邏輯層獲取數據,並將數據封裝到 ModelAndView 中,同時設置 ModelAndView 的 view 邏輯視圖名稱。從 ModelAndView 的名稱可以看出,它保存了 Handler 執行完成之後所需要發送到前端的數據,以及需要跳轉的路徑。這些是 DispatcherServlet 需要用到的。推薦:250 期面試題彙總

10、視圖渲染查找——ViewResolver

這一步是 Spring MVC 將數據的獲取與數據的顯示渲染相分離的關鍵,前端可能採用各種各樣的方式顯示數據,可能是 Jsp, 可能是 Html, 也可能是其他的方式。DispatcherServlet 已經拿到了 ModelAndView,這裏面有執行完成請求後返回的響應結果數據,還有邏輯視圖的路徑,這個時候 DispatcherServlet 就需要根據這個邏輯視圖的路徑去查找誰能把數據進行解析與渲染。

比如說我們使用 FreeMarker 模板引擎渲染數據,那麼這個時候就要找到能夠勝任該工作的那個 View 實現類,那麼問題來了,如何尋找呢?以什麼策略尋找呢?這個就依賴我們的 ViewResolver 了。

通常的尋找策略有以下幾種:

11、數據渲染——View

在根據邏輯視圖名藉助 ViewResolver 查找到對應的 View 實現類之後,DispatcherServlet 就會將 ModelAndView 中的數據交給 View 實現類來進行渲染,待該 View 渲染完成之後,會將渲染完成的數據交給 DispatcherServlet,這時候 DispatcherServlet 將其封裝到 Response 返回給前端顯示。

至此,整個 Spring MVC 的處理流程就算完成了,當然這其中還會有對於國際化的支持,主題的定義與設置等等,但是這些是不常用的,Spring MVC 最主要的處理流程所需要的用到的就是以上這些類。可以看到在此過程中,DispatcherServlet 起到了至關重要的作用,所以說 Spring MVC 的核心就在於 DispatcherServlet。

最後附上一張流程圖作爲以上內容的總結。

SpringMVC 處理流程. jpg

來源:juejin.cn/post/6951343274946723870

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/02on_IylInogdnGpwKyr5g