Foundation · Build

Filters vs Interceptors

Filters run in the servlet chain before Spring mapping; interceptors run inside MVC with access to the chosen handler—pick by what each concern needs to see.

Primary source: Spring MVC: HandlerInterceptor vs Servlet Filters

25 min

Lesson 8 of 11

1d streak

The Architectural Question

A Filter cannot see which handler method matched; reaching for MVC context inside filter code leads to hacks or dead ends.

What this guide proves

  • Filter
  • HandlerInterceptor
  • Execution Order

Servlet Filter vs HandlerInterceptor: The architectural boundary

Both Filters and Interceptors allow you to run cross-cutting logic before and after request processing. However, they live at fundamentally different layers of the Java web application stack and have access to completely different context.

DimensionServlet FilterSpring HandlerInterceptor
LayerServlet Container (Tomcat/Jetty)Spring MVC Framework
PositionBefore DispatcherServletInside DispatcherServlet dispatch loop
Handler AwarenessNo knowledge of Spring controller or methodReceives the target HandlerMethod object
Request AccessCan wrap or replace HttpServletRequestCannot replace request/response objects
Typical Use CasesSecurity, CORS, gzip compression, correlation IDsAuthorization checks, controller auditing, model timing

The once-only read trap and ContentCachingRequestWrapper

The servlet ServletInputStream is a streaming socket input: it can only be read once. If a Filter calls request.getInputStream() or request.getReader() to log the payload, subsequent calls by Spring’s MappingJackson2HttpMessageConverter fail with an empty stream error.

The stream exhaustion error

Logging raw request bodies in a Filter without wrapping causes HttpMessageNotReadableException: Required request body is missing in the controller.

To safely inspect request payloads in a Filter, wrap the request using Spring’s ContentCachingRequestWrapper before passing it down the filter chain.

Implementing a Correlation ID Filter and an Audit Interceptor

CorrelationIdFilter.java
package com.example.common.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.UUID;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter extends OncePerRequestFilter {
    public static final String CORRELATION_ID_HEADER = "X-Correlation-Id";
    public static final String MDC_KEY = "correlationId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String correlationId = request.getHeader(CORRELATION_ID_HEADER);
        if (correlationId == null || correlationId.isBlank()) {
            correlationId = UUID.randomUUID().toString();
        }
        MDC.put(MDC_KEY, correlationId);
        response.setHeader(CORRELATION_ID_HEADER, correlationId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove(MDC_KEY);
        }
    }
}

Trace complete execution order across the pipeline

  1. 01Filter.doFilter() executes before the request enters Spring MVC.
  2. 02DispatcherServlet matches the URL and retrieves the HandlerExecutionChain.
  3. 03HandlerInterceptor.preHandle() runs; returning false aborts the pipeline.
  4. 04Controller HandlerMethod executes and returns the response DTO.
  5. 05HandlerInterceptor.postHandle() runs before response writing completes.
  6. 06HandlerInterceptor.afterCompletion() executes always, even if an exception was thrown.
  7. 07Filter execution unwinds back up through finally blocks.

Active Recall Checkpoint

Where should you place authentication and IP rate limiting: in a Filter or an Interceptor?

Hint: Think about rejecting malicious traffic before it reaches Spring MVC dispatch.

Show answer & explanation

In a Servlet Filter. Rejecting unauthenticated or rate-limited requests early in the servlet chain avoids unnecessary DispatcherServlet routing, argument resolution, and Spring context execution overhead.

Decision rules for Filters vs Interceptors

Use it when

Use a Filter for request-wrapping, compression, protocol headers, correlation IDs, and security enforcement.

Reconsider when

Use a HandlerInterceptor when logic requires inspecting the target controller class or annotations (e.g. checking @AuditLog on the method).

Runtime Architecture & Flow

Decision canvas

Evaluate the choice across three constraints

These are comparison criteria, not sequential steps. A strong answer states which constraint dominates in the current system.

CRITERION 01

Filter

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 02

HandlerInterceptor

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 03

Execution Order

Ask how each option changes this guarantee, its operational cost, and its failure mode.

Decision prompt: Implement a `Filter` to log raw HTTP request times, and an `Interceptor` to check if a specific user header is present.

Working Implementation

Code Walkthrough

Implement a `Filter` to log raw HTTP request times, and an `Interceptor` to check if a specific user header is present.

FiltersInterceptorsLab.java
package com.example.filterinterceptor;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.util.UUID;

public class FiltersInterceptorsLab {

    // 1. Servlet Filter: executes outside Spring MVC at servlet container boundary
    @Component
    public static class CorrelationFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            String correlationId = request.getHeader("X-Correlation-ID");
            if (correlationId == null || correlationId.isBlank()) {
                correlationId = UUID.randomUUID().toString();
            }
            MDC.put("correlationId", correlationId);
            response.setHeader("X-Correlation-ID", correlationId);
            try {
                filterChain.doFilter(request, response);
            } finally {
                MDC.clear(); // Always clean up thread locals!
            }
        }
    }

    // 2. Spring HandlerInterceptor: executes inside Spring MVC around controller handler methods
    @Component
    public static class ExecutionTimeInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            request.setAttribute("startTime", System.currentTimeMillis());
            return true;
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            long startTime = (Long) request.getAttribute("startTime");
            long duration = System.currentTimeMillis() - startTime;
            response.setHeader("X-Response-Time-Ms", String.valueOf(duration));
        }
    }
}
OncePerRequestFilter runs at the Servlet container boundary, ideal for MDC logging and security headers.
HandlerInterceptor runs within Spring MVC with access to the handler Object and ModelAndView.
Always clear ThreadLocal/MDC values in finally blocks to prevent thread pool memory leaks.

Synthesis & Production Verification

Production Acceptance Criteria

  • Implement one Filter and one HandlerInterceptor on the same route
  • Order their log lines relative to controller execution
  • Justify placement choices for timing, authentication, and body inspection

Architectural & Technical Review

Core Interview Questions

Q01When should cross-cutting logic be implemented in a Filter versus an Interceptor?Show answer

Use Filters for protocol concerns, request stream wrapping, CORS, and authentication. Use Interceptors when logic requires inspecting the target controller class or annotations.

Q02Why does reading HttpServletRequest.getInputStream() in a Filter break controllers?Show answer

The servlet input stream can only be read once. Reading it in a Filter exhausts the stream unless wrapped with ContentCachingRequestWrapper.

Q03In what order do interceptor callbacks execute during request processing?Show answer

preHandle runs before the controller, postHandle runs after the controller before view resolution, and afterCompletion runs always after response completion.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Logging raw request payloads without caching wrappers — Directly consuming request streams causes HttpMessageNotReadableException when Spring attempts to deserialize the JSON body.
  • Pitfall: Placing security checks in Interceptors instead of Filters — Interceptors run after Spring MVC dispatching and argument resolution; unauthenticated traffic should be rejected at the Filter layer.

Primary Documentation & Specifications

Supplementary Module Resources