SpringMVC 解析(二)DispatcherServlet


在我的關於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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM