Spring Boot Filters: Servlet Filters, OncePerRequestFilter, and FilterRegistrationBean Explained
Master Spring Boot filters with real-world examples.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ installed
- ✓Spring Boot 3.2+ project (Spring Initializr or existing)
- ✓Basic understanding of HTTP request/response lifecycle
• Filters intercept HTTP requests in Spring Boot before they reach controllers. • Use OncePerRequestFilter to guarantee single execution per request dispatch. • FilterRegistrationBean lets you set URL patterns and order explicitly. • Avoid common pitfalls like double execution with async dispatches or ordering conflicts. • Production incidents often stem from filter ordering or side effects in doFilter.
Think of filters like airport security checkpoints. Each passenger (HTTP request) goes through multiple checks (filters) before boarding (reaching the controller). Some checks are mandatory (OncePerRequestFilter ensures each passenger is checked exactly once, even if they change gates). FilterRegistrationBean is like the airport's security schedule—it decides which checkpoint runs first and which gates they cover.
In the trenches of Spring Boot 3.2.x, filters are your first line of defense and manipulation for incoming HTTP requests. While Spring Boot auto-configures many filters (like Spring Security's filter chain), you'll inevitably need custom logic—logging, authentication, request mutation, or rate limiting. The Servlet Filter API (javax.servlet/jakarta.servlet) is the bedrock, but Spring wraps it with OncePerRequestFilter to solve a subtle but critical issue: filter execution across multiple dispatch types (forward, include, error, async).
I've seen production outages caused by a filter logging request bodies twice because someone used a plain javax.servlet.Filter with async dispatches. The fix was switching to OncePerRequestFilter, which guarantees single execution per request lifecycle. Then there's FilterRegistrationBean—your escape hatch when auto-configuration fights you. Need to inject a filter before Spring Security's chain? Set order to Ordered.HIGHEST_PRECEDENCE. Want to exclude certain paths? Use setUrlPatterns with exclusion logic.
This article covers the gritty details: when to use each type, how to register them without XML (we're not in 2010), and how to debug filter ordering issues that cause mysterious 401s or double header writes. We'll use real code from a payment-processing system where filter ordering was the difference between a clean audit trail and a compliance nightmare.
Understanding the Servlet Filter Contract in Spring Boot 3.2
The Jakarta Servlet specification defines Filter as an interface with init(), doFilter(), and destroy(). In Spring Boot 3.2 (using Jakarta EE 10), you implement jakarta.servlet.Filter. The doFilter() method receives ServletRequest, ServletResponse, and FilterChain. You must call chain.doFilter() to pass the request downstream; failing to do so hangs the request.
Filters execute in a chain, ordered by their priority. Spring Boot auto-discovers filters as beans and wraps them in a FilterChainProxy. The order is determined by the Ordered interface or @Order annotation. Lower order values run first. Spring Security's FilterChainProxy typically runs at order Ordered.LOWEST_PRECEDENCE - 100, so your custom filters can run before or after by setting appropriate order values.
A critical detail: plain Servlet Filters execute once per dispatch type. If your filter is registered for REQUEST and FORWARD, it runs twice for a single user request that triggers an internal forward. This is where OncePerRequestFilter saves you.
What the Official Docs Won't Tell You
Spring's official documentation glosses over a painful truth: OncePerRequestFilter is not thread-safe by default. The base class uses an attribute on the request to track execution, but if you store mutable state in the filter instance itself (e.g., a counter or a cached object), you'll get race conditions under load. I debugged a case where a filter mutated a shared HashMap to track request IDs—within minutes under 500 req/s, we saw ConcurrentModificationException and corrupted data.
Another undocumented behavior: OncePerRequestFilter skips execution for ERROR dispatches by default. That means if your controller throws an exception and Spring's error page mechanism kicks in, the filter won't run again. This is usually desired, but if you need to log errors, you must override shouldNotFilterErrorDispatch() to return false.
Finally, FilterRegistrationBean's setUrlPatterns() accepts Ant-style patterns but does NOT support regex. Many developers try "/api/v[0-9]+/.*" and wonder why it fails. Use Ant patterns like "/api/v1/" or "/api/v2/" explicitly.
Registering Filters with FilterRegistrationBean
Before Spring Boot, you configured filters in web.xml. Now, you use FilterRegistrationBean in a @Configuration class. This gives you explicit control over URL patterns, servlet names, and order. Without it, Spring Boot auto-registers filter beans with default patterns (/*) and arbitrary order based on bean creation time.
FilterRegistrationBean accepts a Filter instance, setUrlPatterns(), setServletNames(), and setOrder(). The order is crucial: lower values run first. Spring Security's filter chain has order Ordered.LOWEST_PRECEDENCE - 100 (usually around Integer.MAX_VALUE - 100). To run before security, use order = Ordered.HIGHEST_PRECEDENCE (Integer.MIN_VALUE). To run after security, use order = Ordered.LOWEST_PRECEDENCE.
One pattern I use in every project: create a dedicated configuration class for filter registration. This avoids scattering bean definitions across multiple files. Also, use setDispatcherTypes() to specify which dispatches the filter applies to. By default, it's only REQUEST. Add FORWARD, ERROR, or ASYNC as needed.
OncePerRequestFilter: The Single-Execution Guarantee
OncePerRequestFilter is an abstract class in Spring that wraps a Servlet Filter and ensures doFilterInternal() is called only once per request dispatch cycle. It uses a request attribute ("org.springframework.web.filter.OncePerRequestFilter") to track execution. If you call chain.doFilter() and the request is forwarded, the filter checks the attribute and skips execution.
This is essential for filters that modify the response (like adding headers) or perform side effects (like logging). Without it, a forward would cause duplicate headers or double log entries. Spring Security's filters are all OncePerRequestFilter for this reason.
To implement, extend OncePerRequestFilter and override doFilterInternal(). You get HttpServletRequest and HttpServletResponse directly—no casting needed. You can also override shouldNotFilter() to skip certain requests based on path or method. Common use cases: adding correlation IDs, validating API keys, rate limiting, and response compression.
Filter Ordering: The Silent Killer in Production
Filter ordering is deceptively simple until it breaks. Spring Boot's auto-configuration registers filters in an order that depends on bean creation order, which is non-deterministic across restarts. I've seen a filter that set a request attribute run after a filter that read that attribute, causing NullPointerException in production.
The rule: always use FilterRegistrationBean with explicit setOrder(). Use constants from Ordered interface or define your own. Common patterns: - Ordered.HIGHEST_PRECEDENCE: For filters that must run before everything (e.g., tenant resolution, correlation ID). - Ordered.HIGHEST_PRECEDENCE + 100: For authentication/authorization filters. - Ordered.LOWEST_PRECEDENCE: For response-wrapping filters (e.g., compression, response logging).
Spring Security uses its own filter chain, which is a single filter in the servlet container's chain. Your custom filters can run before or after the security chain by setting order relative to SecurityProperties.DEFAULT_FILTER_ORDER (which is Ordered.LOWEST_PRECEDENCE - 100).
Advanced: Async Dispatch and Filter Execution
Spring Boot supports async request processing via DeferredResult or Callable. With async, the request thread is released, and the response is written later by a worker thread. Filters must handle ASYNC dispatches correctly. By default, OncePerRequestFilter does NOT run on ASYNC dispatches (shouldNotFilterAsyncDispatch() returns true). This is usually correct because you don't want to log or modify the response twice.
However, if your filter sets response headers that must be present on async responses (e.g., CORS headers), you need to override shouldNotFilterAsyncDispatch() to return false. But beware: the filter will execute on both the initial REQUEST dispatch and the ASYNC dispatch. You must ensure your filter logic is idempotent.
For plain Servlet Filters, you must register them with ASYNC dispatcher type via FilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC). Otherwise, they won't execute on async completions.
Testing Filters: Beyond Unit Tests
Unit testing a filter in isolation tells you nothing about ordering or dispatch type behavior. You need integration tests with MockMvc or TestRestTemplate. Use @WebMvcTest with your filters explicitly registered, or @SpringBootTest with a random port.
Key things to test: filter executes for expected paths, filter does NOT execute for excluded paths, filter sets correct headers/attributes, filter handles exceptions gracefully (should not swallow exceptions), and filter works with async dispatches.
Use MockHttpServletRequest and MockHttpServletResponse for unit tests of doFilterInternal(). But for ordering tests, use @SpringBootTest and inspect the FilterChainProxy bean.
Production Debugging: Filter Not Executing?
When a filter mysteriously doesn't execute, the cause is almost always one of these: wrong URL pattern, wrong order (another filter short-circuits the chain), or the filter is not registered as a bean. Use Spring Boot's actuator endpoints to inspect registered filters. Enable debug logging for org.springframework.boot.web.servlet.FilterRegistrationBean to see which filters are registered.
Another common issue: the filter throws an exception before calling chain.doFilter(), and the exception is swallowed by a parent filter (like Spring Security's ExceptionTranslationFilter). Always log exceptions in your filter's doFilterInternal().
Finally, if you're using Spring Security, your custom filter might be running inside the security filter chain, not the servlet container chain. To add a filter to the security chain, use HttpSecurity.addFilterBefore() or addFilterAfter(). This is different from FilterRegistrationBean.
The Double-Logged Payment: A Filter Ordering Disaster
- Always use OncePerRequestFilter unless you explicitly need multi-dispatch execution.
- Use FilterRegistrationBean to explicitly set order and URL patterns—don't rely on @Order alone.
- Test filter behavior with all dispatch types (REQUEST, FORWARD, INCLUDE, ERROR, ASYNC) in integration tests.
FilterRegistrationBean.setOrder() to enforce correct order. Log the attribute value before use.curl http://localhost:8080/actuator/mappings | jq '.filters'grep 'FilterRegistrationBean' logs/app.log | tail -20| File | Command / Code | Purpose |
|---|---|---|
| SimpleLoggingFilter.java | public class SimpleLoggingFilter implements Filter { | Understanding the Servlet Filter Contract in Spring Boot 3.2 |
| SafeOncePerRequestFilter.java | public class SafeOncePerRequestFilter extends OncePerRequestFilter { | What the Official Docs Won't Tell You |
| FilterConfig.java | @Configuration | Registering Filters with FilterRegistrationBean |
| CorrelationIdFilter.java | public class CorrelationIdFilter extends OncePerRequestFilter { | OncePerRequestFilter |
| OrderedFiltersConfig.java | @Configuration | Filter Ordering |
| AsyncAwareFilter.java | public class AsyncAwareFilter extends OncePerRequestFilter { | Advanced |
| FilterIntegrationTest.java | @WebMvcTest(controllers = SomeController.class) | Testing Filters |
| DebugFilterConfig.java | @Configuration | Production Debugging |
Key takeaways
Interview Questions on This Topic
What is the difference between a Servlet Filter and a Spring Interceptor?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't