When would you choose a Filter over a HandlerInterceptor (and vice versa)? Walk through the full execution flow of a request through both.
answer
- Need raw bytes / static / errors -> Filter
- Need handler annotations / model -> Interceptor
- Filters wrap interceptors wrap handler; never interleave
- Security = filter layer, runs before interceptors
- ContentCachingResponseWrapper -> must copyBodyToResponse
basics
~20 sUse a Filter for low-level, framework-agnostic work that must wrap the whole request or touch raw request/response bytes (logging with body caching, compression, CORS, security). Use an interceptor when you need the matched Spring handler or the ModelAndView. Flow: filters wrap interceptors wrap the handler.
solid answer
~40 sChoose a Filter when the concern is framework-agnostic or must operate on the raw request/response, or apply beyond Spring MVC: request/response body logging via ContentCachingRequest/ResponseWrapper, GZIP, CORS, rate limiting, security (Spring Security is a filter chain), request-ID/MDC setup, wrapping the response to modify bytes. Choose a HandlerInterceptor when you need Spring MVC context: the target HandlerMethod and its annotations, path-pattern scoping, model manipulation before view render, or per-handler metrics. Execution flow for a normal request: Filter1.doFilter (in) -> Filter2.doFilter (in) -> DispatcherServlet -> HandlerMapping resolves handler -> Interceptor.preHandle (in order) -> controller method -> Interceptor.postHandle (reverse) -> view rendered -> Interceptor.afterCompletion (reverse) -> back out through Filter2 -> Filter1. So filters strictly wrap interceptors, which strictly wrap the handler; the two layers never interleave.
code
java · 31 lines// Interceptor that needs handler metadata — a good fit for the interceptor layer
public class RoleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws IOException {
if (handler instanceof HandlerMethod hm) {
RequiresRole ann = hm.getMethodAnnotation(RequiresRole.class);
if (ann != null && !currentUserHasRole(ann.value())) {
res.sendError(HttpServletResponse.SC_FORBIDDEN); // must write response ourselves
return false; // abort: handler + postHandle skipped
}
}
return true;
}
}
// Body logging needs raw bytes — a filter with caching wrappers, not an interceptor
public class BodyLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
var wrappedReq = new ContentCachingRequestWrapper(req);
var wrappedRes = new ContentCachingResponseWrapper(res);
try {
chain.doFilter(wrappedReq, wrappedRes);
} finally {
log.info("req body: {}", new String(wrappedReq.getContentAsByteArray()));
log.info("res body: {}", new String(wrappedRes.getContentAsByteArray()));
wrappedRes.copyBodyToResponse(); // MUST copy cached body back to the real response
}
}
}go deeper
Give the rule of thumb (Filter=low level/whole request; Interceptor=handler-aware) and that filters wrap interceptors.
Provide concrete examples for each and sketch the nested execution flow.
Explain the strict layering (no interleaving), the exception-path differences, and body-wrapping mechanics.
Reasons about placing security/tracing in the filter layer, cross-cutting consistency, and when ResponseBodyAdvice beats a wrapper filter.
## Decision framework Ask: **does the concern need Spring MVC knowledge?** ### Reach for a Filter when… - You must touch the **raw** request or response — reading/replacing the body, GZIP, encoding, wrapping (`ContentCachingRequestWrapper`, `ContentCachingResponseWrapper`, `HttpServletResponseWrapper`). - The concern must apply to **everything**, not just Spring handlers — static resources, other servlets, unmapped 404s, error dispatches. - It's a **framework-agnostic / infrastructural** concern: security (Spring Security's whole model is filters), CORS (`CorsFilter`), request-ID/MDC/tracing setup, rate limiting, tenant resolution, character-encoding, compression. - You need to run **before Spring MVC even initializes** for the request. ### Reach for a HandlerInterceptor when… - You need the **matched handler** — e.g. read a custom annotation on the `HandlerMethod` to decide behavior (`hm.getMethodAnnotation(RequiresRole.class)`). - You want **path-pattern scoping** with `addPathPatterns`/`excludePathPatterns` (cleaner than URL-pattern registration). - You want to **contribute to the model** before a server-side view renders (Thymeleaf/JSP), e.g. common attributes. - You want **per-controller timing/metrics** tied to the resolved handler. - The work is inherently **Spring MVC** and doesn't need raw byte access. ### The overlap and the tie-breaker Many concerns (auth, logging) *can* be done either way. Tie-breakers: - Need to modify a JSON body? Filter (with response wrapper) or `ResponseBodyAdvice`, not `postHandle`. - Need the handler's annotations? Interceptor. - Must also cover non-MVC/static/error paths? Filter. ## Full execution flow (normal, successful request) ``` CLIENT |> Filter1.doFilter (pre-chain code) |> Filter2.doFilter (pre-chain code) |> DispatcherServlet.doDispatch - HandlerMapping selects the handler + interceptor chain |> InterceptorA.preHandle (registration order) |> InterceptorB.preHandle |> @Controller handler method executes <| InterceptorB.postHandle (reverse order, has ModelAndView) <| InterceptorA.postHandle - View rendered (or message converter writes @ResponseBody) <| InterceptorB.afterCompletion (reverse order) <| InterceptorA.afterCompletion <| (DispatcherServlet returns) <| Filter2.doFilter (post-chain code, response committing) <| Filter1.doFilter (post-chain code) CLIENT receives response ``` Key properties: - **Filters strictly wrap interceptors**, which strictly wrap the handler. The layers never interleave — you cannot put a filter *between* two interceptors. - `preHandle` runs in **registration order**; `postHandle`/`afterCompletion` unwind in **reverse**. - On the **filter** side too, code before `chain.doFilter` runs top-down and code after runs bottom-up (classic chain unwinding). ## Exception path differences - If a filter's downstream `chain.doFilter` throws, the exception propagates back up through the outer filters (they can catch it) — filters see exceptions that a `@ControllerAdvice` might otherwise translate, because advice runs inside the DispatcherServlet. - Interceptor `postHandle` is skipped on handler exceptions; `afterCompletion` still runs with the exception. - A filter can observe the **final HTTP status** after Spring's exception handling wrote the response; an interceptor's afterCompletion sees the exception object but the status may reflect the resolved error. ## Practical architectural note Because Spring Security lives in the filter layer, security decisions generally happen **before** any interceptor. Don't try to do primary authentication in an interceptor if a filter-based mechanism is available — the filter runs first and covers more surface.
- Why can't you put a filter between two interceptors in the execution order?They live at different layers. Filters run in the servlet container chain that wraps the entire DispatcherServlet; interceptors run inside the DispatcherServlet after handler mapping. All filters complete their inbound pass before any interceptor runs, so ordering values only rank filters-among-filters and interceptors-among-interceptors, never across the boundary.
- You used a ContentCachingResponseWrapper to log the response body. Users report empty responses. Why?The wrapper caches the body in a buffer instead of writing it straight to the client. You must call copyBodyToResponse() (typically in a finally block) to flush the cached bytes to the real response, otherwise the client gets an empty body.
saying these in an interview costs you the question
- Claiming an interceptor can sit outside/before the filter chain
- Doing primary authentication in a postHandle interceptor
- Forgetting copyBodyToResponse with ContentCachingResponseWrapper
- Saying filters and interceptors can interleave by tuning order values
- Trying to rewrite a @ResponseBody JSON in postHandle