Interceptors
A HandlerInterceptor lets you run logic around controller execution — before the handler runs, after it produces a result, and after the response completes. Because interceptors sit inside Spring MVC’s DispatcherServlet, they have access to the matched handler and the ModelAndView, which servlet filters do not. This page covers the lifecycle, registration, common use cases, and when to choose an interceptor over a filter.
The interceptor lifecycle
HandlerInterceptor defines three callbacks, all with default no-op implementations so you override only what you need.
import jakarta.servlet.http.*;
import org.springframework.web.servlet.*;
public class TimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
req.setAttribute("startTime", System.currentTimeMillis());
return true; // false short-circuits: the handler is NOT invoked
}
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res,
Object handler, ModelAndView mav) {
// runs after the handler, before the view renders
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
long start = (long) req.getAttribute("startTime");
long took = System.currentTimeMillis() - start;
System.out.printf("%s %s -> %d (%d ms)%n",
req.getMethod(), req.getRequestURI(), res.getStatus(), took);
}
}
| Callback | When it runs | Can stop the request? |
|---|---|---|
preHandle | Before the handler | Yes — return false |
postHandle | After handler, before view render | No |
afterCompletion | After the response is complete | No |
Note: Returning
falsefrompreHandlestops the chain — you are then responsible for writing the response (e.g. setting a 401 status). For@RestControllerappspostHandleis rarely useful because there is no view to render.
Registering an interceptor
Register interceptors in a WebMvcConfigurer. You can scope them to path patterns and exclude others.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TimingInterceptor());
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/api/auth/login");
}
}
Because AuthInterceptor is a Spring bean, it can use constructor injection like any other component.
Use case: API key authentication
import jakarta.servlet.http.*;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final ApiKeyService apiKeys;
public AuthInterceptor(ApiKeyService apiKeys) {
this.apiKeys = apiKeys;
}
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String key = req.getHeader("X-API-Key");
if (key == null || !apiKeys.isValid(key)) {
res.setStatus(HttpStatus.UNAUTHORIZED.value());
return false; // stop here; handler is never reached
}
return true;
}
}
Request:
curl -i http://localhost:8080/api/orders # no X-API-Key header
Output:
HTTP/1.1 401 Unauthorized
Tip: For real authentication and authorization, prefer Spring Security filters over a hand-rolled interceptor. Interceptors are great for lightweight, app-specific concerns like timing, request logging, or audit tagging.
Common use cases
- Timing / metrics — measure handler duration in
preHandle/afterCompletion. - Request logging — log method, path, status, and elapsed time.
- Lightweight auth — reject requests missing an API key.
- Correlation IDs — attach a tracing id to MDC for the request’s lifetime.
- Locale / tenant resolution — set per-request context.
Interceptor vs filter
Both wrap request handling, but at different layers. A servlet filter runs in the servlet container, outside Spring MVC; an interceptor runs inside the DispatcherServlet.
| Aspect | Interceptor | Filter |
|---|---|---|
| Layer | Spring MVC dispatcher | Servlet container |
| Knows the handler? | Yes (Object handler) | No |
Access to ModelAndView | Yes | No |
| Can modify raw stream | No (response already routed) | Yes (wrap request/response) |
| Applies to non-MVC requests | No | Yes (all servlets/resources) |
| Registration | WebMvcConfigurer | @Component / FilterRegistrationBean |
Choose an interceptor when you need handler-aware, MVC-scoped logic. Choose a filter when you must touch the raw request/response stream or affect every request, including static resources.
Pitfalls
postHandledoes not run ifpreHandlereturnedfalseor the handler threw — useafterCompletionfor guaranteed cleanup.- Interceptors do not see requests that never reach the dispatcher (e.g. blocked earlier by a filter).
- Throwing from
preHandlepropagates to your exception handlers; setting a status and returningfalsedoes not.