Appearance
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
| Concept | Definition |
|---|---|
| Filter | Intercepts HTTP requests and responses before they reach the servlet |
| Interceptor | Specific 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 Case | Filter or Interceptor? | Reason |
|---|---|---|
| Spring Security (authentication/authorization) | Filter | Must apply to all requests, regardless of servlet |
| CORS headers | Filter | Generic, cross servlet concern |
| Request logging for all APIs | Filter | Servlet agnostic logging |
| Rate limiting at the network level | Filter | Applied before any business logic |
| Authentication specific to your Spring controllers | Interceptor | Specific to Dispatcher Servlet |
| Logging with Spring context (access to beans) | Interceptor | Has access to Spring's HandlerMethod |
| Caching logic tied to controller behavior | Interceptor | Needs 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:
Filter1.doFilter— beforechain.doFilter()Filter2.doFilter— beforechain.doFilter()Interceptor1.preHandleInterceptor2.preHandle- Controller method executes
Interceptor2.postHandleInterceptor1.postHandleInterceptor2.afterCompletionInterceptor1.afterCompletionFilter2.doFilter— afterchain.doFilter()Filter1.doFilter— afterchain.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 completedFilters 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
postHandle | afterCompletion | |
|---|---|---|
| When it runs | After controller, but only on success | Always, even if an exception was thrown |
| Java equivalent | Normal code after method call | finally block |
| Receives exception? | No | Yes (as parameter) |
| Use for | Modifying the model/response before rendering | Cleanup, 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 afterCompletionIf 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
| Dimension | Filter | Interceptor |
|---|---|---|
| Framework | Java Servlet API (Jakarta EE) | Spring MVC |
| Position | Before servlet assignment | After servlet, before controller |
| Applies to | All servlets | Only Dispatcher Servlet |
| Can short circuit? | Yes | Yes (preHandle returns false) |
| Access to Spring beans | Not directly | Yes |
| Access to handler method | No | Yes |
| Ordering | setOrder() on FilterRegistrationBean | Registration sequence |
| Use case | Security, CORS, logging at network level | App 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
- Where does a filter run, and where does an interceptor run?
- What is a servlet, and which servlet handles every request by default in Spring Boot?
- When would you choose a filter, and when an interceptor?
- Two filters have
setOrder(2)andsetOrder(1). Which runs first on the way in? On the way out? - Two interceptors are registered in the order 1, 2. In what order do
preHandle,postHandleandafterCompletionrun? - Interceptor 2's
preHandlereturnsfalse. Which methods still run, and what does the client receive if you set nothing? - Which
Filtermethod must callchain.doFilter(...), and what happens if it does not? - What can an interceptor see that a filter cannot?
Answers
- A filter runs in the servlet container, before the request reaches any servlet. An interceptor runs inside the
DispatcherServlet, before the controller. - A Java class that receives a request and returns a response. The
DispatcherServlet, mapped to/(the default servlet: every request no other servlet claims). - 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.
- The one with order 1 first; on the way out it finishes last.
preHandle1, 2; then the controller;postHandle2, 1;afterCompletion2, 1.- Interceptor 1's
afterCompletion. The controller and allpostHandles are skipped. The client gets 200 with an empty body. doFilter. Without it, the request stops there: later filters, the servlet and the controller never run.- The handler: which controller method will run, with its annotations.