skip to content

HandlerInterceptor vs Servlet Filter

A servlet Filter sits at container level and sees every request; a HandlerInterceptor sits inside MVC and knows which handler was chosen. Choosing correctly between them — and knowing the order they run in — is the question.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is the difference between a Servlet Filter and a Spring HandlerInterceptor?

level: juniorimportance: must knowfreq 72%

answer

  1. Filter = container/Servlet spec; Interceptor = Spring MVC
  2. Filter wraps DispatcherServlet; Interceptor inside it
  3. Interceptor knows the handler; filter does not
  4. Filter sees static/errors/all servlets
  5. pre/post/afterCompletion vs doFilter

basics

~10 s

A Filter runs in the Servlet container around every request, before Spring even sees it. A HandlerInterceptor runs inside Spring MVC's DispatcherServlet, only for controller requests, and knows which controller (handler) will run.

solid answer

~40 s

A servlet Filter (jakarta.servlet.Filter) is a Servlet-spec component that wraps the whole request/response at container level via doFilter(req, res, chain). It runs before and after the DispatcherServlet, sees every request (static files, other servlets, errors), and can short-circuit or wrap the request/response. A HandlerInterceptor is a Spring MVC concept that runs inside the DispatcherServlet, after handler mapping, so it only sees requests routed to a Spring handler and it knows the handler object. It exposes preHandle, postHandle, and afterCompletion. Rule of thumb: use a Filter for low-level, framework-agnostic concerns (logging, compression, CORS, security) and an interceptor when you need Spring MVC context — the target handler, the ModelAndView, or path-pattern matching. Filters are registered with the container; interceptors via WebMvcConfigurer.

code

java · 28 lines
java
// Servlet Filter — container scope, framework-agnostic
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        long start = System.nanoTime();
        try {
            chain.doFilter(req, res); // pass down the chain (into DispatcherServlet)
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} {} -> {} ({} ms)", req.getMethod(), req.getRequestURI(), res.getStatus(), ms);
        }
    }
}

// HandlerInterceptor — MVC scope, handler-aware
public class AuditInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        if (handler instanceof HandlerMethod hm) {
            // We know the exact controller method that will run
            log.info("Invoking {}", hm.getMethod().getName());
        }
        return true; // false would abort before the controller runs
    }
}

go deeper

for a junior

Know the one-line distinction: Filter = container level, runs on everything; Interceptor = Spring MVC level, handler-aware.

for a middle

Should articulate the wrapping order (filter -> interceptor -> handler) and which registration mechanism each uses.

for a senior

Should give concrete 'use which' examples and the @ResponseBody / static-resource gotchas.

for a principal

Frames it as layering: container boundary vs framework boundary, and the consequences for cross-cutting architecture (where security, tracing, and MDC belong).

## The two mechanisms Both are ways to run cross-cutting logic around web request processing, but they live at different layers. ### Servlet Filter `jakarta.servlet.Filter` is part of the **Servlet specification**, not Spring. A filter has one main method: ```java void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) ``` Filters form a **chain**. Each filter does work, then calls `chain.doFilter(req, res)` to pass control to the next filter (and eventually to the `DispatcherServlet`). Code before `chain.doFilter` runs on the way **in**; code after it runs on the way **out**. A filter can: - inspect or modify the request/response, - **wrap** them (`HttpServletRequestWrapper` / `HttpServletResponseWrapper`), - **short-circuit** by not calling `chain.doFilter` (e.g. reject with 401), - catch exceptions bubbling back up. Because it sits at the **container** level, a filter sees **every** request into the web app: requests to `DispatcherServlet`, to other servlets, to static resources, and error dispatches. It has **no idea** which Spring controller will handle the request — Spring hasn't run yet. ### HandlerInterceptor `org.springframework.web.servlet.HandlerInterceptor` is a **Spring MVC** concept. It runs **inside** the `DispatcherServlet`, *after* Spring has decided which handler (controller method) should serve the request. Its three methods: - `boolean preHandle(request, response, handler)` — before the handler runs. Returning `false` stops processing. - `void postHandle(request, response, handler, ModelAndView)` — after the handler, before the view renders. - `void afterCompletion(request, response, handler, Exception)` — after the whole request finishes. Because it runs after handler mapping, it **knows the handler** (often a `HandlerMethod`) and can be scoped to URL patterns. ## Scope difference (key mental model) ``` [Container] Filter -> Filter -> DispatcherServlet [Spring MVC] Interceptor.preHandle -> Controller -> Interceptor.postHandle -> View -> Interceptor.afterCompletion <- back out through the DispatcherServlet <- Filter <- Filter (unwind) ``` A filter **wraps** the interceptor, which wraps the handler. ## When to use which - **Filter**: framework-agnostic, low-level concerns — request/response logging with body caching, GZIP compression, CORS, request ID/MDC setup, authentication (Spring Security itself is a filter chain), rate limiting, anything that must also apply to static resources or non-Spring servlets, or anything needing to modify the raw response bytes. - **HandlerInterceptor**: concerns that need **Spring MVC context** — checking annotations on the target `HandlerMethod`, populating the model, measuring controller execution time, path-pattern-scoped auth checks, adding response headers based on the matched handler. ## Gotchas - A filter can modify the response body by wrapping; an interceptor's `postHandle` cannot usefully change a `@ResponseBody` REST response because the body is already written by the `HttpMessageConverter`. - Interceptors only fire for requests that reach a Spring handler — a 404 for an unmapped path may never hit your interceptor, but a filter still sees it. - Registration differs: filters register with the container (bean auto-registration, `FilterRegistrationBean`, `@WebFilter`); interceptors register via `WebMvcConfigurer.addInterceptors`.

  • Which one can see a request for a static resource or a URL with no controller mapping?
    The Filter. It runs at container level before the DispatcherServlet, so it sees every request including static resources and unmapped paths (404s). A HandlerInterceptor only fires when a request is routed to a Spring handler.
  • Can an interceptor modify a JSON @ResponseBody payload in postHandle?
    Not usefully. For @ResponseBody, the HttpMessageConverter has already serialized and written the body to the response by the time postHandle runs, so mutating the ModelAndView does nothing. To alter the raw bytes you need a Filter with a ContentCachingResponseWrapper (or a ResponseBodyAdvice).

saying these in an interview costs you the question

  • Claiming interceptors run before filters
  • Saying a HandlerInterceptor sees static resources / all requests
  • Thinking Filter and HandlerInterceptor are the same thing with different names
  • Believing an interceptor knows nothing about the target handler

context

open as a page

Explain the three HandlerInterceptor methods — preHandle, postHandle, afterCompletion — and when each is (and isn't) called.

level: middleimportance: must knowfreq 58%

basics

~20 s

preHandle runs before the controller and can abort by returning false. postHandle runs after the controller but before the view renders (skipped if the controller throws). afterCompletion runs after the response is complete, even on error, and receives the exception.

open as a page

How do you register and order Servlet Filters and HandlerInterceptors in a Spring Boot app, and what is OncePerRequestFilter for?

level: middleimportance: should knowfreq 46%

basics

~20 s

Register interceptors by implementing WebMvcConfigurer.addInterceptors and calling registry.addInterceptor(...); order = registration order. Register filters as beans (Spring Boot auto-registers them) or via FilterRegistrationBean to control order and URL patterns. OncePerRequestFilter guarantees the filter runs once per request, even across forwards/async dispatches.

open as a page

When would you choose a Filter over a HandlerInterceptor (and vice versa)? Walk through the full execution flow of a request through both.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use 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.

open as a page

How do HandlerInterceptors and Filters behave with async request processing (Callable/DeferredResult), and what subtle pitfalls arise around postHandle, thread-locals, and dispatch types?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

When a controller returns a Callable or DeferredResult, Spring releases the request thread. For interceptors that means postHandle/afterCompletion are deferred until the async result is re-dispatched; AsyncHandlerInterceptor.afterConcurrentHandlingStarted is called instead on the first pass. Thread-locals set on the original thread don't exist on the async worker thread, so filter/interceptor state must be propagated explicitly.

open as a page