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 ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.2.2.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: 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
|