第四章 Controller介面控制器詳解(1)

五柳-先生發表於2016-03-21


4.1、Controller簡介

Controller控制器,是MVC中的部分C,為什麼是部分呢?因為此處的控制器主要負責功能處理部分:

1、收集、驗證請求引數並繫結到命令物件;

2、將命令物件交給業務物件,由業務物件處理並返回模型資料;

3、返回ModelAndView(Model部分是業務物件返回的模型資料,檢視部分為邏輯檢視名)。

 

還記得DispatcherServlet嗎?主要負責整體的控制流程的排程部分:

1、負責將請求委託給控制器進行處理;

2、根據控制器返回的邏輯檢視名選擇具體的檢視進行渲染(並把模型資料傳入)。

 

因此MVC中完整的C(包含控制邏輯+功能處理)由(DispatcherServlet + Controller)組成。

 

因此此處的控制器是Web MVC中部分,也可以稱為頁面控制器、動作、處理器。

 

Spring Web MVC支援多種型別的控制器,比如實現Controller介面,從Spring2.5開始支援註解方式的控制器(如@Controller、@RequestMapping、@RequestParam、@ModelAttribute等),我們也可以自己實現相應的控制器(只需要定義相應的HandlerMapping和HandlerAdapter即可)。

 

因為考慮到還有部分公司使用繼承Controller介面實現方式,因此我們也學習一下,雖然已經不推薦使用了。

 

對於註解方式的控制器,後邊會詳細講,在此我們先學習Spring2.5以前的Controller介面實現方式。

 

首先我們將專案springmvc-chapter2複製一份改為專案springmvc-chapter4,本章示例將放置在springmvc-chapter4中。

大家需要將專案springmvc-chapter4/ .settings/ org.eclipse.wst.common.component下的chapter2改為chapter4,否則上下文還是“springmvc-chapter2”。以後的每一個章節都需要這麼做。

4.2、Controller介面

package org.springframework.web.servlet.mvc;
public interface Controller {
       ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

 

這是控制器介面,此處只有一個方法handleRequest,用於進行請求的功能處理,處理完請求後返回ModelAndView(Model模型資料部分 和 View檢視部分)。

 

還記得第二章的HelloWorld嗎?我們的HelloWorldController實現Controller介面,Spring預設提供了一些Controller介面的實現以方便我們使用,具體繼承體系如圖4-1:

 

圖4-1

4.3、WebContentGenerator

用於提供如瀏覽器快取控制、是否必須有session開啟、支援的請求方法型別(GET、POST等)等,該類主要有如下屬性:

 

Set<String>   supportedMethods:設定支援的請求方法型別,預設支援“GET”、“POST”、“HEAD”,如果我們想支援“PUT”,則可以加入該集合“PUT”。

boolean requireSession = false:是否當前請求必須有session,如果此屬性為true,但當前請求沒有開啟session將丟擲HttpSessionRequiredException異常;

 

boolean useExpiresHeader = true:是否使用HTTP1.0協議過期響應頭:如果true則會在響應頭新增:“Expires:”;需要配合cacheSeconds使用;

 

boolean useCacheControlHeader = true:是否使用HTTP1.1協議的快取控制響應頭,如果true則會在響應頭新增;需要配合cacheSeconds使用;

 

boolean useCacheControlNoStore = true:是否使用HTTP 1.1協議的快取控制響應頭,如果true則會在響應頭新增;需要配合cacheSeconds使用;

 

private int cacheSeconds = -1:快取過期時間,正數表示需要快取,負數表示不做任何事情(也就是說保留上次的快取設定),

      1、cacheSeconds =0時,則將設定如下響應頭資料:

        Pragma:no-cache             // HTTP 1.0的不快取響應頭

        Expires:1L                  // useExpiresHeader=true時,HTTP 1.0

        Cache-Control :no-cache      // useCacheControlHeader=true時,HTTP 1.1

        Cache-Control :no-store       // useCacheControlNoStore=true時,該設定是防止Firefox快取

 

      2、cacheSeconds>0時,則將設定如下響應頭資料:

        Expires:System.currentTimeMillis() + cacheSeconds * 1000L    // useExpiresHeader=true時,HTTP 1.0

        Cache-Control :max-age=cacheSeconds                    // useCacheControlHeader=true時,HTTP 1.1

 

      3、cacheSeconds<0時,則什麼都不設定,即保留上次的快取設定。

 

 

此處簡單說一下以上響應頭的作用,快取控制已超出本書內容:

HTTP1.0快取控制響應頭

  Pragma:no-cache:表示防止客戶端快取,需要強制從伺服器獲取最新的資料;

  Expires:HTTP1.0響應頭,本地副本快取過期時間,如果客戶端發現快取檔案沒有過期則不傳送請求,HTTP的日期時間必須是格林威治時間(GMT), 如“Expires:Wed, 14 Mar 2012 09:38:32 GMT”;

 

HTTP1.1快取控制響應頭

  Cache-Control :no-cache       強制客戶端每次請求獲取伺服器的最新版本,不經過本地快取的副本驗證;

  Cache-Control :no-store       強制客戶端不儲存請求的副本,該設定是防止Firefox快取

  Cache-Control:max-age=[秒]    客戶端副本快取的最長時間,類似於HTTP1.0的Expires,只是此處是基於請求的相對時間間隔來計算,而非絕對時間。

 

 

還有相關快取控制機制如Last-Modified(最後修改時間驗證,客戶端的上一次請求時間 在 伺服器的最後修改時間 之後,說明伺服器資料沒有發生變化 返回304狀態碼)、ETag(沒有變化時不重新下載資料,返回304)。

 

該抽象類預設被AbstractController和WebContentInterceptor繼承。

4.4、AbstractController

該抽象類實現了Controller,並繼承了WebContentGenerator(具有該類的特性,具體請看4.3),該類有如下屬性:

 

boolean synchronizeOnSession = false:表示該控制器是否在執行時同步session,從而保證該會話的使用者序列訪問該控制器。

 

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
              //委託給WebContentGenerator進行快取控制
              checkAndPrepare(request, response, this instanceof LastModified);
              //當前會話是否應序列化訪問.
              if (this.synchronizeOnSession) {
                     HttpSession session = request.getSession(false);
                     if (session != null) {
                            Object mutex = WebUtils.getSessionMutex(session);
                            synchronized (mutex) {
                                   return handleRequestInternal(request, response);
                            }
                     }
              }
              return handleRequestInternal(request, response);
}

 

可以看出AbstractController實現了一些特殊功能,如繼承了WebContentGenerator快取控制功能,並提供了可選的會話的序列化訪問功能。而且提供了handleRequestInternal方法,因此我們應該在具體的控制器類中實現handleRequestInternal方法,而不再是handleRequest。

 

 

AbstractController使用方法:

首先讓我們使用AbstractController來重寫第二章的HelloWorldController:

 

public class HelloWorldController extends AbstractController {
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		//1、收集引數
		//2、繫結引數到命令物件
		//3、呼叫業務物件
		//4、選擇下一個頁面
		ModelAndView mv = new ModelAndView();
		//新增模型資料 可以是任意的POJO物件
		mv.addObject("message", "Hello World!");
		//設定邏輯檢視名,檢視解析器會根據該名字解析到具體的檢視頁面
		mv.setViewName("hello");
		return mv;
	}
}

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/hello" class="cn.javass.chapter4.web.controller.HelloWorldController"/>

 

從如上程式碼我們可以看出:

1、繼承AbstractController

2、實現handleRequestInternal方法即可。

 

直接通過response寫響應

如果我們想直接在控制器通過response寫出響應呢,以下程式碼幫我們闡述:

 

public class HelloWorldWithoutReturnModelAndViewController extends AbstractController {
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {

		resp.getWriter().write("Hello World!!");		
		//如果想直接在該處理器/控制器寫響應 可以通過返回null告訴DispatcherServlet自己已經寫出響應了,不需要它進行檢視解析
		return null;
	}
}

 

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/helloWithoutReturnModelAndView" class="cn.javass.chapter4.web.controller.HelloWorldWithoutReturnModelAndViewController"/>

 

從如上程式碼可以看出如果想直接在控制器寫出響應,只需要通過response寫出,並返回null即可。

 

強制請求方法型別:

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/helloWithPOST" class="cn.javass.chapter4.web.controller.HelloWorldController">
        <property name="supportedMethods" value="POST"></property>
</bean>

 

 以上配置表示只支援POST請求,如果是GET請求客戶端將收到“HTTP Status 405 - Request method 'GET' not supported”。

 

比如註冊/登入可能只允許POST請求。

 

當前請求的session前置條件檢查,如果當前請求無session將丟擲HttpSessionRequiredException異常:

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/helloRequireSession"
class="cn.javass.chapter4.web.controller.HelloWorldController">
        <property name="requireSession" value="true"/>
</bean>

 

在進入該控制器時,一定要有session存在,否則丟擲HttpSessionRequiredException異常。

 

Session同步:

即同一會話只能序列訪問該控制器。

 

客戶端端快取控制:

1、快取5秒,cacheSeconds=5

 

package cn.javass.chapter4.web.controller;
//省略import
public class HelloWorldCacheController extends AbstractController {
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		
		//點選後再次請求當前頁面
		resp.getWriter().write("<a href=''>this</a>");
		return null;
	}
}

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/helloCache" 
class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
<property name="cacheSeconds" value="5"/>
</bean>

 

如上配置表示告訴瀏覽器快取5秒鐘:

 

開啟chrome瀏覽器除錯工具:

 

伺服器返回的響應頭如下所示:

 

新增了“Expires:Wed, 14 Mar 2012 09:38:32 GMT” 和“Cache-Control:max-age=5” 表示允許客戶端快取5秒,當你點“this”連結時,會發現如下:

 

而且伺服器也沒有收到請求,當過了5秒後,你再點“this”連結會發現又重新請求伺服器下載新資料。

 

注:下面提到一些關於快取控制的一些特殊情況:

    1、對於一般的頁面跳轉(如超連結點選跳轉、通過js呼叫window.open開啟新頁面都是會使用瀏覽器快取的,在未過期情況下會直接使用瀏覽器快取的副本,在未過期情況下一次請求也不傳送);

    2、對於重新整理頁面(如按F5鍵重新整理),會再次傳送一次請求到伺服器的;

 

2、不快取,cacheSeconds=0

 

<!— 在chapter4-servlet.xml配置處理器 -->
<bean name="/helloNoCache"
class="cn.javass.chapter4.web.controller.HelloWorldCacheController">
<property name="cacheSeconds" value="0"/>
</bean>

 

以上配置會要求瀏覽器每次都去請求伺服器下載最新的資料:

 

 

3、cacheSeconds<0,將不新增任何資料

響應頭什麼快取控制資訊也不加。

 

4、Last-Modified快取機制

(1、在客戶端第一次輸入url時,伺服器端會返回內容和狀態碼200表示請求成功並返回了內容;同時會新增一個“Last-Modified”的響應頭表示此檔案在伺服器上的最後更新時間,如“Last-Modified:Wed, 14 Mar 2012 10:22:42 GMT”表示最後更新時間為(2012-03-14 10:22);

(2、客戶端第二次請求此URL時,客戶端會向伺服器傳送請求頭 “If-Modified-Since”,詢問伺服器該時間之後當前請求內容是否有被修改過,如“If-Modified-Since: Wed, 14 Mar 2012 10:22:42 GMT”,如果伺服器端的內容沒有變化,則自動返回 HTTP 304狀態碼(只要響應頭,內容為空,這樣就節省了網路頻寬)。

 

客戶端強制快取過期:

(1、可以按ctrl+F5強制重新整理(會新增請求頭 HTTP1.0 Pragma:no-cache和 HTTP1.1 Cache-Control:no-cache、If-Modified-Since請求頭被刪除)表示強制獲取伺服器內容,不快取。

(2、在請求的url後邊加上時間戳來重新獲取內容,加上時間戳後瀏覽器就認為不是同一份內容:

http://sishuok.com/?2343243243 和 http://sishuok.com/?34334344 是兩次不同的請求。

 

Spring也提供了Last-Modified機制的支援,只需要實現LastModified介面,如下所示:

 

package cn.javass.chapter4.web.controller;
public class HelloWorldLastModifiedCacheController extends AbstractController implements LastModified {
	private long lastModified;
	protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		//點選後再次請求當前頁面
		resp.getWriter().write("<a href=''>this</a>");
		return null;
	}
	public long getLastModified(HttpServletRequest request) {
		if(lastModified == 0L) {
			//TODO 此處更新的條件:如果內容有更新,應該重新返回內容最新修改的時間戳
			lastModified = System.currentTimeMillis();
		}
		return lastModified;
	}	
}

 

<!— 在chapter4-servlet.xml配置處理器 -->   
<bean name="/helloLastModified" 
class="cn.javass.chapter4.web.controller.HelloWorldLastModifiedCacheController"/>

 

HelloWorldLastModifiedCacheController只需要實現LastModified介面的getLastModified方法,保證當內容發生改變時返回最新的修改時間即可。

 

分析:

(1、傳送請求到伺服器,如(http://localhost:9080/springmvc-chapter4/helloLastModified),則伺服器返回的響應為:




(2、再次按F5重新整理客戶端,返回狀態碼304表示伺服器沒有更新過:

 

(3、重啟伺服器,再次重新整理,會看到200狀態碼(因為伺服器的lastModified時間變了)。

 

Spring判斷是否過期,通過如下程式碼,即請求的“If-Modified-Since” 大於等於當前的getLastModified方法的時間戳,則認為沒有修改:

this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));

 

5、ETag(實體標記)快取機制

(1:瀏覽器第一次請求,伺服器在響應時給請求URL標記,並在HTTP響應頭中將其傳送到客戶端,類似伺服器端返回的格式:“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”

(2:瀏覽器第二次請求,客戶端的查詢更新格式是這樣的:“If-None-Match:"0f8b0c86fe2c0c7a67791e53d660208e3"”,如果ETag沒改變,表示內容沒有發生改變,則返回狀態304。

 

 

Spring也提供了對ETag的支援,具體需要在web.xml中配置如下程式碼:

 

<filter>
   <filter-name>etagFilter</filter-name>
   <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>etagFilter</filter-name>
   <servlet-name>chapter4</servlet-name>
</filter-mapping>

 

此過濾器只過濾到我們DispatcherServlet的請求。

 

分析:

1):傳送請求到伺服器:“http://localhost:9080/springmvc-chapter4/hello”,伺服器返回的響應頭中新增了(ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"):

 

2):瀏覽器再次傳送請求到伺服器(按F5重新整理),請求頭中新增了“If-None-Match:

"0f8b0c86fe2c0c7a67791e53d660208e3"”,響應返回304程式碼,表示伺服器沒有修改,並且響應頭再次新增了“ETag:"0f8b0c86fe2c0c7a67791e53d660208e3"”(每次都需要計算):

 

那伺服器端是如何計算ETag的呢?

 

protected String generateETagHeaderValue(byte[] bytes) {
              StringBuilder builder = new StringBuilder("\"0");
              DigestUtils.appendMd5DigestAsHex(bytes, builder);
              builder.append('"');
              return builder.toString();
}

 

bytes是response要寫回到客戶端的響應體(即響應的內容資料),是通過MD5演算法計算的內容的摘要資訊。也就是說如果伺服器內容不發生改變,則ETag每次都是一樣的,即伺服器端的內容沒有發生改變。

 

此處只列舉了部分快取控制,詳細介紹超出了本書的範圍,強烈推薦: http://www.mnot.net/cache_docs/(中文版http://www.chedong.com/tech/cache_docs.html) 詳細瞭解HTTP快取控制及為什麼要快取。

 

快取的目的是減少相應延遲 和 減少網路頻寬消耗,比如css、js、圖片這類靜態資源應該進行快取。

實際專案一般使用反向代理伺服器(如nginx、apache等)進行快取。

 



私塾線上學習網
原創內容(http://sishuok.com

原創內容,轉載請註明私塾線上【http://sishuok.com/forum/blogPost/list/0/5234.html

相關文章