Spring MVC 静态资源的缓存

1. 概述

本文重点介绍在使用Spring MVC提供静态资源(如Javascript和CSS文件)时的缓存。同时介绍下一 当文件更新时,如何从缓存中替换旧版本,不会从缓存中错误地提供,即所谓的“perfect cache”。

2. 静态资源缓存

为了使静态资源可缓存,我们需要配置其相应的资源处理程序。下面是一个简单的示例:将响应的Cache-Control 头信息设置为max-age = 31536000,这会导致浏览器使用该文件的缓存版本一年:

1
2
3
4
5
6
7
8
9
10

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

我们拥有如此长的缓存有效期的原因是我们希望客户端在文件更新之前使用文件的缓存版本,根据RFC的Cache-Control,我们可以使用365天的最大值标题

因此,当客户端第一次请求foo.js时,他将通过网络接收整个文件,状态代码为200 OK。响应将具有以下头信息来控制缓存行为:

1
Cache-Control: max-age=31536000

这个头信息将告诉浏览器缓存文件的过期持续时间为一年:

缓存头信息

当客户端第二次请求相同的文件时,浏览器不会向服务器发出另一个请求。相反,它将直接从其缓存中提供文件并避免网络往返,因此页面加载速度会更快:

来自缓存

Chrome浏览器用户在测试时需要小心,因为如果您通过按屏幕上的刷新按钮或按F5键刷新页面,Chrome将不会使用缓存,您需要在地址栏中敲回车才行。这里有一个关于这个问题的讨论,貌似确实是有一些问题,不知道是不是 chrome 故意为之。

3. 版本化静态资源

使用缓存来提供静态资源会使页面加载速度非常快,但它有一个重要的问题:更新文件时,客户端将无法获取该文件的最新版本,因为如果该文件是最新的并且只是从浏览器缓存中提供文件,它不会检查服务器。

当文件更新的,如果想要浏览器能及时地从服务器重新获取最新文件,可以采取如下的方式:

  • 提供具有版本号的静态资源文件,例如,foo.js 修改为 /js/foo-6944c7e3a9bd20cc30fdc085cae46f2.js
  • 采用一个新的URL链接。
  • 每当文件更新时,都会更新URL的版本部分。例如,当更新 foo.js 时,它的版本信息更新,链接变为:/js/foo-a3d8d7780349a12d739799e9aa7d2623.js 下。

这样客户端将在更新时从服务器请求该文件,因为该页面将具有指向其他URL的链接,因此浏览器无法使用缓存。如果文件未更新,其版本(连带URL)将不会更改,客户端将继续使用该文件的缓存。

通常,我们需要手动完成所有这些操作,但Spring支持这些开箱即用,包括计算每个文件的哈希并将它们附加到URL。让我们看看我们如何配置Spring应用程序来为我们完成所有这些工作。

3.1 给 URL 添加版本支持

我们需要在路径中添加VersionResourceResolver,以便为其下的文件提供更新的版本字符串:

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

这里我们使用内容版本策略。/ js 文件夹中的每个文件都将在具有根据其内容计算的版本的URL,这称为指纹识别。例如,foo.js 现在将有版本信息 URL /js/foo/6944c7e3a9bd20cc30fdc085cae46f2.js

使用此配置,当客户端请求 http://localhost:8080/js/46944c7e3a9bd20cc30fdc085cae46f2.js 时:

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

服务器将使用Cache-Control头信息响应,告诉客户端浏览器将文件缓存一年:

1
2
3
4
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 使用新的URL链接

在我们将版本插入URL之前,我们可以使用一个简单的 script 标记来导入foo.js

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

现在我们在带有版本的URL:

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

显然,手动去修改带有版本信息的 URL 是很麻烦的,而且很难提前知道这些版本的 HASH 值。好在 Spring 提供了一个更好的解决方案来解决这个问题,我们可以使用ResourceUrlEncodingFilter和JSTL的url标记来重写带有版本化链接的链接的URL。

首先需要在 web.xml 下注册 ResourceURLEncodingFilter

1
2
3
4
5
6
7
8
9
<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核心标记库:

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

然后,我们可以使用url标签导入foo.js,如下所示:

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

此时JSP页面在渲染时,Spring 会正确重写该文件的URL,将版本信息追加到 URL 中去,如下所示:

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

3.3 更新URL的版本部分

由于我们 URL 的版本信息是根据文件内容进行 HASH 计算得到的,所以每当更新文件时,都会再次计算其版本,并在包含新版本的URL下提供文件。我们不需要为此做任何额外的工作,VersionResourceResolver 为我们处理这个。

4. 修复CSS链接的问题

CSS文件可以使用 @import 指令导入其他CSS文件,例如,myCss.css文件导入 another.css 文件:

1
@import "another.css";

如果我们采用了版本化的资源路由,那么实际上 another.css 文件的路径会被版本化为 another-9556ab93ae179f87b178cfad96a6ab72.css,而不再是 another.css ,这将导致这个 @import 由于找不到文件而失效。

要解决此问题并向正确的路径发出请求,我们需要将 CssLinkResourceTransformer 引入资源处理程序配置,如下所示:

1
2
3
4
5
6
7
8
9
@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());
}

这样,在处理 css 资源请求时,会修改 myCss.css 中 的内容并使用以下内容,并替换 @import 语句中的文件路径:

1
@import "another-9556ab93ae179f87b178cfad96a6ab72.css";

5. 结论

利用HTTP缓存是对网站性能的巨大推动,但在使用缓存时避免提供过时资源可能很麻烦。

在本文中,我们实现了一个很好的策略,即在使用Spring MVC提供静态资源时使用HTTP缓存,并在更新文件时让缓存失效。