1. 概述
本文将深入探讨 HTTP 缓存机制,并结合 Spring MVC 展示如何在客户端与服务端之间高效实现缓存策略。重点放在 Cache-Control
响应头的使用,以及资源校验(如 ETag、Last-Modified)的实战技巧。
对于有经验的开发者来说,理解这些机制不仅能提升应用性能,还能避免一些常见的“踩坑”问题,比如缓存不生效、浏览器强制刷新等问题。
2. HTTP 缓存机制简介
当你打开一个网页时,浏览器通常会向服务器发起多个 HTTP 请求来加载资源(如 HTML、CSS、JS、图片等):
以上图为例,仅一个 /login
页面就需要下载三个资源。如果频繁访问这类页面,不仅增加网络开销,也会拖慢加载速度。
✅ 解决方案:HTTP 缓存
HTTP 协议支持浏览器对资源进行本地缓存。一旦资源被缓存,后续请求可以直接从本地读取,无需再次下载:
服务端通过在响应中添加 Cache-Control
头来控制缓存行为。例如:
Cache-Control: max-age=60, must-revalidate, no-transform
⚠️ 注意:缓存虽好,但有风险
缓存的是资源副本,若服务器资源已更新而客户端仍使用旧缓存,就会导致“ stale content(陈旧内容)”问题。因此,合理设置缓存过期时间至关重要。
接下来我们将介绍在 Spring MVC 中设置 Cache-Control
的多种方式。
3. 在控制器响应中设置 Cache-Control
3.1 使用 ResponseEntity
最简单直接的方式是使用 Spring 提供的 CacheControl
构建器,配合 ResponseEntity
返回结果。
@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: max-age=60, must-revalidate, no-transform
✅ 测试验证:
@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"));
}
这种方式简洁明了,适合返回 JSON 或纯文本数据的 REST 接口。
3.2 使用 HttpServletResponse
⚠️ 场景差异:视图渲染
如果控制器方法需要返回视图名(如 Thymeleaf、JSP),就不能使用 ResponseEntity
同时指定视图和操作响应头。
此时可以直接注入 HttpServletResponse
,手动设置头信息:
@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";
}
✅ 测试验证:
@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"));
}
这种方式灵活,适用于传统 MVC 架构下的页面跳转场景。
4. 静态资源的缓存控制
现代 Web 应用通常包含大量静态资源(CSS、JS、图片等),这些文件变动频率低,非常适合长期缓存。
Spring MVC 提供了资源处理器机制,可以统一为静态资源设置 Cache-Control
。
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
.setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate());
}
✅ 效果说明:
- 所有匹配
/resources/**
的请求(如/resources/js/app.js
) - 响应头中自动携带
Cache-Control: max-age=60, must-revalidate, no-transform
💡 建议实践:
对于静态资源,可适当延长 max-age
(如 1 小时以上),并结合内容指纹(如 app.a1b2c3.js
)实现强缓存 + 精准更新。
5. 使用拦截器统一管理缓存
若需对多个 URL 模式应用不同的缓存策略,使用拦截器是一种集中式、可复用的方案。
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);
}
✅ 特性亮点:
- 支持按 URL 模式配置不同缓存策略
- 可集中管理登录页、API 接口等不同场景的缓存行为
- 避免在每个控制器重复写相同逻辑
✅ 测试验证:
@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. 缓存校验:避免重复传输
即使资源已过期,也不一定需要重新下载。浏览器可通过校验机制判断资源是否变更,若未变则使用本地缓存,服务端返回 304 Not Modified
。
HTTP 提供两种主流校验机制:
校验方式 | 响应头 | 请求头 | 说明 |
---|---|---|---|
ETag | ETag |
If-None-Match |
基于资源内容生成哈希,精度高,推荐使用 ✅ |
Last-Modified | Last-Modified |
If-Unmodified-Since |
基于最后修改时间,精度秒级,存在碰撞风险 ❌ |
6.1 使用 Last-Modified 进行校验
Spring 提供 WebRequest#checkNotModified
方法简化校验流程:
@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);
}
✅ 测试验证:
@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));
}
⚠️ 局限性:
- 时间精度仅到秒,高并发下可能误判
- 文件内容未变但时间戳更新也会触发重传
6.2 推荐使用 ETag(未在原文展示但值得补充)
虽然原文未展示 ETag 示例,但在实际项目中更推荐使用:
@GetMapping("/data")
public ResponseEntity<String> getData(WebRequest request) {
String etag = "abc123"; // 可基于内容生成 MD5 或版本号
if (request.checkNotModified(etag)) {
return ResponseEntity.status(304).build();
}
return ResponseEntity.ok().eTag(etag).body("some data");
}
✅ 优势:
- 内容级校验,精准无误
- 支持任意粒度变更检测
- 适合动态内容缓存
7. 总结
本文系统介绍了在 Spring MVC 中实现 HTTP 缓存的几种方式:
- ✅ 使用
ResponseEntity
+CacheControl
:适合 RESTful 接口 - ✅ 通过
HttpServletResponse
手动设头:适用于视图返回 - ✅ 静态资源统一配置:提升前端性能
- ✅ 拦截器集中管理:实现策略复用
- ✅ 缓存校验机制:减少无效传输,提升响应效率
📌 关键建议:
- 静态资源设置较长
max-age
,配合指纹命名 - 动态接口合理设置
must-revalidate
或no-cache
- 优先使用 ETag 而非 Last-Modified 进行校验
所有示例代码均可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-java-2