Close

Spring MVC - Cache-Control support

[Last Updated: Sep 19, 2026]

Spring Web MVC provides different ways to configure "Cache-Control" headers for an application. In this tutorial we will demonstrate how to set the "Cache-Control" header in @Controller methods for different scenarios.


Examples

Setting "Cache-Control" with a handler returning a view name

The Controller

package com.logicbig.example;

import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@Controller
public class TheController {

    @RequestMapping(value = "/test1")
    public String handle1 (HttpServletResponse response) {
        String headerValue = CacheControl.maxAge(10, TimeUnit.SECONDS)
                                         .getHeaderValue();

        response.addHeader("Cache-Control", headerValue);
        return "myView";
    }
    .............
}

The CacheControl class is based on the builder pattern, a very convenient way to create 'Cache-Control' headers with different directives. In the code above, we specify that the browser cache should expire after 10 seconds.

/src/main/webapp/WEB-INF/pages/myView.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ page import="java.time.LocalDateTime"%>
<html>
  <body style="margin:20px;">
  JSP page
<p> Page Created:  <%= LocalDateTime.now()%></p>
<a href='test1'>test1</a>
  </body>
</html>

Note that, in the JSP above, we print LocalDateTime.now() so that we can confirm that the page is not fetched from the server more than once within the 10-second cache max age.

Config Class

package com.logicbig.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/")
                .setCachePeriod(30);
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.jsp("/WEB-INF/pages/", ".jsp");
    }
}

Running the application

mvn spring-boot:run

Output:

Clicking the 'test1' link will retrieve the page from the browser's local cache, but clicking the reload button (or F5) will reload the page from the server. This happens if we stay on the same tab. If we open a new tab or a new browser window and enter the URL in the address bar, the page will be retrieved from the cache. This behavior is consistent across current versions of Chrome, Firefox, and Edge (a discussion here).

If we click the 'test1' link several times within the 10-second period, the page's creation timestamp will not change; after 10 seconds it will change. This confirms that the browser is using the local cache within that window instead of making requests to the server.

Setting "Cache-Control" with a handler returning ResponseEntity

In this case, the controller returns a RESTful-style response by using @ResponseBody and returning a ResponseEntity object:

The Controller

package com.logicbig.example;

import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@Controller
public class TheController {
    .............
    @ResponseBody
    @RequestMapping(value = "/test2")
    public ResponseEntity<String> handle2 () {

        CacheControl cacheControl = CacheControl.maxAge(10, TimeUnit.SECONDS);

        String testBody = "<p>Response time: " + LocalDateTime.now() +
                  "</p><a href=''>test2</a>";
        return ResponseEntity.ok()
                             .cacheControl(cacheControl)
                             .body(testBody);
    }
}

Output

Again, clicking the test2 link won't reload the page from the server within the 10-second period.


Setting "Cache-Control" for static resources

We need to use ResourceHandlerRegistration#setCachePeriod(..) when registering the resource location.

@EnableWebMvc
@Configuration
@ComponentScan
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/")
                .setCachePeriod(30);
    }
    .............
}

All static pages under the static folder (on the classpath) will be cached by the browser for 30 seconds.

/src/main/webapp/static/static-test.html

<html>
<body>
This is a static page.
<br/>
<a href="static-test.html">static-test</a>
</body>
</html>

Output

Clicking the static-test link won't reload the page from the server within the 30-second period. To confirm this, we can change the content of the HTML page and click the link again — the page won't update from the server. Note: spring-boot:run runs the application in exploded form, and the contents under webapp are not copied to the 'target' folder. That's why we can make changes under the webapp folder without restarting the server. Even if we restart after modifying static-test.html and access the page again (without F5), the page will still be retrieved from the browser cache instead of a fresh server request.


Default Cache-Control

According to the HTTP caching specification (RFC 9111, which superseded the older RFC 2616/7234 text), if a response has no cache-control header, the browser is still free to cache the content using its own heuristics. In practice it will often be cached for a long time — possibly indefinitely — unless the user performs a hard refresh (F5 or Ctrl+F5) or clears the cache manually in the browser settings.


Integration Test

package com.logicbig.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = WebConfig.class)
public class CacheControlControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvcTester mockMvcTester;

    @BeforeEach
    public void setup() {
        mockMvcTester = MockMvcTester.from(wac);
    }

    // ---------- Scenario 1: handler returning a view name ----------

    @Test
    public void handle1Test() {
        assertThat(mockMvcTester.get().uri("/test1"))
                .hasStatusOk()
                .hasViewName("myView")
                .hasForwardedUrl("/WEB-INF/pages/myView.jsp")
                .headers().hasValue("Cache-Control", "max-age=10");
    }

    // ---------- Scenario 2: handler returning ResponseEntity ----------

    @Test
    public void handle2Test() {
        assertThat(mockMvcTester.get().uri("/test2"))
                .hasStatusOk()
                .headers().hasValue("Cache-Control", "max-age=10");
        assertThat(mockMvcTester.get().uri("/test2"))
                .bodyText().contains("Response time:", "test2");
    }

    // ---------- Scenario 3: static resource caching ----------

    @Test
    public void staticResourceTest() {
        MockMvcTester.MockMvcRequestBuilder getBuilder =
                mockMvcTester.get()
                             .uri("/static/static-test.html");
        assertThat(getBuilder)
                .hasStatusOk()
                .headers().hasValue("Cache-Control", "max-age=30");
        assertThat(getBuilder)
                .bodyText().contains("This is a static page");
    }
}
mvn clean test -Dtest="CacheControlControllerTest.java"

Output

$ mvn clean test -Dtest="CacheControlControllerTest.java"
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.logicbig.example:cache-control-example >-------------
[INFO] Building cache-control-example 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ cache-control-example ---
[INFO] Deleting D:\example-projects\spring-mvc\cache-control-example\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ cache-control-example ---
[INFO] Copying 1 resource from src\main\resources to target\classes
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ cache-control-example ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 3 source files with javac [debug target 25] to target\classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ cache-control-example ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\cache-control-example\src\test\resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ cache-control-example ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ cache-control-example ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.CacheControlControllerTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.347 s -- in com.logicbig.example.CacheControlControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.655 s
[INFO] Finished at: 2026-09-18T23:33:30-05:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 5 ms
INFO: Completed initialization in 4 ms
INFO: Completed initialization in 1 ms

In the next tutorials, we will demonstrate how to set the Last-Modified/If-Modified-Since and ETag headers in Spring MVC.

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 4.2.2.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • spring-test 7.0.6 (Spring TestContext Framework)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
  • assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
  • JDK 25
  • Maven 3.9.11

Spring MVC - Cache-Control support Select All Download
  • cache-control-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TheController.java
          • resources
          • webapp
            • WEB-INF
              • pages
            • static
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join