前言
在使用SpringMVC開發Web應用時,除了會在web.xml中配置之前讨論的ContextLoaderListener項以外(相關内容可參考:SpringMVC流程分析(一):從一行配置入手,搞懂web環境下Ioc容器的建構),還需要在配置一個Servlet,而這個Servlet正是本文所讨論的重點——DispatcherServlet.
下圖展示了本系列文章重點分析的元件資訊,其中 DispatcherServlet是本文分析的重點。
Servlet的前世今生
随着Web開發工具的不斷疊代, Servlet可能已經成為一個相對陌生的話題。接下來,将以餐廳點菜為例,帶你快速熟悉Servlet.
當你走進一家餐廳,拿起菜單準備點菜時,總會有一個“服務員”會在你身旁,負責接收你的點菜資訊,然後将你的餐單交給後廚去處理,等後廚烹調完畢後,“服務員”會将菜肴端到你的桌前。
類似的,當你在浏覽器上通路一個網頁或送出表單時,Servlet就像是一個廚師和服務員的組合。它接收你的訂單(請求),然後根據你的選擇(資料),然後告訴廚師要做什麼(執行業務邏輯),最後将做好的食物端到你的桌子上(傳回動态生成的頁面),讓你享受美食.
簡而言之,Servlet的作用在于接收并處理用戶端的HTTP請求,并生成響應結果.
Servlet的體系結構
- Servlet:Servlet體系根接口
- GenericServlet:Servlet抽象實作類
- HttpServlet: 對HTTP協定封裝的Servlet實作類
Servlet作為一個接口,并不能通過new關鍵字來進行執行個體化,是以如果我們期待享受Servlet所提供的服務,則必須依賴于Servlet的實作類HttpServlet,其擴充了GenericServlet類,專門用于處理HTTP請求.
此外,HttpServlet提供了對HTTP請求的封裝和處理方法,使得Servlet可以根據Http的請求方法的不同資訊,轉發至對應的處理方法,如Get請求轉發至doGet()處理,Post請求轉發至doPost()處理.
Servlet的生命周期
事實上,在Servlet對象的建立、初始化、接收請求、銷毀等過程中,都會執行對應的方法,其分别為init()、service()、destory()方法. 而這些方法也被稱為是Servlet的生命周期方法中. 這些方法的具體功能如下:
- init():在Servlet對象被建立後,容器會調用init()方法進行初始化。該方法隻會在Servlet的整個生命周期中被調用一次。可以在init()方法中進行一些初始化操作,例如加載配置檔案、建立資料庫連接配接等。
- service():每當有HTTP請求到達Servlet時,容器會調用service()方法來處理請求,并據請求的類型(GET、POST等)來調用相應的doGet()、doPost()等方法。在這些方法中,可以定義請求處理邏輯、業務操作邏輯等,并生成相應的HTTP響應.
- destroy():在Servlet容器決定銷毀Servlet對象時,會調用destroy()方法。這通常發生在Web應用程式關閉或Servlet容器關閉的時候。在destroy()方法中,你可以進行一些資源釋放和清理的操作,例如關閉資料庫連接配接、釋放資源等.
總的來看,當Servlet對象建立時,會調用init()進行初始化;在接收HTTP請求時,則會調用service()方法處理請求;而在銷毀時會調用destroy()進行資源清理.
Servlet中的請求處理
- 首先,浏覽器和伺服器建立連接配接,生成請求資料包,将請求資料包發送給伺服器;
- 接着,伺服器解析請求資料包,建立request和response對象,将請求資料存入request對象中;
- 随後,伺服器調用Servlet的service()方法,會将request和response作為參數傳進來,并将處理結果寫入到response中;
- 最後,浏覽器解析response中的響應内容,在頁面上生成響應内容.
揭秘DispatcherServlet
在Spring MVC中,DispatcherServlet作為一個前端控制器,其負責接收所有的HTTP請求,同時,将請求分發給相應的處理器(Controller)來處理,并最終傳回響應結果。
DispatcherServlet體系結構
通過上圖不難看出,DispatcherServlet的體系結構可以分為兩條脈絡,一條以Servlet體系結構為基礎,另一條則以Spring為基礎,其中HttpServletBean則是兩者的一個媒介,進而使得Spring中的某個Bean具有Servlet的相關功能.
但我們并不打算詳細分析DispatcherServlet的體系結構,因為這樣容易讓我們陷入到具體細節中,不利于全局的視野建立. 是以,我們将精力集中在DispatcherServlet之上.
在接下來的分析中,請忘記掉上述DispatcherServlet複雜的繼承關系結構,隻需要記住:DispatcherServlet是一個Servlet. 是以,我們關注的内容聚焦在Servlet的初始化以及Http請求的處理上.
DispatcherServlet初始化的秘密
因為DispatcherServlet是一個Servlet,是以在DispatcherServlet對象被建立後,Tomcat容器會調用其中的init()方法來完成Servlet的初始化工作,是以若要分析DispatcherServlet初始化的秘密,就應該研究其DispatcherServlet中init()的方法.
DispatcherServlet中的init()方法的邏輯如上圖所示,不難發現其中核心方法為initWebApplicationContext,即初始化一個web容器. 其相關代碼如下:
(ps:initWebApplicationContext() 的相關邏輯定義在DispatcherServlet的父類FrameWorkServlet之中.)
FrameWorkServlet# initWebApplicationContext()
java複制代碼
public abstract class FrameWorkServlet {
// 成員變量 webApplicationContext 用于儲存web容器
private WebApplicationContext webApplicationContext;
// ....省略其他無關方法
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
//擷取目前環境中的web容器資訊,如果Spring和SpringMVC整合,則不需要重新建立
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
//設定父子容器将Spring容器設為SpringMVC的父容器
cwac.setParent(rootContext);
}
// 容器重新整理,将SpringMVC配置檔案中的屬性加載到容器中
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// ....省略其他無關代碼
if (wac == null) {
// 如果目前上下文中沒有web容器,則重新建立一個
// 主要針對單獨使用SpringMVC時的情況
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// 調用onRefresh方法,進而為容器中初始化一些元件資訊
// DispathcerServlet的核心元件都通過此處完成初始化
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
return wac;
}
// ....省略其他無關方法
}
通過FrameworkServlet的繼承體系我們可以看到,其實作了ApplicationContextAware接口,這使得其具有擷取到Spring中應用上下文(ApplicationContextContext)的能力. 而FrameworkServlet的成員變量webApplicationContext 可以将 Servlet 上下文與 Spring 容器上下文進行關聯,其實際類型 ConfigurableWebApplicationContext.
事實上,如果在web.xml 中配置的 ContextLoaderListener 監聽器初始化的容器上下文容器資訊. 那麼Spring 和 SpringMVC的容器之間便會存在一種父子關系,即 Spring 的容器是父容器,SpringMVC的容器為子容器.
通過上述分析不難看出DispatcherServlet在初始化階段做的最核心工作就是建構容器,然後加載DispatcherServlet的相關配置資訊.
DispatcherServlet請求處理的秘密
在Servlet的請求進行中,每當有Http請求到達Servlet時,都會調用其内部的service()方法來進行處理,并傳回相應的處理結果. 接下來,我們将分析DispatcherServlet的service()相關内容,進而分析清楚DispatcherServlet内部是如何來完成一個請求處理的.
在開始分析之前,我們應該明白當Http到達DispatcherServlet時,其首先會調用DispatcherServlet中的service()方法,但該方法的具體實作是在FrameworkServlet中,而FrameworkServlet的service方法則會通過super.service()繼續向上調用父類HttpServletBean中service()的方法,但HttpServletBean中并未重寫service()方法,是以隻能繼續向上找到其父類HttpServlet中的service()方法,而此時HttpServlet中的service()方法,邏輯如下:
HttpServlet #service()
java複制代碼protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 擷取請求方法
String method = req.getMethod();
// 當請求方式為Get時,調用doGet處理請求
if (method.equals(METHOD_GET)) {
doGet(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
}
// ....省略其他無關代碼
}
不難發現HttpServlet中service()的主要邏輯為:首先,擷取到請求方法的類型,然後,調用doXXX()方法(例如doGet、doPost等)來處理請求,而這些方法的處理邏輯在FrameworkServlet中進行了重新定義,具體如下:
java複制代碼protected final void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 交給DispatcherServlet進行實作
processRequest(request, response);
}
protected final void processRequest(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// ....省略其他無關代碼
// doService邏輯交給子類DispatcherServlet實作
doService(request, response);
// ....省略其他無關代碼
}
是以,最終的所有處理邏輯都DispatcherServlet中的doService進行處理. 此時,請求處理的整個調用過程如下圖所示. 是以如果要分析Http請求在DispatcherServlet的處理過程,隻需要關注方法doService即可.
Service()的具體調用關系如下所示.
通過上圖可以知道,當一個Http請求到達DispatcherServlet後,其會通過内部的service()方法來完成請求的處理,并最終委托于DispatcherServlet的doService方法來進行處理.
(Ps:由于DispatcherServlet複雜的繼承關系,是以service()的調用過程稍顯複雜; 但通過梳理service()的調用鍊我們可以知道,所有Http請求的處理都會委托于DispatcherServlet中的doService.)
在doService中,所有的處理邏輯又會委托于 doDispatch進行處理,其核心代碼如下:
java複制代碼protected void doDispatch(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 省略無關代碼....
// 檢測請求是否為上傳請求,如果是則通過 multipartResolver 将其封裝成 MultipartHttpServletRequest對象
processedRequest = checkMultipart(request);
// 獲得請求對應的 HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
// 如果擷取不到,則根據配置抛出異常或傳回 404 錯誤
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 獲得目前 handler 對應的 HandlerAdapter 對象
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 攔截器前置處理邏輯
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 調用 handler 方法,也就是執行對應的方法,并傳回視圖
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 後置處理 攔截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 處理正常和異常的請求調用結果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
// 省略其他無關代碼....
}
DispatcherServlet中的doDispatch方法是Spring MVC的核心方法,用于處理HTTP請求并進行請求的分發。它是根據請求的URL和請求類型(GET、POST等)來确定要調用哪個控制器(Controller)的方法,并處理控制器方法的傳回結果,最終生成HTTP響應,其大緻處理邏輯如下圖所示:
- 解析請求資訊: 首先,doDispatch方法會解析HTTP請求的URL、請求類型、請求參數等資訊,以确定要調用哪個控制器的哪個方法來處理請求。
- 查找處理器(Handler): 接下來,doDispatch會根據請求的URL查找合适的處理器(通常是Controller對象)和處理器方法。這個過程是通過Spring的HandlerMapping元件來實作的,HandlerMapping會根據URL和配置的映射規則,找到對應的Controller和方法。
- 執行處理器方法: 一旦找到處理器和方法,doDispatch會調用該方法,并将HTTP請求的參數傳遞給方法。Controller方法會執行業務邏輯,并傳回一個包含響應資料的ModelAndView對象。
- 處理傳回結果: 接着,doDispatch會根據Controller方法傳回的ModelAndView對象,來決定如何處理響應結果。如果傳回結果是一個視圖(View),doDispatch會通過ViewResolver将邏輯視圖名稱解析為真實的視圖對象,然後使用視圖對象來渲染生成HTML等内容。
- 發送響應: 最後,doDispatch将生成的響應内容發送給用戶端(通常是浏覽器),完成HTTP響應的過程。
DispatcherServlet中的doDispatch方法可以根據HTTP請求的資訊,找到合适的控制器并調用處理器方法,然後處理傳回結果,最終生成HTTP響應。是以一定程度上,可以将doDispatch認為是Spring MVC的核心,是以後續我們的讨論都将圍繞上圖doDispatch的處理邏輯展開.
總結
本文首先介紹了Servlet的相關内容,并以此為基礎分析了Spring MVC中的核心元件DispatcherServlet,在整個過程中,我們所秉持的理念一直都是忽視掉DispatcherServlet複雜的繼承關系,而将其看做是一個Servlet,進而從Servlet的角度,并對DispatcherServlet的初始化和請求處理進行了詳細的分析.
不過在整個處理過程中涉及到SpringMVC中處理請求的元件還沒有進行分析,或許你對于許多細節存在疑惑,但不要慌,後續文章将對 SpringMVC的其他核心元件進行分析. 而DispatcherServlet 中的doDispatch方法中的處理邏輯将貫徹整個分析過程,這樣有利于加深我們對 SpringMVC 的了解,同時能将DispatcherServlet 元件同其他元件串聯起來,進而更好的了解SpringMVC中對于一個請求的處理.