1. 概述
在本教程中,我們將學習 HTTP 緩存機制。我們還將探討如何在客户端和 Spring MVC 應用程序之間實現該機制的各種方法。
2. 介紹 HTTP 緩存
當我們通過瀏覽器打開網頁時,通常會從 Web 服務器下載大量的資源:
例如,在這個例子中,瀏覽器需要下載三個資源用於一個 /login 頁面。 瀏覽器通常會為每個網頁發出多個 HTTP 請求。 現在,如果我們頻繁請求這些頁面,會導致大量的網絡流量並延長這些頁面的加載時間。
為了減少網絡負載,HTTP 協議允許瀏覽器緩存一些這些資源。 如果啓用,瀏覽器可以將資源保存到本地緩存中。 這樣,瀏覽器就可以從本地存儲中提供這些頁面,而無需從網絡上請求:
Web 服務器可以通過在響應頭中添加 Cache-Control 頭來指示瀏覽器緩存特定資源。
由於資源已保存在本地副本中, 因此存在從瀏覽器提供過時內容(stale content)的風險。 因此,Web 服務器通常會在 Cache-Control 頭中添加過期時間。
在後續部分,我們將為響應添加此頭,來自 Spring MVC 控制器。 稍後,我們還將看到 Spring API,用於根據過期時間驗證緩存的資源。
3. 控制器響應中的 Cache-Control
3.1. 使用 ResponseEntity
最直接的方法是使用 Spring 提供的 CacheControl builder 類:
@GetMapping("/hello/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate();
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body("Hello " + name);
}這將添加一個 Cache-Control 響應頭:
@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}3.2. 使用HttpServletResponse
通常,控制器需要從處理方法中返回視圖名稱。然而,ResponseEntity 類不允許我們同時返回視圖名稱並處理請求體。
作為替代方案,對於此類控制器,我們可以直接在 HttpServletResponse 中設置 Cache-Control 標頭:
@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform");
return "home";
}這還將向 HTTP 響應添加一個 Cache-Control 標頭,類似於上一部分:
@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/home/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"))
.andExpect(MockMvcResultMatchers.view().name("home"));
}4. 緩存-控制 用於靜態資源
通常,我們的 Spring MVC 應用程序會提供大量的靜態資源,例如 HTML、CSS 和 JS 文件。由於這些文件會消耗大量的網絡帶寬,因此對於瀏覽器緩存它們至關重要。我們再次使用響應頭部的 緩存-控制 機制來實現這一點。
Spring 允許我們通過資源映射來控制緩存行為:
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
.setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate());
}這確保所有 在/resources 下定義的資源 將帶有 Cache-Control 頭響應。
5. 在攔截器中配置 Cache-Control
我們可以使用攔截器在我們的 Spring MVC 應用程序中進行每個請求的預處理和後處理。 這也是一個佔位符,我們可以在此控制應用程序的緩存行為。
現在,我們不再需要實現自定義攔截器,而是將使用 Spring 提供的 WebContentInterceptor:
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate(), "/login/*");
registry.addInterceptor(interceptor);
}在這裏,我們註冊了 WebContentInterceptor,並添加了與上一幾節類似的的 Cache-Control 頭部。 值得注意的是,我們可以為不同的 URL 模式添加不同的 Cache-Control 頭部。
在上面的示例中,對於以 /login 開頭的所有請求,我們將添加以下頭部:
@Test
void whenInterceptor_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/login/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}6. Spring MVC 中緩存驗證
到目前為止,我們已經討論瞭如何在響應中包含 Cache-Control 標頭的方法。這指示客户端或瀏覽器根據配置屬性(如 max-age)基於配置屬性緩存資源。
通常,為每個資源添加緩存過期時間是個好主意。 這樣瀏覽器就可以避免從緩存中提供過期的資源。
雖然瀏覽器應該始終檢查過期時間,但每次重新獲取資源可能並不必要。 如果瀏覽器可以驗證資源是否在服務器端已更改,則可以繼續使用緩存的副本。 並且為此目的,HTTP 提供了兩個響應標頭:
- Etag – 一個 HTTP 響應標頭,用於存儲唯一的哈希值以確定緩存的資源是否在服務器端已更改 – 相應的 If-None-Match 請求標頭必須攜帶上次的 Etag 值
- LastModified – 一個 HTTP 響應標頭,用於存儲資源上次更新的時間單位 – 相應的 If-Unmodified-Since 請求標頭必須攜帶上次修改日期
我們可以使用這些標頭中的任何一個來檢查是否需要重新獲取已過期的資源。 在驗證標頭之後,服務器可以重新發送資源或發送 304 HTTP 代碼以表示沒有更改。 對於後者,瀏覽器可以繼續使用緩存的資源。
LastModified 標頭只能存儲秒級精度的時間間隔。 這在需要更短的過期時間的情況下可能是一個限制。 因此,建議使用 Etag。 由於 Etag 標頭存儲哈希值,因此可以創建更精細的時間間隔,例如納秒。
儘管如此,讓我們看看如何使用 LastModified。
Spring 提供了用於檢查請求是否包含過期標頭的一些實用方法:
@GetMapping(value = "/productInfo/{name}")
public ResponseEntity<String> validate(@PathVariable String name, WebRequest request) {
ZoneId zoneId = ZoneId.of("GMT");
long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45)
.atZone(zoneId).toInstant().toEpochMilli();
if (request.checkNotModified(lastModifiedTimestamp)) {
return ResponseEntity.status(304).build();
}
return ResponseEntity.ok().body("Hello " + name);
}Spring 提供 checkNotModified() 方法,用於檢查資源在上次請求後是否已被修改:
@Test
void whenValidate_thenReturnCacheHeader() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT");
this.mockMvc.perform(MockMvcRequestBuilders.get("/productInfo/baeldung").headers(headers))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(304));
}7. 結論
在本文中,我們通過在 Spring MVC 中使用 Cache-Control 響應頭來學習了 HTTP 緩存。我們可以通過在控制器響應中使用 ResponseEntity 類,或者通過資源映射來處理靜態資源來添加此頭。
我們還可以使用 Spring 攔截器為特定 URL 模式添加此頭。