SpringMVC(17) - HTTP快取支援
參考:https://docs.spring.io/spring/docs/4.3.20.RELEASE/spring-framework-reference/htmlsingle/#mvc-caching
良好的HTTP快取策略可以顯著提高Web應用程式的效能和客戶端的體驗。 'Cache-Control'HTTP響應頭主要負責這一點,以及條件性響應頭,如'Last-Modified'和'ETag'。
'Cache-Control'HTTP響應頭建議私有快取(例如瀏覽器)和公共快取(例如代理)如何快取HTTP響應以供進一步重用。
ETag(entity tag:實體標籤)是由HTTP/1.1相容的Web伺服器返回的HTTP響應頭,用於確定給定URL的內容更改。它可以被認為是Last-Modified頭的更復雜的後繼者。當伺服器返回帶有ETag標頭的表示時,在If-None-Match
1. Cache-Control HTTP頭
Spring Web MVC支援許多用例和方法來為應用程式配置“Cache-Control”頭。雖然RFC 7234第5.2.2節完整地描述了該頭及其可能的指令(參考:https://tools.ietf.org/html/rfc7234#section-5.2.2),但有幾種方法可以解決最常見的情況。
Spring Web MVC在其幾個API中使用配置約定:setCachePeriod(int seconds):
- -1值:不會生成“Cache-Control”響應頭。
- 0值:將使用“Cache-Control:no-store”指令阻止快取。
- n > 0值:將使用'Cache-Control: max-age=n'指令將給定的響應快取n秒。
CacheControl構建器類簡單地描述了可用的“Cache-Control”指令,並使構建自己的HTTP快取策略變得更容易。構建完成後,可以在幾個Spring Web MVC API中接受CacheControl例項作為引數。
// 快取一小時 - "Cache-Control: max-age=3600" CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); // 阻止快取 - "Cache-Control: no-store" CacheControl ccNoStore = CacheControl.noStore(); // 在公共和私有快取中快取十天,公共快取不應該轉換響應 // "Cache-Control: max-age=864000, public, no-transform" CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
2. 靜態資源的HTTP快取支援
應使用適當的“Cache-Control”和條件頭提供靜態資源,以獲得最佳效能。 配置ResourceHttpRequestHandler以提供靜態資源不僅可以通過讀取檔案的元資料本地寫入“Last-Modified”頭,還可以在正確配置的情況下寫入“Cache-Control”頭。
可以在ResourceHttpRequestHandler上設定cachePeriod屬性,也可以使用CacheControl例項,該例項支援更具體的指令:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public-resources/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
}
}
在XML中:
<mvc:resources mapping="/resources/**" location="/public-resources/">
<mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>
3. 控制器中Cache-Control、ETag和Last-Modified響應頭的支援
控制器可以支援'Cache-Control','ETag' 或 'If-Modified-Since'HTTP請求;如果要在響應上設定“Cache-Control”頭,確實建議這樣做。 這涉及計算給定請求的lastModified long 或 Etag值,將其與“If-Modified-Since”請求頭值進行比較,並可能返回狀態程式碼為304(未修改)的響應。
控制器可以使用HttpEntity型別與請求/響應進行互動。 返回ResponseEntity的控制器可以在響應中包含HTTP快取資訊,如下所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
執行此操作不僅會在響應中包含“ETag”和“Cache-Control”頭,而且如果客戶端傳送的條件頭與控制器設定的快取資訊匹配,它還會將響應轉換為具有空響應體的 HTTP 304 Not Modified 響應。
@RequestMapping方法也可能希望支援相同的行為。 這可以通過以下方式實現:
@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {
long lastModified = // 1. application-specific calculation
if (request.checkNotModified(lastModified)) {
// 2. shortcut exit - no further processing necessary
return null;
}
// 3. or otherwise further request processing, actually preparing content
model.addAttribute(...);
return "myViewName";
}
這裡有兩個關鍵元素:呼叫request.checkNotModified(lastModified)並返回null。前者在返回true之前設定適當的響應狀態和響應頭。後者與前者相結合,導致Spring MVC不再對請求進行進一步處理。
請注意,有3種情況:
- request.checkNotModified(lastModified):將lastModified與'If-Modified-Since'或'If-Unmodified-Since'請求頭進行比較
- request.checkNotModified(eTag):將eTag與'If-None-Match'請求頭進行比較
- request.checkNotModified(eTag, lastModified):同時執行這兩項操作,這意味著兩個條件都應該有效
當接收到條件 'GET'/'HEAD' 請求時,checkNotModified 將檢查資源是否未被修改,如果是,則將導致HTTP 304 Not Modified響應。在條件 'POST'/'PUT'/'DELETE' 請求的情況下,checkNotModified將檢查資源是否已被修改,如果已經修改,則將導致HTTP 409 Precondition Failed響應以防止併發修改。
4. 簡單ETag支援
Servlet過濾器ShallowEtagHeaderFilter提供對ETag的支援。它是一個普通的Servlet過濾器,因此可以與任何Web框架結合使用。 ShallowEtagHeaderFilter過濾器建立所謂的簡單ETag(與深ETag相反,稍後將詳細介紹)。過濾器快取呈現的JSP(或其他內容)的內容,生成MD5雜湊,並將其作為ETag返回響應中的頭。客戶端下次傳送對同一資源的請求時,會將該雜湊值用作If-None-Match值。過濾器檢測到此情況,再次呈現檢視,並比較兩個雜湊值。如果它們相等,則返回304。
請注意,此策略可以節省網路頻寬,但不能節省CPU,因為必須為每個請求計算完整響應。控制器級別的其他策略(如上所述)可以節省網路頻寬並避免計算。
此過濾器具有writeWeakETag引數,該引數將過濾器配置為寫入弱ETag,如下所示:W/"02a2d595e6ed9a0b24f027f2b63b134d6",如RFC 7232第2.3節中所定義(https://tools.ietf.org/html/rfc7232#section-2.3)。
在web.xml中配置ShallowEtagHeaderFilter:
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
<!-- Optional parameter that configures the filter to write weak ETags
<init-param>
<param-name>writeWeakETag</param-name>
<param-value>true</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
或者在Servlet 3.0+環境中,
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new ShallowEtagHeaderFilter() };
}
}
有關更多詳細資訊,參考下文。
5. 基於程式碼的Servlet容器初始化
在Servlet 3.0+環境中,可以選擇以程式設計方式配置Servlet容器作為替代方法,也可以與web.xml檔案結合使用。 下面是註冊DispatcherServlet的示例:
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
WebApplicationInitializer是Spring MVC提供的一個介面,可確保檢測到自定義的實現並自動用於初始化任何Servlet 3容器。 名為AbstractDispatcherServletInitializer的WebApplicationInitializer的抽象基類實現通過簡單地重寫方法來指定servlet對映和DispatcherServlet配置的位置,從而更容易註冊DispatcherServlet。
對於使用基於Java的Spring配置的應用程式,建議使用此方法:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
如果使用基於XML的Spring配置,則應直接從AbstractDispatcherServletInitializer擴充套件:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AbstractDispatcherServletInitializer還提供了一種方便的方法來新增Filter例項並使它們自動對映到DispatcherServlet:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每個過濾器都根據其具體型別新增預設名稱,並自動對映到DispatcherServlet。
AbstractDispatcherServletInitializer的isAsyncSupported受保護方法提供了一個單獨的位置來啟用DispatcherServlet上的非同步支援以及對映到它的所有過濾器。 預設情況下,此標誌設定為true。
最後,如果需要進一步自定義DispatcherServlet本身,則可以覆蓋createDispatcherServlet方法。