skip to content

How do you register and order Servlet Filters and HandlerInterceptors in a Spring Boot app, and what is OncePerRequestFilter for?

level: middleimportance: should knowfreq 46%

answer

  1. Interceptor: WebMvcConfigurer.addInterceptors + path patterns
  2. Filter: bean auto-reg OR FilterRegistrationBean
  3. Lower @Order/setOrder = earlier
  4. OncePerRequestFilter = one run per request across FORWARD/ASYNC/ERROR
  5. Double-registration gotcha: setEnabled(false)

basics

~20 s

Register interceptors by implementing WebMvcConfigurer.addInterceptors and calling registry.addInterceptor(...); order = registration order. Register filters as beans (Spring Boot auto-registers them) or via FilterRegistrationBean to control order and URL patterns. OncePerRequestFilter guarantees the filter runs once per request, even across forwards/async dispatches.

solid answer

~40 s

Interceptors: implement WebMvcConfigurer and override addInterceptors(InterceptorRegistry); call registry.addInterceptor(bean).addPathPatterns(...).excludePathPatterns(...).order(n). Without explicit order, they run in registration order. Filters: a Filter declared as a Spring bean is auto-registered by Spring Boot against all URLs, ordered by @Order/Ordered. For finer control — specific URL patterns, dispatcher types, an explicit order, or to disable auto-registration — wrap it in a FilterRegistrationBean and set setOrder/addUrlPatterns. Lower order value = earlier. @WebFilter + @ServletComponentScan is the Servlet-native alternative. OncePerRequestFilter is a Spring base class whose doFilterInternal is guaranteed to run exactly once per request, even when the container re-dispatches (FORWARD, INCLUDE, ASYNC, ERROR) — plain Filters can otherwise run multiple times per logical request. Filters always wrap interceptors regardless of order values, since they are at different layers.

code

java · 24 lines
java
// A robust filter: once per request, ordered early, scoped to /api
public class RequestIdFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String id = Optional.ofNullable(req.getHeader("X-Request-Id"))
                            .orElse(UUID.randomUUID().toString());
        MDC.put("requestId", id);
        res.setHeader("X-Request-Id", id);
        try {
            chain.doFilter(req, res);
        } finally {
            MDC.remove("requestId"); // runs once, even across async/error dispatches
        }
    }
}

@Bean
FilterRegistrationBean<RequestIdFilter> requestIdFilter() {
    var reg = new FilterRegistrationBean<>(new RequestIdFilter());
    reg.addUrlPatterns("/api/*");
    reg.setOrder(Ordered.HIGHEST_PRECEDENCE); // lower value = earlier
    return reg;
}

go deeper

for a junior

Know that interceptors register via WebMvcConfigurer and filters are just beans in Spring Boot.

for a middle

Explain FilterRegistrationBean for order/URL/dispatcher control and what OncePerRequestFilter guarantees.

for a senior

Cover the double-registration pitfall, dispatcher types, and that filter vs interceptor ordering are independent layers.

for a principal

Reasons about where to place tracing/auth in the ordering, opting in/out of async/error dispatch, and consistency with Spring Security's filter ordering.

## Registering a HandlerInterceptor Implement `WebMvcConfigurer` and override `addInterceptors`: ```java @Configuration public class WebConfig implements WebMvcConfigurer { private final AuditInterceptor audit; WebConfig(AuditInterceptor audit) { this.audit = audit; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(audit) .addPathPatterns("/api/**") .excludePathPatterns("/api/health") .order(0); } } ``` - **Path scoping** is built in: `addPathPatterns` / `excludePathPatterns` use `PathPattern` (or Ant patterns) — a big convenience filters lack natively (a filter must inspect the URL itself or use URL-pattern registration). - **Ordering**: interceptors run in the order added to the registry unless you call `.order(int)`; lower runs first. ## Registering a Servlet Filter (Spring Boot) There are three common ways: 1. **Bean auto-registration** — declare the filter as a `@Component`/`@Bean`. Spring Boot's `ServletContextInitializer` machinery registers it automatically, mapped to `/*`. Control relative order with `@Order` or by implementing `Ordered`. 2. **`FilterRegistrationBean`** — for full control: ```java @Bean public FilterRegistrationBean<RequestIdFilter> requestIdFilter() { var reg = new FilterRegistrationBean<>(new RequestIdFilter()); reg.addUrlPatterns("/api/*"); reg.setOrder(Ordered.HIGHEST_PRECEDENCE + 10); reg.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); return reg; } ``` Use this to set URL patterns, dispatcher types, init params, or `setEnabled(false)` to suppress the auto-registration of a filter bean. 3. **`@WebFilter` + `@ServletComponentScan`** — the Servlet-native annotation; ordering is not well defined this way, so prefer FilterRegistrationBean when order matters. ### Ordering caveat If you declare a filter as a bean **and** wrap it in a FilterRegistrationBean, you may get it registered twice. Fix by setting `setEnabled(false)` on the bean auto-registration or by not annotating it as a component. ## OncePerRequestFilter — why it exists `org.springframework.web.filter.OncePerRequestFilter` is an abstract base class. You override `doFilterInternal(...)` instead of `doFilter`. It guarantees your logic runs **exactly once per request**, even though the Servlet container can invoke the filter chain multiple times for a single logical request during **dispatches**: - `FORWARD` (e.g. `RequestDispatcher.forward`), - `INCLUDE`, - `ASYNC` (async re-dispatch after a `Callable`/`DeferredResult`), - `ERROR` (dispatch to the error page). Without `OncePerRequestFilter`, a plain filter might log, authenticate, or set headers **twice** (once for the original request, once for the error/forward dispatch). It uses a request attribute as a guard. This is why nearly all Spring-provided filters (and Spring Security's) extend it. You can override `shouldNotFilterAsyncDispatch()` / `shouldNotFilterErrorDispatch()` to opt in/out of specific dispatch types. ## Cross-layer ordering reality Filter order values and interceptor order values are **independent** — a filter always wraps all interceptors because it is at the container layer, regardless of the numbers you pick. Ordering values only rank filters among filters, and interceptors among interceptors.

  • Why might a plain Filter run twice for a single request, and how does OncePerRequestFilter fix it?
    The container re-dispatches the same request for FORWARD, INCLUDE, ASYNC, and ERROR dispatch types, and the filter chain is applied each time. OncePerRequestFilter records a request attribute the first time it runs and skips subsequent invocations, so doFilterInternal executes exactly once per logical request.
  • How do you scope a HandlerInterceptor to only /api/** but exclude /api/health?
    In addInterceptors: registry.addInterceptor(bean).addPathPatterns("/api/**").excludePathPatterns("/api/health"). Interceptors have built-in path matching; a filter would need URL-pattern registration or a manual URI check.

saying these in an interview costs you the question

  • Saying higher order value runs first (it's lower-first)
  • Claiming filters and interceptors share one ordering space
  • Not knowing filters can run multiple times per request
  • Thinking @WebFilter gives deterministic ordering
  • Registering a filter as both a bean and via FilterRegistrationBean and expecting single registration

context