1. Overview
1.概述
In this tutorial, we’ll learn about HTTP caching. We’ll also look at various ways to implement this mechanism between a client and a Spring MVC application.
在本教程中,我们将学习HTTP缓存的知识。我们还将研究在客户端和Spring MVC应用之间实现这一机制的各种方法。
2. Introducing HTTP Caching
2.引入HTTP缓存
When we open a web page on a browser, it usually downloads a lot of resources from the webserver:
当我们在浏览器上打开一个网页时,它通常会从网络服务器上下载大量的资源。
For instance, in this example, a browser needs to download three resources for one /login page. It’s common for a browser to make multiple HTTP requests for every web page. Now, if we request such pages very frequently, it causes a lot of network traffic and takes longer to serve these pages.
例如,在这个例子中,浏览器需要为一个/login页面下载三个资源。浏览器为每个网页发出多个HTTP请求是很常见的。现在,如果我们非常频繁地请求这样的页面,就会造成大量的网络流量,并且需要更长的时间来提供这些页面。
To reduce network load, the HTTP protocol allows browsers to cache some of these resources. If enabled, browsers can save a copy of a resource in the local cache. As a result, browsers can serve these pages from the local storage instead of requesting it over the network:
为了减少网络负荷,HTTP协议允许浏览器缓存其中的一些资源。如果启用,浏览器可以在本地缓存中保存一份资源的副本。因此,浏览器可以从本地存储中提供这些页面,而不是通过网络请求。
A web server can direct the browser to cache a particular resource by adding a Cache-Control header in the response.
网络服务器可以通过在响应中添加Cache-Control头来指示浏览器对某一特定资源进行缓冲。
Since the resources are cached as a local copy, there is a risk of serving stale content from the browser. Therefore, web servers usually add an expiration time in the Cache-Control header.
由于资源被缓存为本地副本,存在从浏览器提供陈旧内容的风险。因此,网络服务器通常在Cache-Control头中添加一个过期时间。
In the following sections, we’ll add this header in a response from the Spring MVC controller. Later, we’ll also see Spring APIs to validate the cached resources based on the expiration time.
在下面的章节中,我们将在Spring MVC控制器的响应中添加这个头。稍后,我们还将看到Spring的API根据过期时间来验证缓存资源。
3. Cache-Control in Controller’s Response
3.控制器响应中的Cache-Control
3.1. Using ResponseEntity
3.1.使用ResponseEntity
The most straightforward way to do this is to use the CacheControl builder class provided by Spring:
最直接的方法是 使用Spring提供的CacheControlbuilder类。
@GetMapping("/hello/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate();
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body("Hello " + name);
}
This will add a Cache-Control header in the response:
这将在响应中添加一个Cache-Control头。
@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}
3.2. Using HttpServletResponse
3.2.使用HttpServletResponse
Often, the controllers need to return the view name from the handler method. However, the ResponseEntity class doesn’t allow us to return the view name and deal with the request body at the same time.
通常,控制器需要从处理方法中返回视图名称。然而, ResponseEntity类不允许我们同时返回视图名称和处理请求体。
Alternatively, for such controllers we can set the Cache-Control header in the HttpServletResponse directly:
另外,对于这样的控制器,我们可以直接在HttpServletResponse中设置Cache-Control头。
@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform");
return "home";
}
This will also add a Cache-Control header in the HTTP response similar to the last section:
这也将在HTTP响应中添加一个Cache-Control头,与上一节类似。
@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/home/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"))
.andExpect(MockMvcResultMatchers.view().name("home"));
}
4. Cache-Control for Static Resources
4.静态资源的缓存控制
Generally, our Spring MVC application serves a lot of static resources like HTML, CSS and JS files. Since such files consume a lot of network bandwidth, so it’s important for browsers to cache them. We’ll again enable this with the Cache-Control header in the response.
一般来说,我们的Spring MVC应用程序保存了大量的静态资源,如HTML、CSS和JS文件。由于这类文件会消耗大量的网络带宽,所以浏览器对它们进行缓存是非常重要的。我们将再次用响应中的Cache-Control头来启用。
Spring allows us to control this caching behavior in resource mapping:
Spring允许我们在资源映射中控制这种缓存行为。
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
.setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate());
}
This ensures that all resources defined under /resources are returned with a Cache-Control header in the response.
这确保了所有在下定义的资源/resources都在响应中带Cache-Control头返回。
5. Cache-Control in Interceptors
5.拦截器中的缓存控制
We can use interceptors in our Spring MVC application to do some pre- and post-processing for every request. This is another placeholder where we can control the caching behavior of the application.
我们可以在Spring MVC应用程序中使用拦截器来为每个请求做一些前后处理。这是另一个占位符,我们可以控制应用程序的缓存行为。
Now instead of implementing a custom interceptor, we’ll use the WebContentInterceptor provided by Spring:
现在,我们将使用Spring提供的WebContentInterceptor,而不是实现一个自定义拦截器。
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate(), "/login/*");
registry.addInterceptor(interceptor);
}
Here, we registered the WebContentInterceptor and added the Cache-Control header similar to the last few sections. Notably, we can add different Cache-Control headers for different URL patterns.
在这里,我们注册了WebContentInterceptor,并添加了Cache-Control头,与上几节类似。值得注意的是,我们可以为不同的URL模式添加不同的Cache-Control头。
In the above example, for all requests starting with /login, we’ll add this header:
在上面的例子中,对于所有以/login开始的请求,我们将添加这个头。
@Test
void whenInterceptor_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/login/baeldung"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}
6. Cache Validation in Spring MVC
6.Spring MVC中的缓存验证
So far, we’ve discussed various ways of including a Cache-Control header in the response. This indicates the clients or browsers to cache the resources based on configuration properties like max-age.
到目前为止,我们已经讨论了在响应中包含Cache-Control头的各种方法。这表明客户端或浏览器根据配置属性(如max-age)来缓存资源。
It’s generally a good idea to add a cache expiry time with each resource. As a result, browsers can avoid serving expired resources from the cache.
一般来说,在每个资源中添加一个缓存过期时间是个好主意。因此,浏览器可以避免从缓存中提供过期的资源。
Although browsers should always check for expiry, it may not be necessary to re-fetch the resource every time. If a browser can validate that a resource hasn’t changed on the server, it can continue to serve the cached version of it. And for this purpose, HTTP provides us with two response headers:
虽然浏览器应该总是检查是否过期,但可能没有必要每次都重新获取资源。如果浏览器能够验证一个资源在服务器上没有变化,它就可以继续提供它的缓存版本。而为此目的,HTTP为我们提供了两个响应头。
- Etag – an HTTP response header that stores a unique hash value to determine whether a cached resource has changed on the server – a corresponding If-None-Match request header must carry the last Etag value
- LastModified – an HTTP response header that stores a unit of time when the resource was last updated – a corresponding If-Unmodified-Since request header must carry the last modified date
We can use either of these headers to check if an expired resource needs to be re-fetched. After validating the headers, the server can either re-send the resource or send a 304 HTTP code to signify no change. For the latter scenario, browsers can continue to use the cached resource.
我们可以使用这些头信息中的任何一个来检查过期的资源是否需要重新获取。在验证了这些头信息之后,服务器可以重新发送资源或者发送304 HTTP代码来表示没有变化。对于后一种情况,浏览器可以继续使用缓存的资源。
The LastModified header can only store time intervals up to seconds precision. This can be a limitation in cases where a shorter expiry is required. For this reason, it’s recommended to use Etag instead. Since Etag header stores a hash value, it’s possible to create a unique hash up to more finer intervals like nanoseconds.
LastModified标头只能存储精确到秒的时间间隔。在需要更短的到期时间的情况下,这可能是一个限制。出于这个原因,我们建议使用Etag来代替。由于Etagheader存储了一个哈希值,所以可以创建一个独特的哈希值,直到更细的时间间隔,如纳秒级。
That said, let’s check out what it looks like to use LastModified.
也就是说,让我们看看使用LastModified是什么样子的。
Spring provides some utility methods to check if the request contains an expiration header or not:
Spring提供了一些实用的方法来检查请求是否包含过期标头。
@GetMapping(value = "/productInfo/{name}")
public ResponseEntity<String> validate(@PathVariable String name, WebRequest request) {
ZoneId zoneId = ZoneId.of("GMT");
long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45)
.atZone(zoneId).toInstant().toEpochMilli();
if (request.checkNotModified(lastModifiedTimestamp)) {
return ResponseEntity.status(304).build();
}
return ResponseEntity.ok().body("Hello " + name);
}
Spring provides the checkNotModified() method to check if a resource has been modified since the last request:
Spring提供了checkNotModified()方法来检查资源在上次请求后是否被修改。
@Test
void whenValidate_thenReturnCacheHeader() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT");
this.mockMvc.perform(MockMvcRequestBuilders.get("/productInfo/baeldung").headers(headers))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(304));
}
7. Conclusion
7.结语
In this article, we learned about HTTP caching by using the Cache-Control response header in Spring MVC. We can either add the header in the controller’s response using the ResponseEntity class or through resource mapping for static resources.
在这篇文章中,我们通过在Spring MVC中使用Cache-Control响应头了解了HTTP缓存。我们可以使用ResponseEntity类在控制器的响应中添加该头,也可以通过静态资源的资源映射。
We can also add this header for particular URL patterns using Spring interceptors.
我们也可以使用Spring拦截器为特定的URL模式添加这个头。
As always, the code is available over on GitHub.
像往常一样,代码可在GitHub上获得。