Spring Boot Interceptors
Spring MVC interceptors allow us to execute common logic around controller execution without placing the same code inside every controller method.
They are useful when the logic depends on the Spring MVC handler that has been selected, such as the controller class, controller method, or annotations present on that method.
1. The Problem Interceptors Solve
Consider a controller with multiple endpoints:
@RestController
@RequestMapping("/api/students")
public class StudentController {
@GetMapping
public String getAllStudents() {
return "All students";
}
@PostMapping
public String createStudent() {
return "Student created";
}
@DeleteMapping("/{id}")
public String deleteStudent(@PathVariable Long id) {
return "Student deleted";
}
}
Suppose every request needs some common operations:
- Log the requested URL
- Check authentication
- Measure execution time
Without interceptors, this logic would be duplicated in every endpoint:
@GetMapping
public String getAllStudents(HttpServletRequest request) {
// Logging
// Authentication
// Timing
return "All students";
}
Problems with this approach:
- Controller methods become cluttered.
- The same code is duplicated across endpoints.
- A small change must be repeated in several places.
- Business logic becomes mixed with infrastructure logic.
An interceptor provides one central place for such cross-cutting MVC logic.
Definition: A Spring MVC interceptor is a component that executes before a handler, after a handler, and after the complete MVC request-processing cycle.
An interceptor does not normally replace the controller. It surrounds its execution.
2. Where an Interceptor Fits in Spring MVC
The DispatcherServlet is the central servlet of Spring MVC.
When a request reaches a Spring MVC application:
- The
DispatcherServletreceives the request. - A
HandlerMappingidentifies the matching handler. - Spring creates a handler execution chain.
- Registered interceptors execute around the handler.
- A
HandlerAdapterinvokes the controller method. - Spring processes the controller's return value.
- The response is sent to the client.
Why is it called HandlerInterceptor?
Spring uses the general term handler because the selected handler is not always a controller method. It may be:
- A controller method
- A static-resource handler
- Another handler type supported by Spring MVC
For normal controller requests, the handler is commonly represented by a HandlerMethod.
HandlerExecutionChain
When Spring finds a handler, it conceptually creates a chain containing the handler and its interceptors:
HandlerExecutionChain = [Interceptors] + Handler
The DispatcherServlet executes this chain. An interceptor can therefore allow or stop the request before the controller runs.
3. The Three Interceptor Lifecycle Methods
A HandlerInterceptor provides three main callback methods:
preHandle()postHandle()afterCompletion()
Their overall flow:
4. Creating a Custom Interceptor
Create a Spring component that implements HandlerInterceptor:
package in.strikes.crudDemo.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class LoggingInterceptor implements HandlerInterceptor {
}
Modern versions of HandlerInterceptor provide default method implementations. Therefore, an interceptor only needs to override the callbacks it actually uses.
A complete basic interceptor looks like this:
package in.strikes.crudDemo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle executed");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
System.out.println("postHandle executed");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
System.out.println("afterCompletion executed");
}
}
For an interceptor that only checks something before the controller, overriding only preHandle() is sufficient:
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
}
5. Understanding the Method Parameters
HttpServletRequest request
Represents the incoming HTTP request.
Commonly used methods include:
request.getMethod();
request.getRequestURI();
request.getQueryString();
request.getHeader("Authorization");
request.getRemoteAddr();
request.getParameter("name");
request.getSession();
Request-specific values can also be stored as attributes:
request.setAttribute("requestStartTime", System.nanoTime());
They can later be retrieved using:
Long startTime = (Long) request.getAttribute("requestStartTime");
HttpServletResponse response
Represents the outgoing HTTP response.
It can be used to set the status, headers, content type, or response content:
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("X-App-Name", "Student Management System");
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Unauthorized\"}");
Once a response is committed, some changes may be too late. This is especially important when working with REST response bodies.
Object handler
Represents the handler selected by Spring.
For a normal controller request, it is commonly a HandlerMethod:
if (handler instanceof HandlerMethod handlerMethod) {
String controllerName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
System.out.println(controllerName);
System.out.println(methodName);
}
Possible output:
StudentController
getAllStudents
Do not cast the handler blindly:
HandlerMethod handlerMethod = (HandlerMethod) handler; // May throw ClassCastException
Some requests may be handled by something other than a controller method, such as a static-resource handler. Use pattern matching with instanceof instead:
if (handler instanceof HandlerMethod handlerMethod) {
// Safe to use handlerMethod
}
This ability to inspect the selected controller and method is one of the major differences between an interceptor and a servlet filter.
6. Understanding preHandle()
Signature:
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception
It executes after Spring has selected the handler but before the handler is invoked.
Why does it return boolean?
The return value tells Spring whether request processing should continue.
Returning true:
return true;
Spring continues to the next interceptor or the controller.
Returning false:
return false;
Spring stops the execution chain, and the controller is not called.
When returning false, the interceptor should usually prepare an appropriate response. Otherwise, the client may receive an empty or confusing response.
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Authentication required\"}");
return false;
Common uses of preHandle()
- Logging request arrival
- Starting an execution timer
- Reading a tenant or organization ID
- Creating a correlation ID
- Adding request attributes
- Validating custom headers
- Checking custom annotations
- Rejecting a request before the controller
Note: For production-grade authentication and authorization, prefer Spring Security instead of building a complete security system through a custom interceptor.
7. Understanding postHandle()
Signature:
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception
It executes after the handler completes successfully but before traditional MVC view rendering.
Why does it receive ModelAndView?
Traditional Spring MVC controllers may return a view. Spring stores the selected view and model data inside a ModelAndView.
An interceptor can add common model information:
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
modelAndView.addObject("appVersion", "1.0.0");
modelAndView.addObject("appName", "Student Management");
}
}
Important REST API note
For methods using @ResponseBody or ResponseEntity, Spring may already have written the response before postHandle() executes. Therefore, postHandle() is not a reliable place to redesign or wrap a REST response body.
Use ResponseBodyAdvice when a common REST response transformation is required:
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
}
Good uses of postHandle()
- Updating a traditional MVC model
- Adding common view attributes
- Logging successful controller completion
- Inspecting the selected handler
- Running logic before view rendering
For REST APIs, afterCompletion() is often more useful for final logging and timing.
8. Understanding afterCompletion()
Signature:
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception
It executes after the complete Spring MVC request-processing cycle finishes.
It is similar to a finally block and is commonly used for:
- Cleanup
- Final request logging
- Execution-time calculation
- Releasing request-related resources
- Recording the final response status
- Inspecting an unhandled exception
Example:
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
System.out.println("Request completed with status: " + response.getStatus());
}
Inspecting exceptions
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
if (ex != null) {
System.out.println("Request failed: " + ex.getMessage());
} else {
System.out.println("Request completed");
}
}
An important limitation is that ex == null does not always prove that no exception occurred.
An exception may have been handled by Spring through:
@ExceptionHandler@ControllerAdvice@RestControllerAdvice- Another exception resolver
The exception may already have been converted into a valid HTTP response.
Therefore, final monitoring should also inspect:
response.getStatus();
9. Registering an Interceptor with WebMvcConfigurer
Creating an interceptor bean is not enough. It must also be added to Spring MVC's interceptor registry.
package in.strikes.crudDemo.config;
import in.strikes.crudDemo.interceptor.LoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
public WebConfig(LoggingInterceptor loggingInterceptor) {
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor);
}
}
Role of each part
The addInterceptor() method returns an InterceptorRegistration, which can be used to configure:
- Included URL patterns
- Excluded URL patterns
- Execution order
10. Logging Request Details
A practical logging interceptor can access both HTTP information and the selected controller method.
package in.strikes.crudDemo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("--- Incoming Request ---");
System.out.println("HTTP Method: " + request.getMethod());
System.out.println("Request URI: " + request.getRequestURI());
if (handler instanceof HandlerMethod handlerMethod) {
System.out.println("Controller: " + handlerMethod.getBeanType().getSimpleName());
System.out.println("Method: " + handlerMethod.getMethod().getName());
} else {
System.out.println("Handler: " + handler);
}
System.out.println("--- End ---");
return true;
}
}
11. Authentication Demonstration with an API Key
The following example demonstrates how an interceptor can stop a request. It is useful for understanding the flow but should not replace Spring Security in a production application.
Requirement
The client sends:
X-API-KEY: student-secret-key
- Correct key: continue to the controller
- Missing or incorrect key: return 401 Unauthorized
- Controller must not execute
Interceptor
package in.strikes.crudDemo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class ApiKeyAuthenticationInterceptor implements HandlerInterceptor {
private static final String VALID_API_KEY = "student-secret-key";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String apiKey = request.getHeader("X-API-KEY");
if (apiKey == null) {
writeUnauthorizedResponse(response, "API key is missing");
return false;
}
if (!VALID_API_KEY.equals(apiKey)) {
writeUnauthorizedResponse(response, "Invalid API key");
return false;
}
return true;
}
private void writeUnauthorizedResponse(HttpServletResponse response, String message)
throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"message\": \"" + message + "\"}");
}
}
12. Authorization Demonstration
Suppose only users with the ADMIN role can access a delete operation.
The client sends:
X-USER-ROLE: ADMIN
A simplified interceptor check could be:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if ("DELETE".equalsIgnoreCase(request.getMethod())) {
String role = request.getHeader("X-USER-ROLE");
if (!"ADMIN".equals(role)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"message\": \"Admin access required\"}");
return false;
}
}
return true;
}
This is a learning example. Real authorization rules should normally be implemented through Spring Security.
401 vs 403
13. Measuring Request Execution Time
The basic requirement is:
Incorrect approach: using an instance field
@Component
public class TimingInterceptor implements HandlerInterceptor {
private long startTime;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
startTime = System.currentTimeMillis();
return true;
}
}
This is unsafe because Spring components are singleton-scoped by default. Multiple requests may use the same interceptor instance concurrently.
Request-specific mutable data should not be stored in a normal interceptor instance field.
Correct approach: use request attributes
Each request has its own HttpServletRequest object. Store the starting time in the request:
request.setAttribute("requestStartTime", System.nanoTime());
Retrieve it in afterCompletion():
Long startTime = (Long) request.getAttribute("requestStartTime");
Complete timing interceptor
package in.strikes.crudDemo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.TimeUnit;
@Component
public class TimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute("requestStartTime", System.nanoTime());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
Long startTime = (Long) request.getAttribute("requestStartTime");
if (startTime == null) {
return;
}
long durationNanos = System.nanoTime() - startTime;
long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
String handlerName = "UnknownHandler";
if (handler instanceof HandlerMethod handlerMethod) {
handlerName = handlerMethod.getBeanType().getSimpleName()
+ "." + handlerMethod.getMethod().getName();
}
System.out.println("Request: " + request.getMethod() + " " + request.getRequestURI());
System.out.println("Handler: " + handlerName);
System.out.println("Status: " + response.getStatus());
System.out.println("Execution time: " + durationMillis + " ms");
}
}
Why use afterCompletion()?
postHandle()runs after successful handler execution.afterCompletion()measures closer to the end of the complete MVC lifecycle, including later processing such as exception resolution and traditional view handling.
Why use System.nanoTime()?
System.currentTimeMillis()represents wall-clock time. The system clock can be adjusted.System.nanoTime()is designed for measuring elapsed time.
14. Applying an Interceptor to URL Patterns
Without path configuration:
registry.addInterceptor(loggingInterceptor);
the interceptor may apply broadly to requests handled by Spring MVC's configured handler mappings.
One URL pattern
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/api/students/**");
}
Multiple URL patterns
registry.addInterceptor(loggingInterceptor)
.addPathPatterns(
"/api/students/**",
"/api/teachers/**",
"/api/courses/**"
);
Common patterns
15. Excluding URL Patterns
Suppose authentication should apply to all API routes except login and registration:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/auth/login",
"/api/auth/register"
);
}
16. Interceptor Execution Order
When multiple interceptors are registered, their order can be configured:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor).order(1);
registry.addInterceptor(authInterceptor).order(2);
registry.addInterceptor(timingInterceptor).order(3);
}
With multiple interceptors, execution follows:
This resembles nested method calls:
Configuring the order
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor).order(1);
registry.addInterceptor(authInterceptor).order(2);
registry.addInterceptor(timingInterceptor).order(3);
}
Lower order values have higher priority and execute earlier during preHandle().
Only interceptors whose preHandle() successfully participated in the chain are eligible for the corresponding completion callbacks.
17. Filters and Interceptors Together
A filter surrounds servlet processing. An interceptor surrounds a Spring MVC handler.
Core difference
- A filter works at the Servlet layer.
- An interceptor works inside Spring MVC after a handler has been selected.
Request and response wrapping
A filter can wrap or replace the request and response objects passed further into the chain:
chain.doFilter(wrappedRequest, wrappedResponse);
This makes filters suitable for:
- Caching a request body
- Reading a request body multiple times
- Capturing a response body
- Transforming request or response data
- Compression and encoding concerns
An interceptor receives the request and response after processing has already entered Spring MVC. It cannot replace the objects used by the entire servlet chain in the same way.
18. Filter vs Interceptor
19. When to Use a Filter
Use a filter when the concern applies to the complete HTTP or servlet request rather than specifically to a controller method.
Common examples:
- Request or response body wrapping
- Request-body caching
- Response-body capture or transformation
- Character encoding
- Compression
- CORS-related processing
- Logging all servlet traffic
- Security processing before Spring MVC
Spring Security uses servlet filters because authentication and authorization generally need to happen before a request reaches the controller layer.
20. When to Use an Interceptor
Use an interceptor when the requirement depends on Spring MVC or the selected handler.
Common examples:
- Logging controller and method names
- Reading custom annotations from controller methods
- Starting and completing controller-related timing
- Adding common request attributes
- Adding common model attributes in traditional MVC
- Collecting MVC-specific metrics
- Applying tenant or organization context based on handler rules
- Running logic before and after a particular group of controller endpoints
21. Common Mistakes
Mistake 1: Creating an interceptor but not registering it
@Component creates the bean, but addInterceptors() places it in the Spring MVC chain.
Mistake 2: Returning false without writing a response
When preHandle() returns false, the controller does not execute. Prepare an appropriate status and body before stopping the chain.
Mistake 3: Storing request data in an interceptor field
Interceptors are normally singleton beans. Store request-specific values in request attributes instead.
Mistake 4: Blindly casting handler
Not every handler is a HandlerMethod. Always check with instanceof.
Mistake 5: Modifying a REST body inside postHandle()
The response may already have been written. Use ResponseBodyAdvice or a filter-based wrapper, depending on the requirement.
Mistake 6: Building production security manually
A custom API-key interceptor is useful for learning request flow, but production authentication and authorization should normally use Spring Security.
Mistake 7: Assuming ex == null means no error occurred
A global exception handler may already have handled the exception. Also inspect the final HTTP status.
22. Final Mental Model
A servlet filter asks:
What should happen around the complete HTTP request before or after it reaches a servlet?
A Spring MVC interceptor asks:
What should happen around the selected Spring MVC handler before and after the controller executes?
The most important lifecycle rule is:
And with multiple interceptors:
Forward order before the controller
Reverse order after the controller