@ControllerAdvice is a specialization of @Component which can be used to define methods with @ExceptionHandler, @InitBinder, and @ModelAttribute annotations. Such methods are applied to all @RequestMapping methods across multiple @Controller classes instead of just local @Controller class.
Following example shows how to use @ModelAttribute in @ControllerAdvice class to globally apply model attributes.
Example
We are going to populate model attributes with the page counter and the request URI in the @ControllerAdvice class.
package com.logicbig.example;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import jakarta.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
@ControllerAdvice
public class PageCountersControllerAdvice {
//uri to counter map
private ConcurrentHashMap<String, LongAdder> counterMap =
new ConcurrentHashMap<>();
@ModelAttribute
public void handleRequest(HttpServletRequest request, Model model) {
String requestURI = request.getRequestURI();
//counter increment for each access to a particular uri
counterMap.computeIfAbsent(requestURI, key -> new LongAdder())
.increment();
//populating counter in the model
model.addAttribute("counter", counterMap.get(requestURI).sum());
//populating request URI in the model
model.addAttribute("uri", requestURI);
}
}
The Controllers
@Controller
public class ProductsController {
@RequestMapping("/products/**")
public String handleRequest(){
//todo:prepare page content
return "app-page";
}
}
@Controller
public class ServicesController {
@RequestMapping("/services/**")
public String handleRequest() {
//todo:prepare page content
return "app-page";
}
}
The view
src/main/webapp/WEB-INF/views/app-page.jsp<html>
<body>
<p>Page counter: ${counter}</p>
<p>App content .... at ${uri}</p>
</body>
</html>
Output
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
$ curl -s http://localhost:8080/products/info <html> <body>
<p>Page counter: 1</p> <p>App content .... at /products/info</p>
</body> </html>
$ curl -s http://localhost:8080/services/info <html> <body>
<p>Page counter: 1</p> <p>App content .... at /services/info</p>
</body> </html>
Accessing and refreshing pages multiple times:
As seen above, page counter is maintained and supplied as model attribute for both of our controllers.
Integration Tests
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.annotation.DirtiesContext;
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)
@ContextConfiguration(classes = {AppConfig.class})
@WebAppConfiguration
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ControllerAdviceTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvc;
@BeforeEach
public void setup() {
this.mockMvc = MockMvcTester.from(this.wac);
}
@Test
public void testProductsControllerModelAttributes() {
assertThat(mockMvc.get().uri("/products/info").exchange())
.hasStatusOk()
.hasViewName("app-page")
.model()
.containsKey("counter")
.containsKey("uri")
.containsEntry("uri", "/products/info")
.containsEntry("counter", 1L);
assertThat(mockMvc.get().uri("/products/info").exchange())
.hasStatusOk()
.model()
.containsKey("counter")
.containsEntry("uri", "/products/info")
.containsEntry("counter", 2L);
}
@Test
public void testServicesControllerModelAttributes() {
assertThat(mockMvc.get().uri("/services/info").exchange())
.hasStatusOk()
.hasViewName("app-page")
.model()
.containsKey("counter")
.containsKey("uri")
.containsEntry("uri", "/services/info")
.containsEntry("counter", 1L);
}
@Test
public void testIndependentCounterTracking() {
assertThat(mockMvc.get().uri("/products/info").exchange()).hasStatusOk();
assertThat(mockMvc.get().uri("/products/info").exchange()).hasStatusOk();
assertThat(mockMvc.get().uri("/products/info").exchange())
.hasStatusOk()
.model()
.containsEntry("counter", 3L);
assertThat(mockMvc.get().uri("/services/info").exchange())
.hasStatusOk()
.model()
.containsEntry("counter", 1L)
.containsEntry("uri", "/services/info");
}
}
mvn clean test -Dtest="ControllerAdviceTest" Output$ mvn clean test -Dtest="ControllerAdviceTest" [INFO] Scanning for projects... [INFO] [INFO] ---< com.logicbig.example:controller-advice-model-attribute-example >--- [INFO] Building controller-advice-model-attribute-example 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ controller-advice-model-attribute-example --- [INFO] Deleting D:\example-projects\spring-mvc\controller-advice\controller-advice-model-attribute-example\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ controller-advice-model-attribute-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\controller-advice\controller-advice-model-attribute-example\src\main\resources [INFO] [INFO] --- compiler:3.15.0:compile (default-compile) @ controller-advice-model-attribute-example --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 5 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ controller-advice-model-attribute-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\controller-advice\controller-advice-model-attribute-example\src\test\resources [INFO] [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ controller-advice-model-attribute-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) @ controller-advice-model-attribute-example --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ControllerAdviceTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.182 s -- in com.logicbig.example.ControllerAdviceTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.342 s [INFO] Finished at: 2026-05-25T06:58:36+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 2 ms INFO: Completed initialization in 1 ms INFO: Completed initialization in 2 ms
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.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)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- 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
|