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). UseFilterRegistrationBeanwhen 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.
| Aspect | Filter | Interceptor |
|---|---|---|
| Layer | Servlet container | Spring MVC dispatcher |
| API | jakarta.servlet.Filter | HandlerInterceptor |
| Sees the matched handler? | No | Yes |
| Can wrap request/response stream | Yes | No |
| Runs for static resources / errors | Yes | No |
| Registration | @Component / FilterRegistrationBean | WebMvcConfigurer |
| Spring DI available | Yes | Yes |
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.doFiltersilently drops the request — nothing reaches your controller. - Registering a filter both as
@Componentand viaFilterRegistrationBeanruns it twice. - Reading the request body in a filter consumes the stream; wrap with
ContentCachingRequestWrapperif the controller also needs it.