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
这指示浏览器缓存文件一年,响应效果如下:
当客户端再次请求相同文件时,浏览器不会向服务器发起新请求,而是直接从缓存加载文件,避免网络往返,显著提升页面加载速度:
⚠️ 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-store
或 no-cache
),方式类似。
3. 静态资源版本管理
使用缓存虽能加速页面加载,但存在关键陷阱:文件更新后,客户端因未向服务器验证文件有效性,会继续使用缓存中的旧版本。
要使浏览器仅在文件更新时从服务器获取新版本,需满足:
- ✅ 通过带版本的 URL 提供文件(如
foo.js
→/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js
) - ✅ 更新页面中的文件链接为新 URL
- ✅ 文件更新时自动变更 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 获取。