How do HandlerInterceptors registered via addInterceptors work, including ordering and lifecycle, and how do they differ from Servlet Filters?
answer
- preHandle(false→stop) / postHandle(before view, skipped on exception) / afterCompletion(always if preHandle true, gets exception)
- preHandle forward, post/afterCompletion reverse (nesting)
- interceptor inside DispatcherServlet, knows handler + ModelAndView
- filter = container level, wraps whole request incl. static/error, no handler
- postHandle too late to change @ResponseBody → use ResponseBodyAdvice
basics
~20 saddInterceptors registers HandlerInterceptors with preHandle/postHandle/afterCompletion hooks around controller execution. They run inside the DispatcherServlet (know the handler), and execute in registration order. Filters are lower-level Servlet-container components wrapping the whole request, unaware of the specific handler.
solid answer
~50 sIn addInterceptors(InterceptorRegistry) you register HandlerInterceptors and optionally scope them with addPathPatterns/excludePathPatterns. Each interceptor has three hooks: preHandle (before the controller; return false to short-circuit and stop the chain), postHandle (after the controller, before view rendering — can tweak the ModelAndView; skipped if the handler threw), and afterCompletion (after rendering, always runs if preHandle returned true — for cleanup, gets any exception). Ordering follows registration order: preHandle runs in forward order, postHandle and afterCompletion in reverse — a nesting model. Interceptors live inside the DispatcherServlet, so they know the matched handler and participate in MVC concepts (model, view). Servlet Filters (jakarta.servlet.Filter) sit outside/around the DispatcherServlet in the container's chain, wrap the entire request/response including static resources and error dispatches, and don't know which controller will handle it. Use interceptors for handler-aware MVC concerns; filters for coarse cross-cutting (security, compression, request wrapping).
code
java · 25 linespublic class TimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
req.setAttribute("start", System.nanoTime());
return true; // false would short-circuit the whole chain
}
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res,
Object handler, ModelAndView mav) {
// runs before view render; skipped if handler threw
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
long ms = (System.nanoTime() - (long) req.getAttribute("start")) / 1_000_000;
// ex is non-null if the handler/view failed
}
}
@Configuration
class Cfg implements WebMvcConfigurer {
@Override public void addInterceptors(InterceptorRegistry r) {
r.addInterceptor(new TimingInterceptor()).addPathPatterns("/api/**");
}
}go deeper
Should know interceptors have before/after hooks around a controller and are registered in addInterceptors.
Should name the three hooks and that preHandle false short-circuits; know path patterns.
Should explain ordering/nesting, afterCompletion-on-exception, and interceptor-vs-filter layering.
Should reason about where a concern belongs (filter vs interceptor vs advice), REST body-mutation limits, and dispatch-type coverage for cross-cutting infrastructure.
## Registering interceptors ```java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/health", "/api/login") .order(0); } ``` `HandlerInterceptor` has three callbacks: - **`preHandle(request, response, handler)` → boolean** — runs **before** the controller method. Returning **`false`** stops the chain: the handler and remaining interceptors do NOT run, and you're expected to have written the response. Returning `true` continues. - **`postHandle(request, response, handler, ModelAndView)`** — runs **after** the controller returns but **before** the view is rendered. Can inspect/modify the `ModelAndView`. **Not called if the handler threw** an exception. Of limited use with `@ResponseBody`/`ResponseEntity` because the body may already be written by a message converter. - **`afterCompletion(request, response, handler, Exception)`** — runs **after** the view is rendered (or the response is complete), and is the place for resource cleanup. Called **only if this interceptor's preHandle returned true**, and it receives any exception that occurred. Analogous to a finally block. ## Ordering and nesting Interceptors run as a **nested chain** in registration order (or explicit `.order(int)`): - `preHandle`: A → B → C (forward) - controller executes - `postHandle`: C → B → A (reverse) - `afterCompletion`: C → B → A (reverse) If B's `preHandle` returns `false`, then C never runs, B's and C's `postHandle` never run, and `afterCompletion` is invoked only for interceptors whose `preHandle` had already returned true (here A) — DispatcherServlet triggers `afterCompletion` for the ones that started. ## Path matching `addPathPatterns`/`excludePathPatterns` scope the interceptor. Pattern matching uses the configured path-matching strategy (`PathPattern` by default in modern Spring/Boot). Without patterns, it applies to all handler mappings within MVC. ## Interceptor vs Servlet Filter — the key architectural distinction | Aspect | HandlerInterceptor | Servlet Filter | |---|---|---| | Layer | Inside `DispatcherServlet` (Spring MVC) | Servlet container, around the whole servlet | | Knows the handler? | Yes — receives the matched `handler` | No — only request/response | | Scope | Only requests that reach an MVC handler mapping | Entire request incl. static resources, other servlets, ERROR/ASYNC dispatches | | Access to MVC model/view | Yes (`ModelAndView` in postHandle) | No | | API | `org.springframework.web.servlet.HandlerInterceptor` | `jakarta.servlet.Filter` | | Can wrap request/response? | No (can't replace the stream cleanly) | Yes — can substitute wrappers, buffer the body | | Ordering | registration order / `.order()` | `@Order`/`FilterRegistrationBean` order; runs before DispatcherServlet | Execution nesting overall: **Filter(s) → DispatcherServlet → Interceptor.preHandle → Controller → Interceptor.postHandle → view → Interceptor.afterCompletion → back out through Filter(s)**. ## When to choose which - **Filter**: security (Spring Security is filter-based), request/response wrapping and body buffering, compression, CORS at the edge, anything that must also cover static resources and error dispatches, or that must run before Spring MVC even decides on a handler. - **HandlerInterceptor**: handler-aware concerns — per-controller auth/annotation checks, populating model attributes, timing a specific handler, adding common response headers for MVC endpoints, locale/theme changes (`LocaleChangeInterceptor`). ## Gotchas - With REST controllers, `postHandle` is often too late to change the body since converters may have already written it — prefer a `ResponseBodyAdvice` for body mutation. - Exceptions from the handler skip `postHandle` but still hit `afterCompletion` with the exception; if a `@ExceptionHandler`/`HandlerExceptionResolver` handles it, rendering still proceeds. - Returning `false` from `preHandle` means you must write the response yourself (status/body), otherwise the client gets an empty 200. - Interceptors are singletons — keep them stateless/thread-safe.
- Why might postHandle be the wrong place to modify a REST @ResponseBody response?By postHandle the HttpMessageConverter may already have serialized and written the body to the response stream, so changes are ignored. Use a ResponseBodyAdvice (or a Filter with response wrapping) to alter the body instead.
- If interceptor B's preHandle returns false, whose afterCompletion runs?Only interceptors whose preHandle had already returned true before B — DispatcherServlet calls afterCompletion in reverse for those that started. B's own and later interceptors' afterCompletion do not run, and the handler is skipped.
- Why is Spring Security implemented as a Filter rather than a HandlerInterceptor?Security must run before the DispatcherServlet even selects a handler and must cover all requests including static resources and error/async dispatches; a filter sits at the container edge and can wrap the request/response, which an interceptor cannot.
saying these in an interview costs you the question
- Saying afterCompletion runs even when preHandle returned false
- Claiming interceptors can wrap/replace the request or response stream
- Thinking postHandle reliably lets you rewrite a JSON response body
- Believing interceptors run for static resources and error dispatches like filters do
- Confusing registration order (forward preHandle) with reverse postHandle/afterCompletion