Cachable Static Assets with Spring MVC – 使用Spring MVC的可缓存静态资产

最后修改: 2016年 8月 27日

1. Overview

1.概述

This article focuses on caching static assets (such as Javascript and CSS files) when serving them with Spring Boot and Spring MVC.

本文主要讨论在使用Spring Boot和Spring MVC提供静态资产(如Javascript和CSS文件)时的缓存。

We’ll also touch on the concept of “perfect caching”, essentially making sure that – when a file is updated – the old version isn’t incorrectly served from the cache.

我们还将触及 “完美缓存 “的概念,本质上是确保–当一个文件被更新时–旧版本不会被错误地从缓存中提供。

2. Caching Static Assets

2.缓存静态资产

In order to make static assets cacheable, we need to configure its corresponding resource handler.

为了使静态资产可以被缓存,我们需要配置其相应的资源处理程序。

Here’s a simple example of how to do that – setting the Cache-Control header on the response to max-age=31536000 which causes the browser to use the cached version of the file for one year:

这里有一个如何做到这一点的简单例子–将响应中的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));
    }
}

The reason we have such a long time period for cache validity is that we want the client to use the cached version of the file until the file is updated, and 365 days is the maximum we can use according to the RFC for the Cache-Control header.

我们之所以为缓存有效期设置这么长的时间段,是因为我们希望客户端使用文件的缓存版本,直到文件被更新,而根据RFC对Cache-Control头的规定,365天是我们可以使用的最长期限。

And so, when a client requests foo.js for the first time, he will receive the whole file over the network (37 bytes in this case) with a status code of 200 OK. The response will have the following header to control the caching behavior:

因此,当客户端第一次请求foo.js,他将通过网络收到整个文件(本例中为37字节),状态代码为200 OK。响应将有以下头,以控制缓存行为。

Cache-Control: max-age=31536000

This instructs the browser to cache the file with an expiration duration of a year, as a result of the following response:

这就指示浏览器缓存该文件,过期时间为一年,其结果是以下响应。

cache

When the client requests the same file for the second time, the browser will not make another request to the server. Instead, it will directly serve the file from its cache and avoid the network round-trip so the page will load much faster:

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

cache-highlighted

Chrome browser users need to be careful while testing because Chrome will not use the cache if you refresh the page by pressing the refresh button on the screen or by pressing F5 key. You need to press enter on the address bar to observe the caching behavior. More info on that here.

Chrome浏览器的用户在测试时需要注意,因为如果你按屏幕上的刷新按钮或按F5键刷新页面,Chrome就不会使用缓存。您需要在地址栏上按回车键来观察缓存行为。关于这一点的更多信息在这里

2.1. Spring Boot

2.1.春天启动

To customize the Cache-Control headers in Spring Boot, we can use properties under the spring.resources.cache.cachecontrol property namespace. For example, to change the max-age to one year, we can add the following to our application.properties:

要自定义Spring Boot中的Cache-Control头文件,我们可以使用spring.resources.cache.cachecontrol属性命名空间下的属性。例如,为了将最大年龄改为一年,我们可以在我们的application.properties中添加以下内容。

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

This applies to all static resources served by Spring Boot. Therefore, if we just want to apply a caching strategy to a subset of requests, we should use the plain Spring MVC approach.

这适用于由Spring Boot提供的所有静态资源。因此,如果我们只是想将缓存策略应用于一个请求子集,我们应该使用普通的Spring MVC方法。

In addition to max-age, it’s also possible to customize other Cache-Control parameters such as no-store or no-cache with similar configuration properties.

除了max-age,还可以自定义其他Cache-Control参数,如no-store或no-cache,具有类似的配置属性。

3. Versioning Static Assets

3.静态资产的版本管理

Using a cache for serving the static assets makes the page load really fast, but it has an important caveat. When you update the file, the client will not get the most recent version of the file since it does not check with the server if the file is up-to-date and just serves the file from the browser cache.

使用缓存来提供静态资产可以使页面的加载速度非常快,但它有一个重要的注意事项。当你更新文件时,客户端不会得到该文件的最新版本,因为它没有向服务器检查该文件是否是最新的,而只是从浏览器缓存中提供该文件。

Here’s what we need to do to make the browser get the file from the server only when the file is updated:

下面是我们需要做的,以使浏览器只在文件被更新时才从服务器上获取文件。

  • Serve the file under a URL that has a version in it. For example, foo.js should be served under /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js
  • Update links to the file with the new URL
  • Update version part of the URL whenever the file is updated. For example, when foo.js is updated, it should now be served under /js/foo-a3d8d7780349a12d739799e9aa7d2623.js.

The client will request the file from the server when it’s updated because the page will have a link to a different URL, so the browser will not use its cache. If a file is not updated, its version (hence its URL) will not change and the client will keep using the cache for that file.

当文件被更新时,客户端会向服务器请求该文件,因为页面会有一个指向不同URL的链接,所以浏览器不会使用其缓存。如果一个文件没有被更新,它的版本(因此它的URL)将不会改变,客户端将继续使用该文件的缓存。

Normally, we would need to do all of these manually, but Spring supports these out of the box, including calculating the hash for each file and appending them to the URLs. Let’s see how we can configure our Spring application to do all of this for us.

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

3.1. Serve Under a URL With a Version

3.1.在一个带有版本的URL下提供服务

We need to add a VersionResourceResolver to a path in order to serve the files under it with an updated version string in its 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("/**"));
}

Here we use a content version strategy. Each file in the /js folder will be served under a URL that has a version computed from its content. This is called fingerprinting. For example, foo.js will now be served under the URL /js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js.

这里我们使用了一个内容版本策略。/js文件夹中的每个文件都将在一个URL下提供,该URL有一个从其内容计算出来的版本。这就是所谓的指纹识别。例如,foo.js现在将在/js/foo-46944c7e3a9bd20cc30fdc085cae46f2.js.下提供。

With this configuration, when a client makes a request for http://localhost:8080/js/46944c7e3a9bd20cc30fdc085cae46f2.js:

有了这个配置,当客户端对http://localhost:8080/js/46944c7e3a9bd20cc30fdc085cae46f2.js:提出请求时,就会有一个客户端对http://localhost:8080/js/46944c7e3a9bd20cc30fdc085cae46f2.js的请求。

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

The server will respond with a Cache-Control header to tell the client browser to cache the file for a year:

服务器会用一个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

3.2 Spring Boot

To enable the same content-based versioning in Spring Boot, we just have to use a few configurations under the spring.resources.chain.strategy.content property namespace. For example, we can achieve the same result as before by adding the following configurations:

要在Spring Boot中启用同样的基于内容的版本管理,我们只需在spring.resources.chain.strategy.content属性命名空间下使用一些配置。例如,我们可以通过添加以下配置来实现与之前相同的结果。

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

Similar to the Java configuration, this enables the content-based versioning for all assets matching with the /** path pattern.

与Java配置类似,这使得所有与/** 路径模式相匹配的资产都可以进行基于内容的版本管理。

3.3. Update Links With the New URL

3.3.用新的URL更新链接

Before we inserted version into the URL, we could use a simple script tag to import foo.js:

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

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

Now that we serve the same file under a URL with a version, we need to reflect it on the page:

现在我们在一个带有版本的URL下提供同一个文件,我们需要在页面上反映它。

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

It becomes tedious to deal with all those long paths. There’s a better solution that Spring provides for this problem. We can use ResourceUrlEncodingFilter and JSTL’s url tag for rewriting the URLs of the links with versioned ones.

处理所有这些长的路径会变得很乏味。Spring为这个问题提供了一个更好的解决方案。我们可以使用ResourceUrlEncodingFilter和JSTL的url标签,用版本号重写链接的URLs。

ResourceURLEncodingFilter can be registered under web.xml as usual:

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>

JSTL core tag library needs to be imported on our JSP page before we can use the url tag:

在我们使用url标签之前,JSTL核心标签库需要被导入我们的JSP页面。

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

Then, we can use the url tag to import foo.js as follows:

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

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

When this JSP page is rendered, the URL for the file is rewritten correctly to contain the version in it:

当这个JSP页面被渲染时,该文件的URL被正确地重写,以包含其中的版本。

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

3.4. Update Version Part of the URL

3.4.更新URL的版本部分

Whenever a file is updated, its version is computed again and the file is served under a URL that contains the new version. We don’t have to do any additional work for this, VersionResourceResolver handles this for us.

每当一个文件被更新时,它的版本会被再次计算,并且该文件会在一个包含新版本的URL下提供。我们不需要为此做任何额外的工作,VersionResourceResolver为我们处理这个问题。

4. Fix CSS Links

4.修复CSS链接

CSS files can import other CSS files by using @import directives. For example, myCss.css file imports another.css file:

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

@import "another.css";

This would normally cause problems with versioned static assets because the browser will make a request for another.css file, but the file is served under a versioned path such as another-9556ab93ae179f87b178cfad96a6ab72.css.

这通常会给版本化的静态资产带来问题,因为浏览器会对another.css文件进行请求,但该文件是在一个版本化的路径下提供的,例如another-9556ab93ae179f87b178cfad96a6ab72.css。

To fix this problem and to make a request to the correct path, we need to introduce CssLinkResourceTransformer to the resource handler configuration:

为了解决这个问题并向正确的路径发出请求,我们需要在资源处理程序配置中引入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());
}

This modifies the content of myCss.css and swaps the import statement with the following:

这修改了myCss.css的内容,并将导入语句换成了以下内容。

@import "another-9556ab93ae179f87b178cfad96a6ab72.css";

5. Conclusion

5.结论

Taking advantage of HTTP caching is a huge boost to web site performance, but it might be cumbersome to avoid serving stale resources while using caching.

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

In this article, we have implemented a good strategy to use HTTP caching while serving static assets with Spring MVC and busting the cache when the files are updated.

在这篇文章中,我们实现了一个很好的策略,在用Spring MVC服务静态资产时使用HTTP缓存,并在文件更新时破坏缓存。

You can find the source code for this article on GitHub.

你可以在GitHub上找到这篇文章的源代码。