Spring Boot: Filters & HandlerInterceptors

Filters and HandlerInterceptors are two mechanisms for intercepting HTTP requests in Spring Boot. Filters are part of the Servlet specification and run very early in the request pipeline. HandlerInterceptors are Spring-specific and run after the DispatcherServlet has selected a handler, giving them access to Spring MVC context.

Contents

Servlet Filter vs HandlerInterceptor

FeatureServlet FilterHandlerInterceptor
SpecificationJakarta Servlet APISpring MVC
Runs relative to DispatcherServletBefore DispatcherServletAfter DispatcherServlet, around handler
Access to handler / controllerNoYes (via HandlerMethod)
Access to ModelAndViewNoYes (in postHandle)
Can short-circuit responseYesYes (return false from preHandle)
URL pattern matchingServlet-level patternsSpring MVC patterns (Ant, regex)
Spring bean injectionVia @Component or registration beanFull Spring bean

Implementing a Filter

Extend OncePerRequestFilter (Spring helper that guarantees single execution per request) or implement jakarta.servlet.Filter directly.

Java
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component  // auto-registered for all URLs; use FilterRegistrationBean for more control
public class RequestLoggingFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();

        log.info("--> {} {}", request.getMethod(), request.getRequestURI());

        chain.doFilter(request, response); // continue filter chain

        long duration = System.currentTimeMillis() - startTime;
        log.info("<-- {} {} {} ({}ms)",
            request.getMethod(), request.getRequestURI(),
            response.getStatus(), duration);
    }
}

Registering with FilterRegistrationBean

Use FilterRegistrationBean when you need to control URL patterns, ordering, or avoid auto-registration.

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

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
        FilterRegistrationBean<RequestLoggingFilter> registration =
            new FilterRegistrationBean<>(new RequestLoggingFilter());

        registration.addUrlPatterns("/api/*"); // apply only to /api/ paths
        registration.setOrder(1);             // lower number = higher priority
        registration.setName("requestLoggingFilter");
        return registration;
    }

    @Bean
    public FilterRegistrationBean<CorrelationIdFilter> correlationFilter() {
        FilterRegistrationBean<CorrelationIdFilter> registration =
            new FilterRegistrationBean<>(new CorrelationIdFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(0); // runs before logging filter
        return registration;
    }
}
If you annotate a filter with @Component, it is registered for all URLs automatically. Use FilterRegistrationBean (without @Component) for URL-specific or ordered filters.

HandlerInterceptor (preHandle / postHandle / afterCompletion)

An interceptor can run code before the controller, after it, and after the response is complete.

Java
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class AuditInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(AuditInterceptor.class);

    /**
     * Called before the handler method. Return false to abort the request.
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        if (handler instanceof HandlerMethod hm) {
            log.debug("Pre-handle: {}.{}",
                hm.getBeanType().getSimpleName(), hm.getMethod().getName());
        }

        // Example: check for API key header
        String apiKey = request.getHeader("X-API-Key");
        if (apiKey == null || !isValidApiKey(apiKey)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key");
            return false; // abort — controller is NOT called
        }

        request.setAttribute("startTime", System.currentTimeMillis());
        return true; // continue processing
    }

    /**
     * Called after the handler returns, before the view is rendered.
     * Not called if preHandle returns false or an exception was thrown.
     */
    @Override
    public void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView) throws Exception {
        // Modify model data here if needed
    }

    /**
     * Called after the complete request is finished (view rendered or response committed).
     * Always called if preHandle returned true, even on exceptions.
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler,
                                 Exception ex) throws Exception {
        long duration = System.currentTimeMillis() - (Long) request.getAttribute("startTime");
        log.info("{} {} completed in {}ms — status {}",
            request.getMethod(), request.getRequestURI(), duration, response.getStatus());

        if (ex != null) {
            log.error("Request threw exception", ex);
        }
    }
}

Registering Interceptors with addInterceptors

Register interceptors in a WebMvcConfigurer and choose the paths they apply to.

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

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final AuditInterceptor auditInterceptor;
    private final RateLimitInterceptor rateLimitInterceptor;

    public WebMvcConfig(AuditInterceptor auditInterceptor,
                        RateLimitInterceptor rateLimitInterceptor) {
        this.auditInterceptor = auditInterceptor;
        this.rateLimitInterceptor = rateLimitInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(auditInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/health", "/api/public/**")
            .order(1);

        registry.addInterceptor(rateLimitInterceptor)
            .addPathPatterns("/api/**")
            .order(2);
    }
}

Use Cases (Logging, Auth, Rate Limiting)

Typical filters add a correlation ID to every request, check authentication or limit request rates.

Java
// Request correlation ID filter
public class CorrelationIdFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String correlationId = Optional.ofNullable(req.getHeader("X-Correlation-ID"))
            .orElse(UUID.randomUUID().toString());

        MDC.put("correlationId", correlationId);
        res.setHeader("X-Correlation-ID", correlationId);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.remove("correlationId");
        }
    }
}
Java
// Simple in-memory rate limiter interceptor (per IP, 100 requests / minute)
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res,
                              Object handler) throws Exception {
        String ip = req.getRemoteAddr();
        int count = requestCounts.computeIfAbsent(ip, k -> new AtomicInteger(0))
                                 .incrementAndGet();
        if (count > 100) {
            res.setStatus(429); // Too Many Requests
            res.getWriter().write("{\"error\":\"Rate limit exceeded\"}");
            return false;
        }
        return true;
    }
}