Skip to content
Chapter 19 · Spring Boot

Filters vs Interceptors ​

Chapter 18 built a HandlerInterceptor that runs before a controller. A filter looks similar but sits elsewhere: in the servlet container, before the request even reaches Spring's DispatcherServlet. When both can read and change the request and response, which should you use? This chapter answers that in order: the one-line definitions, where each sits in the request's journey (and what a servlet is), when to use which, how to write and order several of each, and the exact order everything runs in.

Read the two definitions below now; they will make full sense by the end of the chapter.

One Line Definitions ​

ConceptDefinition
FilterIntercepts HTTP requests and responses before they reach the servlet
InterceptorSpecific to the Spring framework; intercepts HTTP requests and responses after the servlet is chosen but before they reach the controller

The Full Request Journey ​

When an HTTP request arrives at a Spring Boot application, it follows this path:

On the way back, the response passes the same layers in reverse: interceptor 2, interceptor 1, then filter 2, filter 1.

What Is a Servlet? ​

A servlet is a plain Java class that accepts an incoming request, processes it, and returns a response. A single application can have multiple servlets:

  • One servlet for REST APIs
  • One servlet for SOAP APIs
  • One servlet for file uploads
  • One servlet for static resources

Each servlet is registered for some URLs, and the container picks the servlet whose URLs match the request. In older monolithic applications you would find a servlet for REST APIs, one for SOAP, one for file uploads, each with its own routing code.

In a Spring Boot application this need is mostly gone. The DispatcherServlet, Spring's one servlet, is registered for /, which in servlet terms means the default servlet: every request no other servlet claims. It hands each request to the right controller. Spring Boot's startup log (with debug logging for ServletContextInitializerBeans) shows it:

text
Mapping servlets: dispatcherServlet urls=[/]

(Its URL can be changed with spring.mvc.servlet.path, for example to serve only /api/....)

Where Filters Fit ​

Filters exist inside the servlet container but before any specific servlet is chosen. Because they run at the container level, they are applied to every request regardless of which servlet will eventually handle it.

Where Interceptors Fit ​

Interceptors are a Spring framework concept. They exist inside the Dispatcher Servlet, between the servlet and the controller. They only intercept requests that the Dispatcher Servlet handles — they cannot intercept requests going to other servlets.


When to Use Each ​

Use CaseFilter or Interceptor?Reason
Spring Security (authentication/authorization)FilterMust apply to all requests, regardless of servlet
CORS headersFilterGeneric, cross servlet concern
Request logging for all APIsFilterServlet agnostic logging
Rate limiting at the network levelFilterApplied before any business logic
Authentication specific to your Spring controllersInterceptorSpecific to Dispatcher Servlet
Logging with Spring context (access to beans)InterceptorHas access to Spring's HandlerMethod
Caching logic tied to controller behaviorInterceptorNeeds knowledge of which controller will run

Rule of thumb:

  • Logic that is generic and must apply to all servlets → Filter
  • Logic that is specific to your Spring Boot application and its controllers → Interceptor

Creating a Filter ​

Filters implement Java's Filter interface and have three lifecycle methods:

java
// MyFilter.java
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Called once when the filter object is created
        // Use this for one-time initialization (e.g., loading config)
        System.out.println("MyFilter initialized");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // Called for every request matching this filter's URL pattern

        System.out.println("MyFilter: before processing request");

        // Pass request to the next filter in the chain (or to the servlet if no more filters)
        chain.doFilter(request, response);

        System.out.println("MyFilter: after processing response");
    }

    @Override
    public void destroy() {
        // Called once when the filter is removed from service
        System.out.println("MyFilter destroyed");
    }
}

Registering Filters with Ordering ​

java
// AppConfig.java
@Configuration
public class AppConfig {

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter1Registration() {
        FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MyFilter());
        registrationBean.addUrlPatterns("/*");   // applies to all URLs
        registrationBean.setOrder(1);            // lower number = higher priority
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<MySecondFilter> myFilter2Registration() {
        FilterRegistrationBean<MySecondFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MySecondFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(2);
        return registrationBean;
    }
}

The same startup log lists the filters, with their order:

text
Mapping filters: mySecondFilter urls=[/*] order=1, myFirstFilter urls=[/*] order=2, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter ...

Spring Boot registers some filters of its own (characterEncodingFilter, formContentFilter, and with Spring Security, the whole security filter chain).

Note: You can also just put @Component on a filter. It then applies to every URL (/*); you can still order it with @Order, but to limit it to certain URL patterns you need FilterRegistrationBean.


Creating an Interceptor ​

Interceptors implement HandlerInterceptor and have three methods:

java
// MyInterceptor.java
@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // Runs BEFORE the controller method
        // Return true  → continue processing
        // Return false → stop here; controller and subsequent interceptors are NOT called
        System.out.println("Interceptor 1: preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // Runs AFTER the controller method, but only if no exception was thrown
        System.out.println("Interceptor 1: postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // Runs ALWAYS after the complete request, even if an exception occurred
        // Similar to a finally block
        System.out.println("Interceptor 1: afterCompletion");
    }
}

Registering Interceptors with Ordering ​

java
// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor1;

    @Autowired
    private MySecondInterceptor myInterceptor2;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Order is determined by registration sequence
        registry.addInterceptor(myInterceptor1)
                .addPathPatterns("/api/**")         // apply to these URLs
                .excludePathPatterns("/api/public"); // exclude these URLs

        registry.addInterceptor(myInterceptor2)
                .addPathPatterns("/api/**");
    }
}

Request and Response Flow with Multiple Filters and Interceptors ​

With two filters and two interceptors, the complete execution order looks like this:

Incoming request:

  1. Filter1.doFilter — before chain.doFilter()
  2. Filter2.doFilter — before chain.doFilter()
  3. Interceptor1.preHandle
  4. Interceptor2.preHandle
  5. Controller method executes
  6. Interceptor2.postHandle
  7. Interceptor1.postHandle
  8. Interceptor2.afterCompletion
  9. Interceptor1.afterCompletion
  10. Filter2.doFilter — after chain.doFilter()
  11. Filter1.doFilter — after chain.doFilter()

A real run, with myFirstFilter registered with order 2 and mySecondFilter with order 1, and interceptors 1 and 2 registered in that sequence:

text
MyFilter2 inside
MyFilter1 inside
inside pre handle of interceptor 1
inside pre handle of interceptor 2
fetching the user details (controller)
inside post handle of interceptor 2
inside post handle of interceptor 1
inside after completion of interceptor 2
inside after completion of interceptor 1
MyFilter1 completed
MyFilter2 completed

Filters run in order value order (1 before 2), not in the order the beans were declared; interceptors run in registration order.

Key insight: The response path is the reverse of the request path. The last interceptor or filter to run on the way in is the first to run on the way out. For filters, "the way out" is simply the code after chain.doFilter(...), which runs when everything after it has returned.


postHandle vs afterCompletion ​

postHandleafterCompletion
When it runsAfter controller, but only on successAlways, even if an exception was thrown
Java equivalentNormal code after method callfinally block
Receives exception?NoYes (as parameter)
Use forModifying the model/response before renderingCleanup, resource release, guaranteed logging

preHandle Returning False ​

When preHandle returns false, the request processing stops immediately:

java
@Override
public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {
    String token = request.getHeader("Authorization");
    if (token == null || !isValid(token)) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return false; // stops everything — controller is never called
    }
    return true;
}

Only the interceptors whose preHandle already returned true get their afterCompletion called. With two interceptors where the second returns false, the log is:

Interceptor1 preHandle
Interceptor2 preHandle
Interceptor1 afterCompletion

If you do not set a status yourself, the client receives 200 OK with an empty body, so always set one (such as 401) before returning false.


Accessing Request and Response ​

Both filters and interceptors have access to the raw HttpServletRequest and HttpServletResponse. You can read headers, modify the response, add attributes, and more.

java
// Reading a header in a filter
String authHeader = ((HttpServletRequest) request).getHeader("Authorization");

// Adding a response header in an interceptor
response.addHeader("X-Request-Id", UUID.randomUUID().toString());

// Setting a request attribute to pass data to the controller
request.setAttribute("userId", parsedUserId);

Interceptors Have Access to Spring Context ​

One key advantage interceptors have over filters: they receive the handler object, which is the HandlerMethod representing the controller method that will be invoked. This gives you access to Spring's full context — including method annotations, parameter types, and the bean itself.

java
@Override
public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {
    if (handler instanceof HandlerMethod) {
        HandlerMethod method = (HandlerMethod) handler;
        // Check if this method has a specific annotation
        MyCustomAnnotation ann = method.getMethodAnnotation(MyCustomAnnotation.class);
        if (ann != null) {
            // act based on annotation value
        }
    }
    return true;
}

Filters do not have this capability — they have no knowledge of Spring's routing or annotation infrastructure.


Summary Comparison ​

DimensionFilterInterceptor
FrameworkJava Servlet API (Jakarta EE)Spring MVC
PositionBefore servlet assignmentAfter servlet, before controller
Applies toAll servletsOnly Dispatcher Servlet
Can short circuit?YesYes (preHandle returns false)
Access to Spring beansNot directlyYes
Access to handler methodNoYes
OrderingsetOrder() on FilterRegistrationBeanRegistration sequence
Use caseSecurity, CORS, logging at network levelApp specific logic, auth, caching

Next: Filters and interceptors act on requests. Chapter 20 turns to responses: HATEOAS adds links to a response that tell the client what it can do next.


Check yourself ​

  1. Where does a filter run, and where does an interceptor run?
  2. What is a servlet, and which servlet handles every request by default in Spring Boot?
  3. When would you choose a filter, and when an interceptor?
  4. Two filters have setOrder(2) and setOrder(1). Which runs first on the way in? On the way out?
  5. Two interceptors are registered in the order 1, 2. In what order do preHandle, postHandle and afterCompletion run?
  6. Interceptor 2's preHandle returns false. Which methods still run, and what does the client receive if you set nothing?
  7. Which Filter method must call chain.doFilter(...), and what happens if it does not?
  8. What can an interceptor see that a filter cannot?
Answers
  1. A filter runs in the servlet container, before the request reaches any servlet. An interceptor runs inside the DispatcherServlet, before the controller.
  2. A Java class that receives a request and returns a response. The DispatcherServlet, mapped to / (the default servlet: every request no other servlet claims).
  3. A filter for generic logic that must apply to every request whatever the servlet (security, CORS). An interceptor for logic specific to your Spring controllers.
  4. The one with order 1 first; on the way out it finishes last.
  5. preHandle 1, 2; then the controller; postHandle 2, 1; afterCompletion 2, 1.
  6. Interceptor 1's afterCompletion. The controller and all postHandles are skipped. The client gets 200 with an empty body.
  7. doFilter. Without it, the request stops there: later filters, the servlet and the controller never run.
  8. The handler: which controller method will run, with its annotations.