Spring Boot: Request Interceptors with HandlerInterceptor
HandlerInterceptor is a Spring MVC mechanism that lets you intercept HTTP requests before they reach a controller, after the controller returns, and after the complete response is committed. Interceptors are ideal for cross-cutting concerns like logging, authentication, and rate limiting that need access to the Spring MVC context.
Contents
What Are Interceptors
In Spring MVC, a HandlerInterceptor
: runs before the controller method is invoked. Can short-circuit the request by returningpreHandle .false : runs after the controller returns but before the view is rendered. Gives access to thepostHandle .ModelAndView : runs after the entire request is finished (response committed). Always called ifafterCompletion returnedpreHandle , even when exceptions occur.true
Interceptors operate within the Spring MVC layer, meaning they run after the DispatcherServlet
DispatcherServletCommon use cases for interceptors include:
- Request and response logging with timing metrics
- Authentication and authorization checks before controller execution
- Rate limiting per client or IP address
- Setting request attributes or MDC context for downstream processing
- Modifying the
before view renderingModelAndView
Creating an Interceptor
To create an interceptor, implement the HandlerInterceptor
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class SampleInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(SampleInterceptor.class);
/**
* Called before the handler method executes.
* Return true to continue, false to abort the request.
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
log.info("preHandle: {} {}", request.getMethod(), request.getRequestURI());
// Access the controller method if the handler is a HandlerMethod
if (handler instanceof HandlerMethod hm) {
log.debug("Controller: {}.{}()",
hm.getBeanType().getSimpleName(),
hm.getMethod().getName());
}
return true; // continue to controller
}
/**
* Called after the handler returns, before view rendering.
* Not called if preHandle returned false or an exception was thrown.
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
log.info("postHandle: {} {}", request.getMethod(), request.getRequestURI());
}
/**
* Called after the complete request finishes.
* Always called if preHandle returned true, even on exceptions.
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
log.info("afterCompletion: {} {} — status {}",
request.getMethod(), request.getRequestURI(), response.getStatus());
if (ex != null) {
log.error("Request resulted in exception: {}", ex.getMessage());
}
}
}
postHandlepreHandlefalseafterCompletionRegistering Interceptors
Interceptors are registered through WebMvcConfigureraddInterceptors
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 WebMvcConfig implements WebMvcConfigurer {
private final SampleInterceptor sampleInterceptor;
public WebMvcConfig(SampleInterceptor sampleInterceptor) {
this.sampleInterceptor = sampleInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sampleInterceptor)
.addPathPatterns("/api/**") // apply to all /api/ routes
.excludePathPatterns("/api/health") // skip health endpoint
.excludePathPatterns("/api/public/**"); // skip public endpoints
}
}
Key points about path pattern configuration:
: specifies which URL patterns the interceptor should match. Uses Ant-style patterns:addPathPatterns matches all paths,/** matches one level,/api/* matches all levels./api/** : specifies URL patterns to skip. Exclude patterns take priority over include patterns.excludePathPatterns- If no path patterns are specified, the interceptor applies to all requests (
)./**
newRequest Logging Interceptor
A practical logging interceptor that records the HTTP method, URI, response status, and execution time for every request.
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
private static final String START_TIME_ATTR = "requestStartTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
log.info("--> {} {} [client={}]",
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute(START_TIME_ATTR);
long duration = System.currentTimeMillis() - startTime;
log.info("<-- {} {} | status={} | duration={}ms",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration);
if (ex != null) {
log.error("Request failed with exception: {}", ex.getMessage(), ex);
}
}
}
Register the logging interceptor for all API routes:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "/css/**", "/js/**")
.order(0); // run first
}
Example log output:
INFO --> GET /api/users [client=192.168.1.10]
INFO <-- GET /api/users | status=200 | duration=45ms
INFO --> POST /api/orders [client=192.168.1.10]
INFO <-- POST /api/orders | status=201 | duration=123ms
Authentication Interceptor
An interceptor that validates a Bearer token in the Authorization
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public AuthInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// Allow preflight CORS requests to pass through
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Missing or invalid Authorization header\"}");
return false;
}
String token = authHeader.substring(7);
if (!tokenService.validateToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Invalid or expired token\"}");
return false;
}
// Store the authenticated user info for downstream use
String userId = tokenService.extractUserId(token);
request.setAttribute("authenticatedUserId", userId);
return true;
}
}
Register the auth interceptor on protected routes, excluding public endpoints:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/auth/login",
"/api/auth/register",
"/api/public/**",
"/api/health"
)
.order(1);
}
Rate Limiting Interceptor
A simple in-memory rate limiter that restricts each client IP to a configurable number of requests per time window. Exceeding the limit returns HTTP 429 (Too Many Requests).
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private static final int MAX_REQUESTS_PER_MINUTE = 60;
private static final long WINDOW_SIZE_MS = 60_000L;
private final ConcurrentMap<String, ClientRequestInfo> clientRequests =
new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String clientIp = request.getRemoteAddr();
ClientRequestInfo info = clientRequests.computeIfAbsent(
clientIp, k -> new ClientRequestInfo());
// Reset the counter if the time window has elapsed
long now = System.currentTimeMillis();
if (now - info.windowStart > WINDOW_SIZE_MS) {
info.counter.set(0);
info.windowStart = now;
}
int currentCount = info.counter.incrementAndGet();
// Add rate limit headers to every response
response.setHeader("X-RateLimit-Limit",
String.valueOf(MAX_REQUESTS_PER_MINUTE));
response.setHeader("X-RateLimit-Remaining",
String.valueOf(Math.max(0, MAX_REQUESTS_PER_MINUTE - currentCount)));
if (currentCount > MAX_REQUESTS_PER_MINUTE) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Rate limit exceeded. Try again later.\"}");
return false;
}
return true;
}
private static class ClientRequestInfo {
volatile long windowStart = System.currentTimeMillis();
final AtomicInteger counter = new AtomicInteger(0);
}
}
Interceptor Execution Order
When multiple interceptors are registered, they execute in a defined sequence. The orderpreHandleafterCompletion
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/**")
.order(0); // runs first
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**", "/api/public/**")
.order(1); // runs second
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.order(2); // runs third
}
For three interceptors A (order 0), B (order 1), and C (order 2), the execution flow is:
A.preHandle() -> B.preHandle() -> C.preHandle()
|
Controller Method
|
C.postHandle() <- B.postHandle() <- A.postHandle() (reverse never happens — see note)
|
C.afterCompletion() -> B.afterCompletion() -> A.afterCompletion()
Key rules about execution order:
methods run in order (lowest to highest). If any interceptor returnspreHandle , subsequent interceptors and the controller are skipped.false methods run in reverse order (highest to lowest). Skipped entirely if anypostHandle returnedpreHandle or the controller threw an exception.false methods run in reverse order, but only for interceptors whoseafterCompletion returnedpreHandle . This guarantees cleanup even on failure.true
preHandlefalseafterCompletionpreHandletruepostHandleInterceptors vs Filters
Filters and interceptors can both intercept HTTP requests, but they operate at different layers and have different capabilities.
| Aspect | Servlet Filter | HandlerInterceptor |
|---|---|---|
| Specification | Jakarta Servlet API | Spring MVC |
| Execution point | Before/after DispatcherServlet | After DispatcherServlet, around the handler |
| Access to handler/controller | No | Yes (via HandlerMethod |
| Access to ModelAndView | No | Yes (in postHandle |
| Can modify request/response | Yes (wrapper classes) | Limited (can set attributes, write to response) |
| Exception handling | Must handle in filter | afterCompletion |
| URL pattern matching | Servlet URL patterns | Ant-style Spring MVC patterns |
| Spring bean injection | Via @ComponentFilterRegistrationBean | Full Spring bean support |
| Works with non-MVC requests | Yes (all servlet requests) | No (only DispatcherServlet requests) |
Use a Filter when you need to:
- Modify the raw request or response (e.g., wrapping the input stream, compressing the response)
- Intercept all servlet requests including static resources and non-Spring endpoints
- Work at the lowest level before Spring MVC processing (e.g., CORS headers, encoding)
Use an Interceptor when you need to:
- Access the controller method or its annotations (e.g., checking a custom
annotation)@RateLimit - Run logic only for specific controller routes using Ant-style path patterns
- Modify the
before view renderingModelAndView - Use
for guaranteed cleanup with exception visibilityafterCompletion