# Spring Web MVC中的頁面緩存支持 ——跟我學SpringMVC系列
**
注:本章講的是Spring2的@Deprecated,但還是有必要提一下。跟我學SpringMVC系列。。**
## 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實現接口,Spring默認提供了一些Controller接口的實現以方便我們使用,具體繼承體系如圖4-1:

## 、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;
}
}
```
可以看出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;
}
}
```
```
<!— 在chapter3-servlet.xml配置處理器 -->
<bean name="/hello" class="cn.javass.chapter3.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;
}
}
```
<!— 在chapter3-servlet.xml配置處理器 -->
```
<bean name="/helloWithoutReturnModelAndView" class="cn.javass.chapter3.web.controller.HelloWorldWithoutReturnModelAndViewController"/>
```
從如上代碼可以看出如果想直接在控制器寫出響應,只需要通過response寫出,并返回null即可。
**強制請求方法類型:**
```
<!— 在chapter3-servlet.xml配置處理器 -->
<bean name="/helloWithPOST" class="cn.javass.chapter3.web.controller.HelloWorldController">
<property name="supportedMethods" value="POST"></property>
</bean>
```
以上配置表示只支持POST請求,如果是GET請求客戶端將收到“HTTP Status 405 - Request method 'GET' not supported”。
比如注冊/登錄可能只允許POST請求。
**當前請求的session前置條件檢查,如果當前請求無session將拋出HttpSessionRequiredException異常:**
```
<!— 在chapter3-servlet.xml配置處理器 -->
<bean name="/helloRequireSession"
class="cn.javass.chapter3.web.controller.HelloWorldController">
<property name="requireSession" value="true"/>
</bean>
```
```
在進入該控制器時,一定要有session存在,否則拋出HttpSessionRequiredException異常。
```
**Session同步:**
即同一會話只能串行訪問該控制器。
**緩存控制:**
**1、?緩存5秒,cacheSeconds=5**
```
package cn.javass.chapter3.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;
}
}
```
<!— 在chapter3-servlet.xml配置處理器 -->
```
<bean name="/helloCache"
class="cn.javass.chapter3.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**
<!— 在chapter3-servlet.xml配置處理器 -->
```
<bean name="/helloNoCache"
class="cn.javass.chapter3.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/](http://sishuok.com/)?2343243243?和?[http://sishuok.com/?3433434](http://sishuok.com/?34334343)4?是兩次不同的請求。
**Spring也提供了Last-Modified機制的支持,只需要實現LastModified接口,如下所示:**
package cn.javass.chapter3.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;
}
}
```
<!— 在chapter3-servlet.xml配置處理器 --> ??
```
<bean name="/helloLastModified"
class="cn.javass.chapter3.web.controller.HelloWorldLastModifiedCacheController"/>
```
HelloWorldLastModifiedCacheController只需要實現LastModified接口的getLastModified方法,保證當內容發生改變時返回最新的修改時間即可。
**分析:**
(1、發送請求到服務器,如([http://localhost:9080/springmvc-chapter3/helloLastModified](http://localhost:9080/springmvc-chapter3/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>chapter3</servlet-name>
</filter-mapping>
```
此過濾器只過濾到我們DispatcherServlet的請求。
**分析:**
1):發送請求到服務器:“[http://localhost:9080/springmvc-chapter3/hello](http://localhost:9080/springmvc-chapter3/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.mnot.net/cache_docs/)(中文版[http://www.chedong.com/tech/cache_docs.html](http://www.chedong.com/tech/cache_docs.html))?詳細了解HTTP緩存控制及為什么要緩存。**
**緩存的目的是減少相應延遲?和?減少網絡帶寬消耗,比如css、js、圖片這類靜態資源應該進行緩存。**
**實際項目一般使用反向代理服務器(如nginx、apache等)進行緩存。**
- 跟我學 Spring3
- 【第二章】 IoC 之 2.1 IoC基礎 ——跟我學Spring3
- 【第二章】 IoC 之 2.2 IoC 容器基本原理 ——跟我學Spring3
- 【第二章】 IoC 之 2.3 IoC的配置使用——跟我學Spring3
- 【第三章】 DI 之 3.1 DI的配置使用 ——跟我學spring3
- 【第三章】 DI 之 3.2 循環依賴 ——跟我學spring3
- 【第三章】 DI 之 3.3 更多DI的知識 ——跟我學spring3
- 【第三章】 DI 之 3.4 Bean的作用域 ——跟我學spring3
- 【第四章】 資源 之 4.1 基礎知識 ——跟我學spring3
- 【第四章】 資源 之 4.2 內置Resource實現 ——跟我學spring3
- 【第四章】 資源 之 4.3 訪問Resource ——跟我學spring3
- 【第四章】 資源 之 4.4 Resource通配符路徑 ——跟我學spring3
- 【第五章】Spring表達式語言 之 5.1 概述 5.2 SpEL基礎 ——跟我學spring3
- 【第五章】Spring表達式語言 之 5.3 SpEL語法 ——跟我學spring3
- 【第五章】Spring表達式語言 之 5.4在Bean定義中使用EL—跟我學spring3
- 【第六章】 AOP 之 6.1 AOP基礎 ——跟我學spring3
- 【第六章】 AOP 之 6.2 AOP的HelloWorld ——跟我學spring3
- 【第六章】 AOP 之 6.3 基于Schema的AOP ——跟我學spring3
- 【第六章】 AOP 之 6.4 基于@AspectJ的AOP ——跟我學spring3
- 【第六章】 AOP 之 6.5 AspectJ切入點語法詳解 ——跟我學spring3
- 【第六章】 AOP 之 6.6 通知參數 ——跟我學spring3
- 【第六章】 AOP 之 6.7 通知順序 ——跟我學spring3
- 【第六章】 AOP 之 6.8 切面實例化模型 ——跟我學spring3
- 【第六章】 AOP 之 6.9 代理機制 ——跟我學spring3
- 【第七章】 對JDBC的支持 之 7.1 概述 ——跟我學spring3
- 【第七章】 對JDBC的支持 之 7.2 JDBC模板類 ——跟我學spring3
- 【第七章】 對JDBC的支持 之 7.3 關系數據庫操作對象化 ——跟我學spring3
- 【第七章】 對JDBC的支持 之 7.4 Spring提供的其它幫助 ——跟我學spring3【私塾在線原創】
- 【第七章】 對JDBC的支持 之 7.5 集成Spring JDBC及最佳實踐 ——跟我學spring3
- 【第八章】 對ORM的支持 之 8.1 概述 ——跟我學spring3
- 【第八章】 對ORM的支持 之 8.2 集成Hibernate3 ——跟我學spring3
- 【第八章】 對ORM的支持 之 8.3 集成iBATIS ——跟我學spring3
- 【第八章】 對ORM的支持 之 8.4 集成JPA ——跟我學spring3
- 【第九章】 Spring的事務 之 9.1 數據庫事務概述 ——跟我學spring3
- 【第九章】 Spring的事務 之 9.2 事務管理器 ——跟我學spring3
- 【第九章】 Spring的事務 之 9.3 編程式事務 ——跟我學spring3
- 【第九章】 Spring的事務 之 9.4 聲明式事務 ——跟我學spring3
- 【第十章】集成其它Web框架 之 10.1 概述 ——跟我學spring3
- 【第十章】集成其它Web框架 之 10.2 集成Struts1.x ——跟我學spring3
- 【第十章】集成其它Web框架 之 10.3 集成Struts2.x ——跟我學spring3
- 【第十章】集成其它Web框架 之 10.4 集成JSF ——跟我學spring3
- 【第十一章】 SSH集成開發積分商城 之 11.1 概述 ——跟我學spring3
- 【第十一章】 SSH集成開發積分商城 之 11.2 實現通用層 ——跟我學spring3
- 【第十一章】 SSH集成開發積分商城 之 11.3 實現積分商城層 ——跟我學spring3
- 【第十二章】零配置 之 12.1 概述 ——跟我學spring3
- 【第十二章】零配置 之 12.2 注解實現Bean依賴注入 ——跟我學spring3
- 【第十二章】零配置 之 12.3 注解實現Bean定義 ——跟我學spring3
- 【第十二章】零配置 之 12.4 基于Java類定義Bean配置元數據 ——跟我學spring3
- 【第十二章】零配置 之 12.5 綜合示例-積分商城 ——跟我學spring3
- 【第十三章】 測試 之 13.1 概述 13.2 單元測試 ——跟我學spring3
- 【第十三章】 測試 之 13.3 集成測試 ——跟我學spring3
- 跟我學 Spring MVC
- SpringMVC + spring3.1.1 + hibernate4.1.0 集成及常見問題總結
- Spring Web MVC中的頁面緩存支持 ——跟我學SpringMVC系列
- Spring3 Web MVC下的數據類型轉換(第一篇)——《跟我學Spring3 Web MVC》搶先看
- Spring3 Web MVC下的數據格式化(第二篇)——《跟我學Spring3 Web MVC》搶先看
- 第一章 Web MVC簡介 —— 跟開濤學SpringMVC
- 第二章 Spring MVC入門 —— 跟開濤學SpringMVC
- 第三章 DispatcherServlet詳解 ——跟開濤學SpringMVC
- 第四章 Controller接口控制器詳解(1)——跟著開濤學SpringMVC
- 第四章 Controller接口控制器詳解(2)——跟著開濤學SpringMVC
- 第四章 Controller接口控制器詳解(3)——跟著開濤學SpringMVC
- 第四章 Controller接口控制器詳解 (4)——跟著開濤學SpringMVC
- 第四章 Controller接口控制器詳解(5)——跟著開濤學SpringMVC
- 跟著開濤學SpringMVC 第一章源代碼下載
- 第二章 Spring MVC入門 源代碼下載
- 第四章 Controller接口控制器詳解 源代碼下載
- 第四章 Controller接口控制器詳解(6)——跟著開濤學SpringMVC
- 第四章 Controller接口控制器詳解(7 完)——跟著開濤學SpringMVC
- 第五章 處理器攔截器詳解——跟著開濤學SpringMVC
- 源代碼下載 第五章 處理器攔截器詳解——跟著開濤學SpringMVC
- 注解式控制器運行流程及處理器定義 第六章 注解式控制器詳解——跟著開濤學SpringMVC
- 源代碼下載 第六章 注解式控制器詳解
- SpringMVC3強大的請求映射規則詳解 第六章 注解式控制器詳解——跟著開濤學SpringMVC
- Spring MVC 3.1新特性 生產者、消費者請求限定 —— 第六章 注解式控制器詳解——跟著開濤學SpringMVC
- SpringMVC強大的數據綁定(1)——第六章 注解式控制器詳解——跟著開濤學SpringMVC
- SpringMVC強大的數據綁定(2)——第六章 注解式控制器詳解——跟著開濤學SpringMVC
- SpringMVC數據類型轉換——第七章 注解式控制器的數據驗證、類型轉換及格式化——跟著開濤學SpringMVC
- SpringMVC數據格式化——第七章 注解式控制器的數據驗證、類型轉換及格式化——跟著開濤學SpringMVC
- SpringMVC數據驗證——第七章 注解式控制器的數據驗證、類型轉換及格式化——跟著開濤學SpringMVC