SpringMVC 解析(二)DispatcherServlet

御狐神發表於2022-02-09

在我的關於Tomcat容器介紹的文章中,介紹了Tomcat容器的工作原理,我們知道Tomcat容器在收到請求之後,會把請求處理為Request/Response物件,交給Servlet例項處理。對於Spring的Web應用,得到Tomcat容器的請求之後會交給DispatcherServlet去處理。DispatcherServlet是Spring Web應用處理請求的核心元件,本文會介紹DispatcherServlet的工作原理及關鍵原始碼,本文主要參考了Spring的官方文件

Servlet簡介

什麼是Servlet容器

首先我們需要知道什麼是Web伺服器,Web 伺服器使用 HTTP 協議傳輸資料。在一般情況下,使用者在瀏覽器(客戶端)中鍵入 URL(例如www.baidu.com/static.html ), 並獲取要讀取的網頁。所以伺服器所做的就是向客戶機傳送一個網頁。資訊的交換採用指定請求和響應訊息的格式的 HTTP 協議。

Web伺服器

正如我們看到的,使用者/客戶端只能從伺服器請求靜態網頁。如果使用者希望根據自己的輸入閱讀網頁,那麼這還不夠好。Servlet 容器的基本思想是使用 Java 動態生成伺服器端的網頁。所以 Servlet 容器本質上是與 Servlet 互動的 Web 伺服器的一部分。

Servlet伺服器
Servlet容器的實現往往比較複雜,以典型的Tomcat容器為例,容器內包含聯結器和Container兩大元件,以及類載入器、服務元件、伺服器元件等多種元件。Servlet容器會複雜把請求打包為標準的Request/Response,然後交個Servlet例項進行處理,下圖為Tomcat容器的結構圖。

Tomcat容器

什麼是Servlet?

Servelt容器會負責處理請求並把請求轉為Request/Response物件,但是Servlet容器不會實際處理業務邏輯,而是交給Servlet處理。Servlet 是 javax.servlet 包中定義的介面。它宣告瞭 Servlet 生命週期的三個基本方法:init()、service() 和 destroy()。它們由每個 Servlet Class(在 SDK 中定義或自定義)實現,並由伺服器在特定時機呼叫。

  • init() 方法在 Servlet 生命週期的初始化階段呼叫。它被傳遞一個實現 javax.servlet.ServletConfig 介面的物件,該介面允許 Servlet 從 Web 應用程式訪問初始化引數。
  • service() 方法在初始化後對每個請求進行呼叫。每個請求都在自己的獨立執行緒中提供服務。Web容器為每個請求呼叫 Servlet 的 service() 方法。service() 方法確認請求的型別,並將其分派給適當的方法來處理該請求。
  • destroy() 方法在銷燬 Servlet 物件時呼叫,用來釋放所持有的資源。

從 Servlet 物件的生命週期中,我們可以看到 Servlet 類是由類載入器動態載入到容器中的。每個請求都在自己的執行緒中,Servlet 物件可以同時服務多個執行緒(執行緒不安全的)。當它不再被使用時,會被 JVM 垃圾收集。像任何Java程式一樣,Servlet 在 JVM 中執行。為了處理複雜的 HTTP 請求,Servlet 容器出現了。Servlet 容器負責 Servlet 的建立、執行和銷燬。

Servlet請求處理

那麼Servlet容器是如何處理一個Http請求的呢?在我的另外一篇關於Tomcat容器中介紹了Tomcat容器處理Http請求的詳細流程,此處就簡單介紹一下邏輯:

  1. Web伺服器接收HTTP請求。
  2. Web伺服器將請求轉發到Servlet容器。
  3. 如果對應的Servlet不在容器中,那麼將被動態檢索並載入到容器的JVM中。
  4. 容器呼叫init()方法進行初始化(僅在第一次載入 Servlet 時呼叫一次)。
  5. 容器呼叫Servlet的service()方法來處理HTTP請求,即讀取請求中的資料並構建響應。
  6. Web 伺服器將動態生成的結果返回到瀏覽器/客戶端。

javax包中關於Servlet的介面定義如下所示:


public interface Servlet {
    
    void init(ServletConfig var1) throws ServletException;
    
    ServletConfig getServletConfig();
    
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    
    String getServletInfo();

    void destroy();
}

DispatcherServlet簡介

Spring容器註冊DispatcherServlet

上文中我們知道Servlet主要用於處理Request/Response物件,Spring Web應用中用於處理請求和響應的Servlet實現就是DispatcherServlet。如果學習過Tomcat或者其它Servlet容器相關的知識,我們應該知道一個Web應用容器允許有多個Servlet例項,可以通過路徑或者其它路由規則進行路由。SpringBoot中我們可以通過如下方式向容器中註冊一個DispatcherServlet,註冊完成之後SpringBoot會在Servlet容器中生成對應的元件(如Tomcat的Wrapper容器)。

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

DispatcherServlet處理請求的流程

通過前面的學習我們知道Servlet的主要作用就是處理Request,DispatcherServlet處理請求的流程如下所示:

  1. 把DispatcherServlet對應的WebApplicationContext通過Request.setAttribute和Request進行繫結,這樣每個Reqeust就有自己對應的ApplicationContext。
  2. 把用於國際化的LocalResolve和Request進行繫結。如果程式不需要格式化,則可以忽略這部分邏輯。
  3. 把設定主題的ThemeResolve和Request進行繫結。如果程式不需要主題設定,則可以忽略這部分邏輯。
  4. 如果請求中包含multipart檔案,並且容器中包含MultipartResolver,那麼會使用這個Resolver把請求中的檔案封裝為MultipartHttpServletRequest。MultiPart Resolver是Spring MVC的另外一個功能,我會在後續詳細介紹。
  5. 為這個請求查詢合適的HandlerMapping,合適的HandlerMapping可以從請求中獲取合適的處理器鏈(包含預處理、後處理和Controller等邏輯)。
  6. 如果需要返回View,對View進行渲染,如果不需要那麼直接返回body。

DispatcherServlet包含的元件

WebApplicationContext還提供了統一處理異常的HandlerExceptionResolver,用於處理請求過程中的異常。異常可以有多種處理策略:如處理@ExceptionHandler註解的ResponseStatusExceptionResolver,將異常處理為對應介面的SimpleMappingExceptionResolver等。

DispatcherServlet支援一些和Spring相關的特殊引數,比如包含DispatcherServlet的容器型別等:

欄位名稱 說明資訊
contextClass 包含了這個Servlet的ConfigurableWebApplicationContext,預設情況下是XmlWebApplicationContext
contextConfigLocation 用於指定上下文配置檔案的位置,可以用逗號分割指定多個檔案
namespace WebApplicationContext的名稱空間
throwExceptionIfNoHandlerFound 當存咋Handler找不到的情況時,是否丟擲異常

DispatcherServlet包含的元件與配置

Web請求的處理流程比較複雜,DispatcherServlet會使用Spring容器中的一些特殊的Bean來幫忙處理請求。這些Bean有預設實現,但是使用者也可以使用自定義實現來代替預設實現邏輯。DispatcherServlet包含的關鍵元件及各個元件之間的協作原理如下所示。

DispatcherServlet包含的元件

從上面的DispatcherServlet結構圖可以看出來,DispatcherServlet處理請求的過程中需要多個元件協調工作,接下來我們會一一介紹各個元件的功能及基本原理。

  1. Web配置:用於配置Servlet的屬性,可以通過Bean或者檔案的形式進行配置。
  2. 處理器對映器HandlerMapping:主要功能是根據請求獲取對應的攔截器列表和處理請求的程式。
  3. 處理器介面卡HandlerAdaptor:呼叫請求實際對應處理器的介面卡,封裝了實際呼叫處理器的邏輯。
  4. 實際處理器Controller:實際的業務邏輯都封裝在這裡面,由介面卡反射呼叫。
  5. 各種Resolver:比如異常處理、檢視解析和國際化解析等等。

DispatcherServlet元件配置

在上面的介紹中,我們知道DispatcherServlet會呼叫很多特殊元件來處理請求,DispatcherServlet會在ApplicationContext的Refresh階段去容器中找對應的Bean,如果沒有找到自定義的Bean元件,那麼會使用預設的Bean元件,這些元件在DispatcherServlet.properties檔案中有定義。

在大多數情況下我們並不需要自定義元件,而僅僅需要修改預設元件的引數,比如新增型別轉換服務和自定義校驗邏輯等等,這種情況下最好的辦法是配置WebMVC Config,關於MVC Config的配置會在我的另外一篇文章中進行介紹。

DispatcherServlet註冊配置

我們知道在Tomcat容器中需要配置web.xml檔案,在裡面需要指定Servlet的類和Servlet的對映路徑。在Spring中我們也可以自定一個Servlet,並且指定Servlet處理的URL路徑。我們可以通過如下的方式向Spring Web容器中註冊一個Servlet。

import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }
}

WebApplicationInitializer類是Spring提供的一個用於初始化Servlet容器的介面,Spring會通過ServiceLoader去程式中查詢並載入WebApplicationInitializer並呼叫其onStartup方法。有時候我們可能只需要向容器中註冊一個Servlet,並不需要配置Servlet的其它引數,那麼我們可以通過繼承Spring提供的抽象實現這個功能,Spring針對註解和xml配置檔案有兩個抽象類,其使用方法如下所示:


public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MyWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

如果我們只需要對容器中已經存在的Servlet新增Filter,那麼我們也只需要繼承Spring提供的另外一個抽象類AbstractDispatcherServletInitializer,然後重寫對應的方法。如果你需要按照自己的要求生成DispatcherServlet,你也可以重寫createDispatcherServlet方法。

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }
}

DispatcherServlet與Web應用

關於Tomcat容器和Springboot之間的整合方式,我在其它文章中有詳細介紹,此處再簡單說一下原理:Springboot在啟動的時候會根據包中的類名判斷容器的型別,是Web應用的情況下獲取關於Web容器的配置,然後根據配置生成Tomcat容器。Web應用型別的Spring容器會包含ServletContext和Servlet配置相關的資訊。常見的WebApplicationContext介面定義如下所示:

public interface WebApplicationContext extends ApplicationContext {


    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

    String SCOPE_REQUEST = "request";

    String SCOPE_SESSION = "session";

    String SCOPE_APPLICATION = "application";

    String SERVLET_CONTEXT_BEAN_NAME = "servletContext";

    String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";

    String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";

    @Nullable
    ServletContext getServletContext();

}

DispatcherServlet路徑匹配

URL的劃分

DispatcherServlet從Tomcat中獲取的Request中包含了完整的URL,並且會按照Servlet的對映路徑把路徑劃分為contextPath、servletPath和pathInfo三部分,三者之間的關係如下所示。

Reqeust Path

DispatcherServlet在收到請求後,需要根據路徑去查詢對應的HandlerMapping,這個路徑通常情況下是不包含Servlet容器對映到Servlet容器的路徑,如ContextPath和部分ServletPath。如下圖中的紅色方框部分所示。

Match Path

URL的編碼

在因特網上傳送URL,只能採用ASCII字符集,也就是說URL只能使用英文字母、阿拉伯數字和某些標點符號,不能使用其他文字和符號,這意味著如果URL中有漢字,就必須編碼後使用。國際標準並沒有對編碼格式進行規範,但是我們常用的瀏覽器會採用“%”+UTF8的形式進行編碼。如下的示例中顯示了URL編碼前和編碼後的對比。

URL 編碼

那麼Spring在匹配對應的路徑的時候應該使用編碼前的路徑還是編碼後的路徑呢?由於編碼路徑是瀏覽器或者框架的操作,使用者並不知道這一部分邏輯,對於使用者來說,始終應該只知道解碼後的路徑,所以Spring的路徑匹配始終應該使用解碼後的路徑。

路徑匹配問題

servletPath和pathInfo包含的是解碼之後的路徑資訊,解碼之後的路徑無法再和原始的RequestURL進行路徑匹配,這可能會帶來一些問題:如果路徑中包含編碼解碼的關鍵字元(如:“/”和“;”),會導致解碼出現問題。此外不同的Servlet容器可能使用不同的解碼方式,這也可能帶來一些匹配方面的問題。

Spring預設使用的servletPath是"/" ,這並不會帶來路徑匹配的問題,如果使用者需要自定義servletPath,就需要對這方面多加關注了。

DispatcherServlet攔截器

Spring提供了HandlerInterceptor攔截器介面讓使用者對每次請求進行加工處理(如許可權校驗),所有型別的HandlerMapping都支援HandlerInterceptor。該介面一共包含三個方法:

  1. preHandle:在呼叫處理請求的Handler之前呼叫該方法,返回false表示該方法不合法。
  2. postHandle:在呼叫處理請求的Handler之後呼叫該方法。
  3. afterCompletion:請求處理完成之後呼叫該方法。
public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }

}

DispatcherServlet異常處理

DispatcherServlet處理請求的過程中,如果出現了異常,DispatcherServlet會去異常處理鏈中查詢合適的HandlerExceptionResolver,並且由HandlerExceptionResolver生成對應的View。Spring提供了多種HandlerExceptionResolver,列表及功能如下:

HandlerExceptionResolver 說明
SimpleMappingExceptionResolver 用於把異常類對映為對應的錯誤介面
DefaultHandlerExceptionResolver 把異常對映為對應的HttpCode
ResponseStatusExceptionResolver 用於處理@ResponseStatus對應的HttpCode
ExceptionHandlerExceptionResolver 處理@ExceptionHandler方法異常,可以參考此處

DispatcherServlet會逐個呼叫HandlerExceptionResolver,直到其中一個異常處理器返回View或者呼叫完所有的異常處理器。

其它元件

上面的文章中,我們主要介紹了DispatcherServlet的一些關鍵元件,還有一些檢視元件、國際化元件和主題元件等此處只做簡單介紹。

  1. 檢視解析:檢視解析元件主要用於將響應渲染為頁面,對於Json格式的放回則不進行渲染;
  2. 國際化:如時區切換、請求頭語言、Coooke和Session等都需要國際化元件的引數;
  3. 主題元件:用於切換網頁的主題,使用的比較少;
  4. Multipart:通常用於上傳檔案的解析,該元件會把“multipart/form-data”請求中的資料轉為MultipartHttpServletRequest。
  5. 日誌元件:比如是不是列印請求詳情等。

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd
qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!

相關文章