把Spring MVC工作流實現完走了一遍後,從DispatcherServlet開始看它的具體實現,前端控制器(或叫分發器)作爲整個流程的核心,依靠它完成HTTP請求的攔截和分發處理,翻看了DispatcherServlet的源碼,看到它實現了多級繼承,於是決定寫一篇日誌,從上往下一步一步總結每一層的作用和實現,先從Servlet規範開始。
組件之間的通信
前端控制器DispatcherServlet可以說是保證整個Spring MVC工作流的最核心組件,DispatcherServlet根據web.xml中的配置攔截到用戶的HTTP請求後,首先初始化,加載springmvc.xml配置文件中的配置,例如各種組件,之後,一個Spring MVC工作流開始。
第一步,DispatcherServlet會去遍歷所有的處理器映射器,尋找一個可以處理該HTTP請求的處理器。匹配成功的處理器映射器會向DispatcherServlet返回一個處理器執行鏈,裏面包含了一個處理器。
第二步,DispatcherServlet拿到處理器後,再去遍歷所有的處理器適配器,尋找一個支持自己手中處理器的處理器適配器,因爲只有處理器適配器才知道如何使用這個處理器處理請求。
第三步,DispatcherServlet將控制權交給處理器適配器,處理器適配器將HTTP請求HttpServletRequest和HTTP響應HttpServletResponse傳遞給處理器(或者說控制器Controller),控制器完成請求處理後,返回帶有數據模型和邏輯視圖的ModelAndView對象到處理器適配器,最終由處理器適配器返回給DispatcherServlet。
第四步,DispatcherServlet遍歷所有的視圖解析器ViewResolver,得到一個確定的視圖,並將數據模型傳遞到視圖中,完成數據填充,生成最終返回給用戶的界面,通過HTTP響應HttpServletResponse發送給用戶。
由此可見,前端控制器DipsatcherServlet是十分關鍵的組件,它是一個分發器,幾乎參與了整個Spring MVC工作流的每一步。
DispatcherServlet的多級繼承
作爲Spring MVC的入口,DispatcherServlet其實就是一個Servlet,DispatcherServlet經過多級繼承,最終繼承自符合Servlet規範的HttpServlet。HttpServlet又繼承自GenericServlet,GenericServlet最終實現Servlet接口,它們之間的關係如下圖:
看到如此多級的繼承關係,你可能會有疑問,爲什麼需要這麼多級的繼承和實現?實現這樣多層次的繼承,是爲了每一級完成特定的任務,如初始化,請求分發,請求處理,清理資源等。下面就來看看各個類和接口裏面都有那些實現,以及它們之間的關係。
HTTP和Servlet規範
HTTP請求中包含了用戶信息,URI(統一資源標識符,標識Web上的一種可用資源,例如HTML文檔,圖片和視頻),協議版本protocol,和請求的方法(GET、PUT、POST、DELETE等)。HTTP支持的方法有GET、PUT、POST、DELETE、HEAD、OPTIONS,TRACE。
- GET方法把請求參數放到HTTP請求頭中,發送到服務器,請求服務器進行處理並返回HTTP響應。
- PUT方法用來請求將某個資源放在服務器的某一路徑下。
- POST方法向服務器傳送數據,可以要求服務器對其做處理並返回響應。
- DELETE方法請求刪除服務器某一路徑下的某個資源。
- HEAD方法用於查找服務器中某個對象的頭部信息。
- OPTIONS方法用來查詢Servlet中實現的方法信息。
- TRACE方法用於調試操作。
在請求/響應模型下,客戶端用戶的一個HTTP請求發送到Web容器中後,Web容器就會封裝這個HTTP請求,並創建一個HTTP響應,用來回應客戶端。Web容器將HTTP請求和自己創建的HTTP響應傳遞到Servlet的service()方法中。
Servlet是Servlet規範中定義的一個服務器組件接口,所有用於處理用戶請求的服務器組件都要實現這個接口。Servlet規範包括一下內容:
可以看到,Servlet接口中定義了三個方法:
- init()方法用來初始化Servlet,例如註冊組件。
- service()方法用來處理Web傳遞過來的用戶請求。
- destroy()方法用來釋放Servlet組件資源。
GenericServlet和HttpServlet
由上面的Servlet規範可以看到,GenericServlet是Servlet的一個抽象實現,作用是保存Servlet的配置,爲後面實現的Servlet提供初始化參數和信息。GenericServlet實現了Servlet接口方法init(),並提供了一個無參數的init()方法供子類去重寫,實現了對Servlet規範中Servlet的初始化信息保存,並且讓子類去初始化自己的配置(如註冊組件),來看看GenericServlet中的init(ServletConfig config)代碼:
public void init(Servlet config) throws ServletException{
//保存Servlet的配置信息
this.config = config;
//另一個無參數的init()方法,該方法是GenericServlet類中的一個抽象方法,
//提供給子類重寫,實現初始化。
this.init();
}
Init()方法中將Servlet的配置config保存在自己的成員變量中,最後調用一個無參數的init()方法,子類就是通過重寫這個無參數的init()方法實現特定行爲的初始化。
GenericServlet還實現了Servlet接口的十分重要的方法,service(),該方法負責分發和處理客戶端的請求,GenericServlet提供的是一個通用協議的service()方法,子類必須按照自己的協議,重寫這個service方法(例如下面會看到的HttpServlet類重寫了該方法,實現支持HTTP協議的Servlet)。看看GenericServlet提供的通用協議的service()方法:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
HttpServlet繼承了GenericServlet,得到了Servlet的配置信息,並在此基礎上,提供了一些基本方法的實現,即上面提到的HTTP支持的各種方法:GET、PUT、POST、DELETE、HEAD、OPTIONS和TRACE。HttpServlet會根據用戶HTTP請求中的請求方法,將HTTP請求分發到不同類型的方法中去處理,對於HEAD、OPTIONS、和TRACE這類通用的方法,HttpServlet提供了通用的實現,而對於GET、POST、PUT、和DELETE這裏業務邏輯處理方法,則提供了模板方法,子類需要有選擇地重寫這些方法來完成請求處理。
HttpServlet重寫了父類GenericServlet中的service()方法, 前面看到,在頂級接口Servlet中,service()方法是處理Web傳遞的用戶請求,GenericServlet實現了Servlet接口,但沒有重寫該方法,因爲GenericServlet的任務是保留Servlet的初始化信息,提供空參數的init()方法供子類複寫,完成初始化工作。到了HttpServlet,其任務是實現service()方法,根據HTTP請求中標識的方法,完成HTTP請求的分發,分發到相應的處理方法中(GET、PUT、POST、DELETE等)。來看看HttpServlet中的service()方法:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 從HTTP請求中獲取請求方法
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
// 如果請求的是GET方法,先獲取這個Servlet的最後修改時間
long lastModified = getLastModified(req);
if (lastModified == -1) {
// -1表示Servlet不支持修改最後的修改時間,則直接調用doGet()方法處理這個HTTP請求
doGet(req, resp);
} else {
// 如果支持修改最後的修改時間,則修改爲HTTP請求頭中的最後修改時間
// 先獲取HTTP請求頭中的最後修改時間
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified /1000 * 1000)) {
// 如果HTTP請求頭中的修改時間早於Servlet的修改時間,表明這個Servlet在用戶進行
// 上一次HTTP請求時已被修改,則將最新的修改時間放到響應頭中
maybeSetLastModified(resp, lastModified);
// 調用doGet()方法處理HTTP請求
doGet(req, resp);
} else {
// 如果HTTP請求頭中的修改時間晚於Servlet的修改時間,表明這個Servlet在請求的最後
// 修改時間後都沒有被修改,則返回一個HTTP響應SC_NOT_MODIFIED
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
// 如果請求的時HEAD方法
// HEAD方法無論HTTP請求頭中的修改時間是早於還是晚於Servlet的最後修改時間,都會將Servlet的
// 最後修改時間修改到響應頭中(如果這個Servlet支持最後修改時間的修改操作)
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
// 請求使用POST方法
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
// 請求使用PUT方法
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
// 請求使用DELETE方法
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
// 請求使用OPTIONS方法
doOptions(req, resp);
} else if (method.equals(METHOD_TRACE)) {
// 請求使用TRACE方法
doTrace(req, resp);
} else {
// 如果請求使用了未定義方法,則返回錯誤代碼SC_NOT_IMPLEMENTED響應,並且顯示錯誤信息
String errMsg = Strings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_TMPLEMENTED, errMsg);
}
}
可以看到,service()方法首先獲取HTTP請求中所請求使用的方法method,然後method.equals()判斷並根據不同的請求方法進行HTTP請求的分發。前面說到,對於doHead()、doTrace()和doOptions()這三個通用方法,HttpServlet提供了具體實現,子類可以直接使用,子類需要去重寫的是doGet()、doPut()、doPost()和doDelete(),Spring MVC也是有選擇地重寫了這些方法。
佔位符方法
例如我們來看看HttpServlet中的doGet()方法:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
// 獲取請求頭中的HTTP版本
String protocol = req.getProtocol();
// 直接發送錯誤信息,因爲這個方法是佔位符,需要子類重寫模板方法
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
// 如果HTTP版本是1.1,則讓HTTP迴應發送錯誤信息SC_METHOD_NOT_ALLOWED
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
doGet()方法在獲取了HTTP版本信息後,直接發送錯誤消息,原因是對於子類Servlet,若要使用這些模板方法,必須重寫。doPut()、doPost()和doDelete()這些方法實現都和doGet()方法類似,這裏不貼出來了,它們都是一個佔位符,需要子類有選擇地重寫這些方法,實現自己的HTTP請求服務。
TRACE、OPTIONS和HEAD方法
對於不同的Servlet組件,doTrace()和doOptions()方法基本一樣,TRACE方法返回服務器信息,OPTIONS方法返回Servlet中實現的方法信息,因此,HttpServlet爲其提供了具體的實現,先來看看doTrace()方法:
protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
// 保存字符串的字節長度
int responseLength;
// 連接URI和版本信息字符串
String CRLF = "\r\n";
String responseString = "TRACE " + req.getRequestURI +
" " + req.getProtocol();
// 枚舉(一次獲得一個)對象集合中的元素
Enumeration reqHeaderEnum = req.getHeaderNames();
// 遍歷所有的請求頭信息
while (reqHeaderEnum.hasMoreElements()) {
String headerName = (String)reqHeaderEnum.nextElement();
// 將所有請求頭拼接到字符串中,請求頭信息之間使用回車換行分隔
responseString += CRLF + headerName + ":" + req.getHeader(headerName);
// 回車換行
responseString += CRLF;
// 獲取字符串的字節長度
responseLength = responseString.length();
// 設置響應類型爲message/http
resp.setContentType("message/http");
// 設置響應體的長度
resp.setContentLength(responseLength);
// 輸出字符串信息到響應中
ServletOutputStream out = resp.getOutputStream();
out.print(responseString);
// 關閉響應
out.close();
return;
}
}
doTrace()方法返回了HTTP請求reg中的URI(統一資源標識符)和HTTP版本信息,並且便利了HTTP請求頭中的信息,一起拼接成字符串,最終通過HTTP響應輸出。
doOptions()方法,返回Servlet實現的方法信息,具體來看代碼:
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
// 獲取當前Servlet和它的父類Servlet聲明的所有方法,不包括本類HttpServlet聲明的方法
Method[] methods = getAllDeclareMethods(this.getClass());
// 初始化狀態,除了OPTIONS和TRACE方法(HttpServlet爲其提供了具體實現),假設其他HTTP方法都不支持
boolean ALLOW_GET = false;
boolean ALLOW_PUT = false;
boolean ALLOW_POST = false;
boolean ALLOW_DELETE = false;
boolean ALLOW_HEAD = false;
boolean ALLOW_OPTIONS = true;
boolean ALLOW_TRACE = true;
// 根據子類Servlet是否重寫了HttpServlet的模板方法,判斷這個Servlet是否支持這個HTTP方法
for (int i=0; i<methods.length; i++) {
// 遍歷得到所有聲明的方法
Method m = methods[i];
if (m.getName().equals("doGet")) {
ALLOW_GET = true;
ALLOW_HEAD = true;
}
if (m.getName().equals("doPut"))
ALLOW_PUT = true;
if (m.getName().equals("doPost"))
ALLOW_POST = true;
if (m.getName().equals("doDelete"))
ALLOW_DELETE = true;
}
//把Servlet支持的HTTP方法名拼接成字符串
String allow = null;
if (ALLOW_GET)
if (allow == null) allow = METHOD_GET;
if (ALLOW_PUT)
if (allow == null) allow = METHOD_PUT;
else allow += ", " + METHOD_PUT;
if (ALLOW_POST)
if (allow == null) allow = METHOD_POST;
else allow += ", " + METHOD_POST;
if (ALLOW_DELETE)
if (allow == null) allow = METHOD_DELETE;
else allow += ", " + METHOD_DELETE;
if (ALLOW_HEAD)
if (allow == null) allow = METHOD_HEAD;
else allow += ", " + METHOD_HEAD;
if (ALLOW_TRACE)
if (allow == null) allow = METHOD_TRACE;
else allow += ", " + METHOD_TRACE;
if (ALLOW_OPTIONS)
if (allow == null) allow = METHOD_OPTIONS;
else allow += ", " + METHOD_OPTIONS;
// 將字符串設置到HTTP響應頭中
resp.setHeader("Allow", allow);
}
在doOptions()方法中,將Servlet實現的HTTP方法(也就是重寫了的方法),一一遍歷出來,拼接到字符串中,並設置到HTTP響應裏,返回給用戶。細心的你可能會看到,第21行,對於GET方法的判斷,爲什麼只要判斷GET方法是已實現的,則HEAD方法也確定爲已實現的?來看看doHead()的源碼:
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
doGet(req, resp);
} else {
NoBodyResponse response = new NoBodyResponse(resp);
// 重用doGet()方法
doGet(req, response);
// 設置響應體字節大小
response.setContentLength();
}
}
可以看到,doHead()方法將HTTP響應包裝成了NoBodyResponse類,也就是隻有響應頭而沒有響應體(因爲HEAD方法就是用來返回HTTP響應頭的信息,),然後重用了doGet()方法,既然用到了GET方法,則doGet()一定要被子類重寫了,所以doHead()方法也等於實現了,不需要重寫。這就是爲什麼上面的OPTIONS方法中,只要判斷了GET方法已實現,則HEAD方法也判斷爲已實現的原因。
由上面各個方法的代碼可以看出,在HttpServlet中大部分方法都是一個佔位符(GET、PUT、POST、DELETE等),這些服務的實現需要子類去重寫,Spring MVC就是有選擇地重寫這些方法來實現服務的。
Servlet規範小結
最後總結一下Servlet規範。由前面的圖可以看出,Servlet規範有三部分:Servlet接口、GenericServlet和HttpServlet抽象類。
最頂層的Servlet接口提供了三個接口方法,init()方法負責初始化Servlet對象,service()方法負責響應處理客戶端請求,destory()方法當Servlet對象退出生命週期後,釋放其資源。對於每一個服務器組件,都要去實現這個接口。
GenericServlet是Servlet的一個抽象實現,它的init()方法以賦值給自己成員變量的方式,保存了Servlet的config配置,並且提供了一個無參數的init()方法,這樣在子類初始化時,可以重寫這個無參數的init()方法。GenericServlet實現了Servlet接口的service()方法,這是一個通用協議的service()方法,供它的子類去重寫,例如HttpServlet就把service()方法重寫爲支持HTTP協議。
HttpServlet對HTTP支持的各種方法,GET、PUT、POST、DELETE提供了佔位符,對OPTIONS,TRACE和HEAD提供了具體實現,所以HttpServlet的作用就是提供部分具體實現方法和模板方法,讓子類根據業務邏輯去重寫來處理HTTP請求。
總結完Servlet規範後,下一篇日誌就繼續往下走,看看每一層的作用是什麼,最後如何到達DispatcherServlet。
本文源碼已上傳:
https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project