總結前端控制器DispatcherServlet(上)-Servlet規範

把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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章