Filter vs Inerceptor in Spring

TL;DR

Aspect Servlet Filter Spring MVC Interceptor
Layer Servlet container (before DispatcherServlet) Spring MVC (around handler/controller)
Typical uses CORS, security pre-checks, logging, request/response wrapping, compression Auth/ACL per handler, auditing with handler info, adding model attrs, metrics
Scope Can apply to all URLs (incl. static, error pages) Applies to MVC handler mappings (controllers)
API javax.servlet.Filter / OncePerRequestFilter HandlerInterceptor
Order/Dispatch types @Order / FilterRegistrationBean, DispatcherType control Registered via WebMvcConfigurer#addInterceptors, path include/exclude
Body access Can wrap request/response (e.g., ContentCaching*) Not for rewriting body; mainly metadata & flow control
Async & errors Can surround chain.doFilter() and catch errors postHandle, afterCompletion, afterConcurrentHandlingStarted

Request flow: Filter(s) β†’ DispatcherServlet β†’ Interceptor.preHandle β†’ Controller β†’ Interceptor.postHandle β†’ view β†’ Interceptor.afterCompletion β†’ Filter(s).


When to pick which

  • Pick a Filter if you need cross-cutting behavior at the servlet level: CORS, security headers, request/response logging, wrapping the body, handling DispatcherType.ERROR, etc. (Spring Security is filter-based.)
  • Pick an Interceptor if behavior depends on the handler (e.g., which controller/method, annotations, path variables) or you want to add attributes to the model, or block a specific controller call.

Minimal examples

1) Logging Filter (runs once per request)

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // before others, if needed
public class RequestLoggingFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
      throws ServletException, IOException {
    long t0 = System.nanoTime();
    try { chain.doFilter(req, res); }
    finally {
      long ms = (System.nanoTime() - t0) / 1_000_000;
      logger.info("{} {} -> {} ({} ms)", req.getMethod(), req.getRequestURI(), res.getStatus(), ms);
    }
  }
}

Optional fine control (URL patterns, order, dispatcher types):

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;

@Bean
FilterRegistrationBean<RequestLoggingFilter> loggingFilterReg(RequestLoggingFilter filter) {
  var reg = new FilterRegistrationBean<>(filter);
  reg.addUrlPatterns("/*");
  reg.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
  reg.setOrder(Ordered.LOWEST_PRECEDENCE);
  return reg;
}

2) Auth Interceptor (blocks specific controller paths)

import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.*;

@Component
public class ApiKeyInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
    if (!(handler instanceof HandlerMethod)) return true; // skip non-controller resources
    String apiKey = req.getHeader("X-API-KEY");
    if (!"secret".equals(apiKey)) {
      res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing/invalid API key");
      return false; // stop invocation; controller won't run
    }
    return true;
  }
}

Register & scope it to routes:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {
  private final ApiKeyInterceptor apiKeyInterceptor;
  public WebConfig(ApiKeyInterceptor apiKeyInterceptor) { this.apiKeyInterceptor = apiKeyInterceptor; }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(apiKeyInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/public/**");
  }
}

Extra notes (production-friendly)

  • Reading request/response bodies for logging? Do it in a Filter with ContentCachingRequestWrapper / ContentCachingResponseWrapper to avoid consuming the stream.
  • Error pages & forwards: Filters can target DispatcherType.ERROR/INCLUDE/FORWARD; Interceptors won’t see error dispatches.
  • Async MVC: Interceptor has afterConcurrentHandlingStarted; Filters see async via DispatcherType.ASYNC.
  • Reactive stack (WebFlux): use WebFilter (instead of Filter) and HandlerFilterFunction (instead of Interceptor).

Quick decision recipe

  • Need handler method/annotation info? β†’ Interceptor
  • Need to affect all traffic or wrap/inspect raw requests/responses? β†’ Filter
  • Using Spring Security? β†’ Prefer security filters over custom interceptors.
Back to blog

Leave a comment