1. 概述

本文将深入探讨 HTTP 缓存机制,并结合 Spring MVC 展示如何在客户端与服务端之间高效实现缓存策略。重点放在 Cache-Control 响应头的使用,以及资源校验(如 ETag、Last-Modified)的实战技巧。

对于有经验的开发者来说,理解这些机制不仅能提升应用性能,还能避免一些常见的“踩坑”问题,比如缓存不生效、浏览器强制刷新等问题。

2. HTTP 缓存机制简介

当你打开一个网页时,浏览器通常会向服务器发起多个 HTTP 请求来加载资源(如 HTML、CSS、JS、图片等):

http cache

以上图为例,仅一个 /login 页面就需要下载三个资源。如果频繁访问这类页面,不仅增加网络开销,也会拖慢加载速度。

解决方案:HTTP 缓存

HTTP 协议支持浏览器对资源进行本地缓存。一旦资源被缓存,后续请求可以直接从本地读取,无需再次下载:

http cached resources

服务端通过在响应中添加 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-revalidateno-cache
  • 优先使用 ETag 而非 Last-Modified 进行校验

所有示例代码均可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-java-2


原始标题:Cache Headers in Spring MVC