DEV Community

Ankit Verma
Ankit Verma

Posted on

Filters vs Interceptors vs AOP โ€” when each

๐Ÿง  The big idea in one line

Filters, interceptors, and aspects are three places to run code around a request โ€” at three different depths of the stack.

  • Every web app has work that isn't the "real" job of any single endpoint: logging, timing, auth checks, adding headers, collecting metrics.
  • Copying that work into every controller method is a mess. You repeat it everywhere, and you forget it somewhere.
  • A cross-cutting concern is a job that applies to many requests or many methods โ€” it cuts across your normal code instead of belonging to one spot.
  • Spring gives you three tools to handle these in one place. They differ by where in the request's journey they sit, and how much they can see.
  • You meet this the first time you need "run this before every request" โ€” and immediately hit the real question: which of the three?

๐Ÿ›ฃ๏ธ First, the journey of one request

Before comparing the tools, you need to see the path a request takes. It does not land on your controller directly.

  • Servlet container (Tomcat, Jetty): the web server that speaks HTTP and hands your app a raw HttpServletRequest.
  • DispatcherServlet: Spring's single front-door servlet. It reads the URL, picks the right controller method, calls it, and turns the return value into a response.
  • Handler: the specific controller method chosen to serve this request.

Here is where each tool sits, from outside in:

HTTP request
  โ”‚
  โ–ผ
โ–“ Servlet Filter            โ† outermost ยท raw HTTP ยท no idea which controller
  โ”‚
  โ–ผ
  โ–“ Handler Interceptor     โ† inside Spring MVC ยท knows the handler
    โ”‚
    โ–ผ
    โ–“ Controller method
      โ”‚  (calls a service bean)
      โ–ผ
      โ–“ AOP Aspect          โ† wraps any bean method ยท fires for non-web calls too
Enter fullscreen mode Exit fullscreen mode
  • The order is the whole insight: filters sit outermost, interceptors sit inside the DispatcherServlet, aspects sit deepest โ€” around your bean methods.
  • As you move inward, each layer sees more Spring detail and less raw HTTP.

๐Ÿงฑ Layer 1 โ€” Servlet Filters (the outer wall)

  • A Filter is part of the Servlet spec โ€” plain Jakarta/Java EE, not Spring. It wraps the request before Spring even runs.
  • It sees the raw HttpServletRequest and HttpServletResponse, and nothing about which controller will run.
@Component
public class TimingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        long start = System.currentTimeMillis();
        chain.doFilter(req, res);   // hand off to the next filter, then the app
        long took = System.currentTimeMillis() - start;
        System.out.println("Request took " + took + "ms");
    }
}
Enter fullscreen mode Exit fullscreen mode
  • chain.doFilter(...) is the pivot. Everything before it runs on the way in; everything after it runs on the way out.
  • If you never call chain.doFilter, the request stops right here. That is how a filter blocks a request entirely โ€” for example, rejecting a missing API key before any Spring code runs.
  • It holds both the request and the response, so it can even wrap or replace the response body.

Filters are good at:

  • Work that must happen for every HTTP request โ€” controller-bound or not (static files, error paths).
  • Reading or modifying the raw request/response (compression, CORS headers, caching the request body).
  • Security gates that should run before Spring. Spring Security itself is built as one big filter.

๐Ÿงญ Layer 2 โ€” HandlerInterceptors (inside Spring MVC)

  • A HandlerInterceptor runs inside the DispatcherServlet, after Spring has already decided which handler will serve the request.
  • So it knows the handler โ€” a filter never does.

It gives you three hooks around the controller call:

Hook When it runs Typical use
preHandle before the controller method auth check; start a timer; return false to block
postHandle after the controller, before the view renders tweak the model or response
afterCompletion after everything, even when the handler threw cleanup; stop a timer; log the exception
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        if (req.getHeader("X-User") == null) {
            res.setStatus(401);
            return false;          // stop: the controller never runs
        }
        return true;               // continue to the controller
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Returning false from preHandle stops the request โ€” the same idea as a filter that skips chain.doFilter, except you are already inside Spring.
  • You register it against URL patterns, and the handler argument lets you read the target method's annotations:
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/api/**");
    }
}
Enter fullscreen mode Exit fullscreen mode

Interceptors are good at:

  • Web concerns that need to know the controller or endpoint (per-route auth, request logging that names the method).
  • Anything tied to the MVC lifecycle, like touching the model before the view renders.

๐ŸŽฏ Layer 3 โ€” AOP / Aspects (around any bean method)

The first two tools only see web requests. But cross-cutting work often lives deeper โ€” inside services that are called from web endpoints and from scheduled jobs and from message listeners.

  • AOP (Aspect-Oriented Programming) lets you run code around any Spring bean method call, web or not.
  • How does it reach inside a method call? Spring wraps your bean in a proxy โ€” a stand-in object that looks identical to your bean but runs extra code before and after the real method.
caller โ”€โ”€โ–ถ [ Proxy ] โ”€โ”€โ–ถ your real bean method
               โ”‚
               โ””โ”€ runs "advice" before & after the real call
Enter fullscreen mode Exit fullscreen mode

Two terms fall out of that picture:

  • Advice: the extra code the aspect runs (before, after, or around the method).
  • Pointcut: the rule that picks which methods to wrap โ€” e.g. "every method in the service package".
@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.example.service..*(..))")
    public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();      // call the real method
        long took = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature() + " took " + took + "ms");
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • pjp.proceed() is the AOP version of chain.doFilter โ€” it runs the real method. Same in-and-out shape as a filter, but wrapped around a plain Java method instead of an HTTP request.
  • The pointcut execution(* com.example.service..*(..)) targets every method under the service package.

Aspects are good at:

  • Business-level cross-cutting: @Transactional, @Cacheable, retries, method-level security, a custom @Audited annotation.
  • Work that is not about HTTP at all โ€” it fires no matter who called the method.

โš ๏ธ Easy to confuse โ€” the proxy gotcha (self-invocation)

  • Because AOP works through a proxy, calling one method of a bean from another method of the same bean skips the proxy โ€” the advice does not run.
  • The reason: this.otherMethod() goes straight to the real object and never touches the wrapper.
  • This is the classic "@Transactional did nothing" bug: an internal call bypassed the proxy that would have opened the transaction.

๐Ÿงฉ Bad vs good: picking the wrong layer

โŒ Putting per-endpoint auth in a Filter

  • A filter cannot see which controller method will run, so you end up re-parsing the URL by hand to decide the rule. Fragile, and it duplicates Spring's own routing.

โœ… Use an interceptor (or method security) instead

  • It already knows the handler and its annotations, so the rule lives next to where the endpoint is defined.

โŒ Using AOP to set an HTTP response header

  • Your service method has no HttpServletResponse โ€” you would have to smuggle one in. Wrong layer.

โœ… Use a filter or interceptor

  • They hold the raw response and can set headers directly.

โš ๏ธ Easy to confuse โ€” quick separations

  • Filter vs Interceptor: both wrap web requests. A filter is at the servlet level, has no idea which controller runs, and fires even for non-Spring requests. An interceptor is inside Spring MVC, knows the handler, and only fires for requests the DispatcherServlet routes.
  • Interceptor vs Aspect: an interceptor is web-only and lifecycle-based (pre/post/after). An aspect targets any method by pointcut and fires for non-web calls too.
  • The "continue" call in each: chain.doFilter(), return true, and pjp.proceed() all mean "go on to the next thing." Skip any of them and the flow stops there.

๐Ÿ“Š Quick summary

Filter Interceptor Aspect (AOP)
Level Servlet container Spring MVC (DispatcherServlet) Any Spring bean method
Spec Servlet (Jakarta) Spring MVC Spring AOP
Sees raw request/response handler + request/response method args + return value
Knows the controller? โŒ no โœ… yes it is a bean method
Fires for non-web calls? โŒ no โŒ no โœ… yes
"Continue" call chain.doFilter() return true pjp.proceed()
Typical use CORS, compression, security gate per-endpoint auth, MVC logging @Transactional, caching, audit

๐ŸŽฏ Decision rule

  • Need it for every HTTP request โ€” even errors and static files โ€” or must it run before Spring? โ†’ Filter.
  • Need to know which controller/endpoint runs, or hook the MVC lifecycle? โ†’ Interceptor.
  • Need it around service/business methods, or for non-web calls too? โ†’ Aspect (AOP).
  • Rough guide: outer = HTTP plumbing, inner = business logic. Pick the outermost layer that still has everything you need.

๐Ÿ’ก Remember this

  • Three layers, outer to inner: Filter โ†’ Interceptor โ†’ Aspect. Each sees less raw HTTP and more Spring/business detail.
  • The "continue" step is the heart of all three: chain.doFilter / return true / proceed(). Skip it to stop the flow.
  • Only the filter runs without Spring MVC; only the aspect fires for non-web method calls.
  • AOP works through a proxy, so a self-invocation inside the same bean silently skips the advice.

Top comments (0)