Skip to content
Spring Boot sb web 3 min read

Servlet Filters

A filter is a servlet-container component that wraps every request before and after it reaches your application. Filters operate on the raw HttpServletRequest/HttpServletResponse and can modify, block, or pass them along the chain. In Spring Boot 3 they use the jakarta.servlet API. This page covers writing filters, OncePerRequestFilter, registration, ordering, and how filters compare with interceptors.

A basic filter

Implement jakarta.servlet.Filter and call chain.doFilter(...) to pass control onward. Skipping that call short-circuits the request.

import jakarta.servlet.*;
import java.io.IOException;

public class TimingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        long start = System.currentTimeMillis();
        try {
            chain.doFilter(req, res);              // continue down the chain
        } finally {
            long took = System.currentTimeMillis() - start;
            System.out.println("Handled in " + took + " ms");
        }
    }
}

OncePerRequestFilter

Spring’s OncePerRequestFilter guarantees a single execution per request even across forwards/includes, and offers the typed HttpServletRequest/HttpServletResponse directly. Prefer it for almost all filters.

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.*;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;

public class CorrelationIdFilter extends OncePerRequestFilter {

    public static final String HEADER = "X-Correlation-Id";

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String id = req.getHeader(HEADER);
        if (id == null || id.isBlank()) id = UUID.randomUUID().toString();
        res.setHeader(HEADER, id);
        MDC.put("correlationId", id);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.remove("correlationId");
        }
    }
}

Registration via @Component

The simplest registration is to annotate the filter as a @Component. Spring auto-registers it for all requests; control relative order with @Order.

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class CorrelationIdFilter extends OncePerRequestFilter { ... }

Note: With @Component, the filter maps to /* (every request). Use FilterRegistrationBean when you need to limit URL patterns or set precise ordering.

Registration via FilterRegistrationBean

FilterRegistrationBean gives full control over URL patterns, order, and the filter name.

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

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<CorrelationIdFilter> correlationIdFilter() {
        FilterRegistrationBean<CorrelationIdFilter> reg = new FilterRegistrationBean<>();
        reg.setFilter(new CorrelationIdFilter());
        reg.addUrlPatterns("/api/*");
        reg.setOrder(1);
        reg.setName("correlationIdFilter");
        return reg;
    }
}

Tip: If you register a filter through FilterRegistrationBean, do not also annotate it @Component, or it registers twice. Pick one mechanism.

Ordering

Filters run in ascending order — the lowest @Order / setOrder value runs first on the way in and last on the way out. A correlation-id filter should run very early (low order); a response-compression filter usually runs late.

@Bean
public FilterRegistrationBean<AuthFilter> authFilter() {
    FilterRegistrationBean<AuthFilter> reg = new FilterRegistrationBean<>();
    reg.setFilter(new AuthFilter());
    reg.setOrder(2);   // runs after the correlation-id filter (order 1)
    return reg;
}

Output (console for one request):

[correlationId=8f1c...] CorrelationIdFilter: in
[correlationId=8f1c...] AuthFilter: in
[correlationId=8f1c...] AuthFilter: out
[correlationId=8f1c...] CorrelationIdFilter: out

Filter vs interceptor

Both wrap request processing, but at different layers. See Interceptors for the dispatcher-level alternative.

AspectFilterInterceptor
LayerServlet containerSpring MVC dispatcher
APIjakarta.servlet.FilterHandlerInterceptor
Sees the matched handler?NoYes
Can wrap request/response streamYesNo
Runs for static resources / errorsYesNo
Registration@Component / FilterRegistrationBeanWebMvcConfigurer
Spring DI availableYesYes

Use a filter for cross-cutting concerns that touch the raw stream or must apply to every request (correlation ids, request/response wrapping, gzip, security). Use an interceptor when you need MVC-handler awareness.

Pitfalls

  • Forgetting to call chain.doFilter silently drops the request — nothing reaches your controller.
  • Registering a filter both as @Component and via FilterRegistrationBean runs it twice.
  • Reading the request body in a filter consumes the stream; wrap with ContentCachingRequestWrapper if the controller also needs it.
Last updated June 13, 2026
Was this helpful?