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.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等)進行緩存。