1. Overview

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.

2. Introducing HTTP Caching

When we open a web page on a browser, it usually downloads a lot of resources from the webserver:

http cache

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.

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 cached resources

A web server can direct the browser to cache a particular resource by adding a Cache-Control header in the response.

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.

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.

3. Cache-Control in Controller’s Response

3.1. Using ResponseEntity

The most straightforward way to do this is to use the CacheControl builder class provided by Spring:

@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:

@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

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.

Alternatively, for such controllers we can set the Cache-Control header in the HttpServletResponse directly:

@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:

@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

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 allows us to control this caching behavior in resource mapping:

@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.

5. Cache-Control in Interceptors

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.

Now instead of implementing a custom interceptor, we’ll use the WebContentInterceptor provided by Spring:

@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.

In the above example, for all requests starting with /login, we’ll add this header:

@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

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.

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:

  1. 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
  2. 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.

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.

That said, let’s check out what it looks like to use LastModified.

Spring provides some utility methods to check if the request contains an expiration header or not:

@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:

@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

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.

We can also add this header for particular URL patterns using Spring interceptors.

As always, the code is available over on GitHub.