1. 概述

本文聚焦于使用 Spring Boot 和 Spring MVC 服务静态资源(如 JavaScript 和 CSS 文件)时的缓存策略。我们将深入探讨"完美缓存"的概念——确保文件更新后,旧版本不会错误地从缓存中提供服务。

2. 静态资源缓存配置

要让静态资源可缓存,需要配置对应的资源处理器。以下是一个简单示例,通过设置响应头 Cache-Control: max-age=31536000,使浏览器缓存文件长达一年:

@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**") 
                .addResourceLocations("/js/") 
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }
}

设置如此长的缓存有效期是因为:我们希望客户端持续使用缓存版本直到文件更新,而 365 天是 RFC 2616 规范允许的最大值。

当客户端首次请求 foo.js,会通过网络接收完整文件(本例中 37 字节),状态码为 200 OK,响应包含以下缓存控制头:

Cache-Control: max-age=31536000

这指示浏览器缓存文件一年,响应效果如下:

cache

当客户端再次请求相同文件时,浏览器不会向服务器发起新请求,而是直接从缓存加载文件,避免网络往返,显著提升页面加载速度:

cache-highlighted

⚠️ Chrome 用户注意:测试时需避免使用刷新按钮或 F5(Chrome 会强制刷新缓存),应在地址栏按回车键观察缓存行为。详见 Stack Overflow 讨论

2.1. Spring Boot 中的配置

在 Spring Boot 中,可通过 spring.resources.cache.cachecontrol 命名空间下的属性自定义缓存头。例如,将 max-age 设置为一年:

spring.resources.cache.cachecontrol.max-age=365d

此配置适用于 Spring Boot 提供的所有静态资源。若只需对特定请求子集应用缓存策略,应使用原生 Spring MVC 方式。

max-age 外,还可配置其他 Cache-Control 参数(如 no-storeno-cache),方式类似。

3. 静态资源版本管理

使用缓存虽能加速页面加载,但存在关键陷阱:文件更新后,客户端因未向服务器验证文件有效性,会继续使用缓存中的旧版本。

要使浏览器仅在文件更新时从服务器获取新版本,需满足:

  1. ✅ 通过带版本的 URL 提供文件(如 foo.js/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js
  2. ✅ 更新页面中的文件链接为新 URL
  3. ✅ 文件更新时自动变更 URL 中的版本部分(如 foo.js 更新后 → /js/foo-a3d8d7780349a12d739799e9aa7d2623.js

客户端因页面链接指向新 URL,会在文件更新时重新请求;未更新文件则 URL 不变,客户端继续使用缓存。

Spring 原生支持这些功能,包括自动计算文件哈希并追加到 URL。下面看具体配置。

3.1. 通过带版本 URL 提供资源

需在路径中添加 VersionResourceResolver,使文件通过包含版本字符串的 URL 提供服务:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/js/**")
            .addResourceLocations("/js/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
            .resourceChain(false)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}

此处使用内容版本策略:/js 文件夹中的每个文件将通过基于内容的哈希值(指纹)提供。例如 foo.js 将通过 URL /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js 访问。

配置后,当客户端请求 http://localhost:8080/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js

curl -i http://localhost:8080/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js

服务器响应包含缓存控制头,指示客户端缓存文件一年:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 09 Aug 2016 06:43:26 GMT
Cache-Control: max-age=31536000

3.2. Spring Boot 中的版本管理

在 Spring Boot 中启用相同的内容版本管理,只需配置 spring.resources.chain.strategy.content 命名空间:

spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

与 Java 配置等效,此设置对匹配 /** 路径模式的所有资源启用基于内容的版本管理。

3.3. 更新页面中的资源链接

引入版本前,可用简单 script 标签引入 foo.js

<script type="text/javascript" src="/js/foo.js">

现在需更新为带版本的 URL:

<script type="text/javascript" 
  src="/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js">

手动维护这些长路径很繁琐。Spring 提供了更优方案:使用 ResourceUrlEncodingFilter 和 JSTL 的 url 标签自动重写链接。

web.xml 中注册 ResourceURLEncodingFilter

<filter>
    <filter-name>resourceUrlEncodingFilter</filter-name>
    <filter-class>
        org.springframework.web.servlet.resource.ResourceUrlEncodingFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>resourceUrlEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

在 JSP 页面导入 JSTL 核心标签库:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

使用 url 标签引入 foo.js

<script type="text/javascript" src="<c:url value="/js/foo.js" />">

JSP 渲染时,URL 会被自动重写为包含版本:

<script type="text/javascript" src="/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js">

3.4. 自动更新 URL 版本部分

文件更新时,其版本会重新计算,并通过新 URL 提供服务。无需额外操作,VersionResourceResolver 会自动处理。

4. 修复 CSS 中的资源链接

CSS 文件可能通过 @import 指令引入其他 CSS 文件。例如 myCss.css 引入 another.css

@import "another.css";

这会导致版本化资源出现问题:浏览器请求 another.css,但实际文件通过版本化路径(如 another-9556ab93ae179f87b178cfad96a6ab72.css)提供服务。

为解决此问题,需在资源处理器配置中引入 CssLinkResourceTransformer

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/", "classpath:/other-resources/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
            .resourceChain(false)
            .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"))
            .addTransformer(new CssLinkResourceTransformer());
}

此转换器会修改 myCss.css 的内容,将导入语句替换为:

@import "another-9556ab93ae179f87b178cfad96a6ab72.css";

5. 总结

利用 HTTP 缓存可极大提升网站性能,但避免服务过时资源同时保持缓存有效性是个挑战。本文实现了使用 Spring MVC 服务静态资源时的 HTTP 缓存策略,并在文件更新时自动使缓存失效。

本文源代码可在 GitHub 获取。


原始标题:Cachable Static Assets with Spring MVC