What sets an interceptor apart from a filter is position. An interceptor runs inside the DispatcherServlet, after handler mapping has decided which controller handles the request. So an interceptor already knows which handler is about to run. It intercepts that execution at three points (before the controller, after the controller, after view rendering), and at the first point it can block the request entirely.
The three hooks
HandlerInterceptor provides three methods, each intercepting a different moment.
preHandle
preHandle runs before the controller and returns a boolean. Return true and the request proceeds to the next interceptor or the controller; return false and request processing stops right there, with the controller never executing. That’s why it fits gatekeeper roles like authentication checks, permission validation, and rate limiting.
postHandle
postHandle runs after the controller finishes normally, before the view is rendered. It receives ModelAndView, so it’s used to inject model data needed across all views in one place. If the controller throws, this method is skipped. In a @RestController-based REST API there’s no view rendering, so postHandle is rarely useful.
afterCompletion
afterCompletion always runs after view rendering finishes and the response has gone out. It runs even when the controller threw, which makes it right for post-processing like resource cleanup, logging, and execution-time measurement. Its fourth parameter is an Exception: null if none occurred, otherwise the thrown object, so you can attach extra logging or alerts.
How the hooks and the block run together
The order with multiple interceptors trips people up. Registered as A then B, both passing:
preHandle A → preHandle B → Controller → postHandle B → postHandle A
→ (view rendering) → afterCompletion B → afterCompletion A
preHandle runs in registration order (A → B); postHandle and afterCompletion run in reverse (B → A), like a stack: last in, first out.
Add a block and the rule applies once more. If B’s preHandle returns false, neither the controller nor postHandle runs, and only the afterCompletion of the already-passed A is called.
preHandle A(true) → preHandle B(false)
→ afterCompletion A (B itself and everything after is skipped)
Because of this rule, a resource started before a preHandle block is still cleaned up in afterCompletion.
Implementation and registration
Implement HandlerInterceptor, then register it on the InterceptorRegistry in WebMvcConfigurer’s addInterceptors.
public class CustomInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// before controller execution
return true; // false stops request processing
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
// after a clean controller return, before view rendering
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// after view rendering (runs even on exception)
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "/error")
.order(1);
}
}
addPathPatterns sets the included paths, excludePathPatterns the excluded ones, and order the sequence. Unlike a filter, which hits every request uniformly, an interceptor can narrow its scope with path patterns. Paths use Ant-style matching: ** is any sub-path, * is a single segment, ? is a single character. /api/** matches every path starting with /api, and /user/*/profile matches /user/123/profile.
Authentication
The typical use of the gate is authentication. Check the token in preHandle, and if it’s invalid, return false to block the request and set the 401 response yourself. This is a clearer flow than throwing an exception.
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !tokenService.isValid(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Unauthorized\"}");
return false; // never reaches the controller
}
return true;
}
}
Timing
preHandle and afterCompletion bracket the start and end of a request, so they’re often used as a pair. Store the start time in a request attribute, then compute the delta at the end. Since afterCompletion runs even on exception, the measurement is never missed.
@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("Request {} {} completed in {}ms", request.getMethod(), request.getRequestURI(), duration);
}
}
Keeping the start time in a request attribute rather than a ThreadLocal isolates it per request and is safer.
Things to watch
- To use Spring beans, register the interceptor as a bean. Mark it
@Componentor define it as a bean in@Configuration, then inject it. Creating it withnewmeans DI won’t work. - Every request goes through it, so avoid heavy work. Reduce DB queries and external API calls with caching where you can.
- In a REST API, prefer
preHandle/afterCompletionoverpostHandle. With no view rendering,postHandleis mostly empty. - Handle auth failure by returning
false, not by throwing. Setting the response yourself makes the flow clearer.