Spring MVC 缓存静态资源
1. 概述
本文重点介绍在使用 Spring Boot 和 Spring MVC 服务时缓存静态资源(例如 Javascript 和 CSS 文件)。
我们还将涉及“完美缓存”的概念,主要是确保在更新文件时不会错误地从缓存中提供旧版本。
2. 缓存静态资源
为了使静态资源可缓存,我们需要配置其对应的资源处理程序。
这是一个如何做到这一点的简单示例——在对max-age=31536000的响应中设置Cache-Control标头,这会导致浏览器使用该文件的缓存版本一年:
@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时,他将通过网络收到整个文件(在本例中为 37 个字节),状态码为200 OK。响应将具有以下标头来控制缓存行为:
Cache-Control: max-age=31536000
作为以下响应的结果,这会指示浏览器缓存有效期为一年的文件:
当客户端第二次请求同一个文件时,浏览器不会再向服务器发出请求。相反,它将直接从其缓存中提供文件并避免网络往返,因此页面加载速度会更快:
Chrome 浏览器用户在测试时需要小心,因为如果您通过按屏幕上的刷新按钮或按 F5 键刷新页面,Chrome 将不会使用缓存。您需要在地址栏上按回车来观察缓存行为。更多信息在这里 。
2.1. Spring Boot
要在 Spring Boot 中 自定义Cache-Control 标头,我们可以使用*spring.resources.cache.cachecontrol 属性命名空间下的属性。例如,要将 max-age更改为一年,我们可以在application.properties*中添加以下内容:
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。让我们看看如何配置我们的 Spring 应用程序来为我们完成所有这些工作。
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文件夹中的每个文件都将在一个 URL 下提供,该 URL 具有根据其内容计算的版本。这称为指纹识别。例如,foo.js 现在将在 URL /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js 下提供。
使用此配置,当客户端请求http://localhost:8080/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js 时:
curl -i http://localhost:8080/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js
服务器将响应一个 Cache-Control 标头,告诉客户端浏览器将文件缓存一年:
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. 使用新 URL 更新链接
在我们将版本插入 URL 之前,我们可以使用一个简单的script标签来导入foo.js:
<script type="text/javascript" src="/js/foo.js">
现在我们在带有版本的 URL 下提供相同的文件,我们需要在页面上反映它:
<script type="text/javascript"
src="<em>/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js</em>">
处理所有这些漫长的路径变得乏味。Spring 为这个问题提供了更好的解决方案。我们可以使用ResourceUrlEncodingFilter和 JSTL 的url标签来用版本化的链接重写链接的 URL。
ResourceURLEncodingFilter可以像往常一样在web.xml下注册:
<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核心标签库,才能使用url标签:
<%@ 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文件导入另一个.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 语句替换为以下内容:
@import "another-9556ab93ae179f87b178cfad96a6ab72.css";