Stage 30: observe handler execution with an interceptor
Learning path / Stage 30 of 37
Follow one familiar product request and observe the framework step introduced in this lesson.
Download the complete Stage 30 project · Open the project guide
Before you start
Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.
This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.
What we change and why
Reuse a tiny product catalog
ProductService returns two Product records. There is no database in this lesson, so the request-handling mechanism is the only changing concern.
Follow the HTML request
Tomcat receives /products and invokes the registered DispatcherServlet. A HandlerMapping identifies ProductController.products. A HandlerAdapter invokes that method. The controller adds products to Model and returns the view name products.
Resolve and render the view
Spring MVC’s view-resolution infrastructure locates the Thymeleaf view. The Thymeleaf template resolver finds templates/products.html, and the template engine renders its rows. Returning products is a view name, not the returned product data.
See framework infrastructure without implementing it
Boot configures the DispatcherServlet and MVC infrastructure because this application has the MVC starter. You write the controller, service and template. Use a debugger breakpoint in ProductController.products, then step through the call stack to see the framework invoking it.
Compare JSON output
Open /api/products or send GET in Postman. @RestController applies response-body semantics. A suitable HttpMessageConverter serializes the returned List<Product> to JSON; it does not select products.html. Both endpoints use the same service. The same annotation does not mean every value uses JSON: converter selection also considers the returned type and negotiated media type.
Wrap Servlet processing
The filter logs before chain.doFilter and again in finally after the downstream chain returns. It also adds a response header before the body is written. Boot registers this Filter bean with the Servlet container. Filters can operate on requests that never reach a mapped controller.
Register an MVC interceptor
Declaring the interceptor as a component alone does not attach it to MVC routes. WebConfig.addInterceptors registers it for the two product paths. preHandle returns true to continue; false would stop this handler execution.
Observe the normal HTML order
For the successful synchronous HTML request: Filter before → preHandle → controller → postHandle → view rendering → afterCompletion → Filter after. postHandle occurs before HTML view rendering; JSON response-body output may already be written when postHandle runs. Async and exception paths require additional care.
Keep security in the proper layer
This interceptor is a tracing example. Use Spring Security for authentication and authorization rather than treating this logging interceptor as a complete security solution.
Run this example
Open a terminal in the extracted folder containing pom.xml. Run one command at a time.
mvn test
mvn spring-boot:runOpen http://localhost:8130/products. Keep the terminal running. Stop with Ctrl+C. If the port is occupied, edit server.port in application.properties before restarting.
Expected result
GET /products:
Filter before: /products
Interceptor: preHandle
Controller: products
Interceptor: postHandle
Interceptor: afterCompletion
Filter after: /products
Response headers: X-Lesson-Filter=visited; X-Lesson-Handler=productsObject identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.
Understand each complete file
Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version><relativePath/></parent>
<groupId>com.edupoly</groupId><artifactId>spring-stage-30-interceptor</artifactId><version>1.0.0</version>
<properties><java.version>21</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
src/main/java/com/edupoly/lesson/Product.java
package com.edupoly.lesson;
public record Product(int id, String name, int price) { }
src/main/java/com/edupoly/lesson/ProductService.java
package com.edupoly.lesson;
import java.util.List;
import org.springframework.stereotype.Service;
@Service public class ProductService {
public List<Product> findAll() { return List.of(new Product(1, "Notebook", 80), new Product(2, "Pen", 20)); }
}
src/main/java/com/edupoly/lesson/ProductController.java
package com.edupoly.lesson;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller public class ProductController {
private final ProductService service;
public ProductController(ProductService service) { this.service = service; }
@GetMapping("/products") public String products(Model model) {
System.out.println("Controller: products");
model.addAttribute("products", service.findAll()); return "products";
}
}
src/main/java/com/edupoly/lesson/ProductApi.java
package com.edupoly.lesson;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController public class ProductApi {
private final ProductService service;
public ProductApi(ProductService service) { this.service = service; }
@GetMapping("/api/products") public List<Product> products() { return service.findAll(); }
}
src/main/java/com/edupoly/lesson/RequestLogFilter.java
package com.edupoly.lesson;
import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
@Component public class RequestLogFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
var http = (HttpServletRequest) request;
var output = (HttpServletResponse) response;
System.out.println("Filter before: " + http.getRequestURI());
output.setHeader("X-Lesson-Filter", "visited");
try { chain.doFilter(request, response); }
finally { System.out.println("Filter after: " + http.getRequestURI()); }
}
}
src/main/java/com/edupoly/lesson/ProductInterceptor.java
package com.edupoly.lesson;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component public class ProductInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("Interceptor: preHandle"); response.setHeader("X-Lesson-Handler", "products"); return true;
}
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("Interceptor: postHandle");
}
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
System.out.println("Interceptor: afterCompletion");
}
}
src/main/java/com/edupoly/lesson/WebConfig.java
package com.edupoly.lesson;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration public class WebConfig implements WebMvcConfigurer {
private final ProductInterceptor interceptor;
public WebConfig(ProductInterceptor interceptor) { this.interceptor = interceptor; }
@Override public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/products", "/api/products");
}
}
src/main/resources/templates/products.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><title>Products</title></head>
<body><h1>Products</h1><table><thead><tr><th>ID</th><th>Name</th><th>Price</th></tr></thead>
<tbody><tr th:each="product : ${products}"><td th:text="${product.id}"></td><td th:text="${product.name}"></td><td th:text="${product.price}"></td></tr></tbody></table></body></html>
src/main/java/com/edupoly/lesson/Application.java
package com.edupoly.lesson;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}
src/main/resources/application.properties
server.port=8130
spring.application.name=spring-stage-30-interceptor
src/test/java/com/edupoly/lesson/LessonTest.java
package com.edupoly.lesson;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
class LessonTest {
@Test void realHttpRequestRendersProducts() throws Exception {
try (var context = org.springframework.boot.SpringApplication.run(Application.class, "--server.port=0")) {
int port = context.getEnvironment().getProperty("local.server.port", Integer.class);
var client = java.net.http.HttpClient.newHttpClient();
String base = "http://127.0.0.1:" + port;
var response = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/products")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode()); assertTrue(response.body().contains("Notebook"));
assertFalse(response.body().contains("th:each"));
assertTrue(response.headers().firstValue("Content-Type").orElse("").contains("text/html"));
var api = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/api/products")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
assertEquals(200, api.statusCode()); assertTrue(api.body().contains("\"Notebook\""));
assertTrue(api.headers().firstValue("Content-Type").orElse("").contains("application/json"));
assertEquals("visited", response.headers().firstValue("X-Lesson-Filter").orElse(""));
assertEquals("products", response.headers().firstValue("X-Lesson-Handler").orElse(""));
var missing = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/missing")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
assertEquals(404, missing.statusCode());
}
}
}
Try a failure, then correct it
Remove the addInterceptors registration. The component still exists but its callbacks no longer run for the routes. Restore registration.
Words you can now explain
| Word | Meaning in this example |
|---|---|
| Interceptor | Participates in Spring MVC handler processing. |
| preHandle / postHandle / afterCompletion | Callbacks before handling, after handling and after completion. |
Practice assignment
Add one product and verify both the rendered table and, where present, JSON output. Explain the part of the request flow changed in this stage.
Concepts used in this stage
Use the right column for a quick reminder, or open a concept above for its full explanation.