When implementing request logging or authentication checks in Spring Boot, you may wonder whether to use a Filter or an Interceptor. Both serve the same purpose of “inserting common processing before and after requests,” but they differ in execution timing and the information they can access.
This article organizes the technical differences between Filter and HandlerInterceptor, and provides selection criteria for common real-world cases such as authentication, logging, and CORS.
Conclusion First: Use These Criteria When in Doubt
Before diving into the details, here’s a summary of how to choose for common cases.
- Choose Filter: When you want to process requests before they reach Spring MVC / when you want to log all requests including 404s / when you want to re-read the request body (such as with ContentCachingRequestWrapper)
- Choose Interceptor: When you want to use annotation information on Controller methods / when you want fine-grained control over application scope per URL pattern / when you have many dependencies on Spring Beans
- When either works, prefer Interceptor: Due to better integration with the Spring ecosystem and easier testing
Below, we’ll examine the differences in execution timing, Spring DI management, and accessible information that form the basis for these criteria, with implementation samples for authentication, logging, CORS, exception handling, and more.
Basic Roles of Filter and Interceptor
Filter is a mechanism defined by the Java Servlet specification and operates at the servlet container level. On the other hand, Interceptor is a Spring MVC-specific mechanism that operates within the Spring context.
Both are means of implementing cross-cutting concerns by inserting common processing before and after requests, but since they operate at different layers, they need to be used appropriately.
Difference in Execution Timing
The most important difference between Filter and Interceptor is execution timing.
Request → Filter → DispatcherServlet → Interceptor → Controller → Interceptor → DispatcherServlet → Filter → Response
Since Filter runs before DispatcherServlet, it can modify requests before Spring MVC processing begins, or block requests before they reach Spring MVC.
Interceptor runs after DispatcherServlet receives the request, just before and after the Controller method is called. This allows it to access Controller-specific information.
Difference in Spring DI Container Management
Since Interceptor is a Spring-managed Bean, you can easily inject other Beans with @Autowired.
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService; // Bean injection is natural
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// Processing using userService
return true;
}
}
Filter can also have Beans injected by adding @Component, but it is originally something managed by the servlet container. Spring Boot automatically registers Filters as Beans, but if you want to control this explicitly, use FilterRegistrationBean.
Difference in Accessible Information
Filter can only access HttpServletRequest and HttpServletResponse.
Interceptor can access the HandlerMethod object, so it can retrieve information about the Controller method being called. Since it can handle annotation information and method parameter type information, more flexible processing is possible.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// Get method annotation information
if (method.isAnnotationPresent(RequireAuth.class)) {
// Authentication check processing
}
}
return true;
}
Basic Pattern for Filter Implementation
When implementing a Filter, it is common to extend OncePerRequestFilter. This guarantees that it runs only once per request.
@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.info("{} {} - {}ms", request.getMethod(), request.getRequestURI(), duration);
}
}
}
You can control execution order with the @Order annotation. The smaller the number, the earlier it runs.
Basic Pattern for Interceptor Implementation
Interceptor implements the HandlerInterceptor interface.
@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("Controller execution time: {}ms", duration);
}
}
To register an Interceptor, implement WebMvcConfigurer.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ExecutionTimeInterceptor executionTimeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(executionTimeInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
You can control the URL range to which the Interceptor applies with addPathPatterns and excludePathPatterns.
Implementing Request/Response Logging
If you want to log all requests, use a Filter. You can also record requests that did not reach DispatcherServlet (such as 404s).
If you want to log on a Controller method basis, Interceptor is more suitable. You can record information about which Controller method was called.
If you want to output the request body or response body to logs, use ContentCachingRequestWrapper and ContentCachingResponseWrapper.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
byte[] requestBody = wrappedRequest.getContentAsByteArray();
byte[] responseBody = wrappedResponse.getContentAsByteArray();
// Log output processing
wrappedResponse.copyBodyToResponse(); // Important: copy the response back
}
}
For detailed logging configuration, see Spring Boot Logging Configuration Guide.
Implementing Authentication Checks
If you are using Spring Security, the basic approach is to leave authentication processing to the SecurityFilterChain.
If you are implementing your own token authentication and not using Spring Security, implement it with a Filter. This is because authentication checks are needed for all requests.
If you want to check specific annotations before calling the Controller to verify permissions, Interceptor is convenient.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireAdmin annotation = handlerMethod.getMethodAnnotation(RequireAdmin.class);
if (annotation != null) {
// Admin permission check
if (!isAdmin(request)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
}
}
return true;
}
The basic usage of Spring Security is explained in Spring Boot Basic Authentication Implementation Tutorial.
Implementing CORS Configuration
For CORS configuration, you should use the standard method provided by Spring. Avoid implementing it with a Filter.
The simplest is the @CrossOrigin annotation.
@RestController
@CrossOrigin(origins = "http://localhost:3000")
public class ApiController {
// ...
}
If you want to configure it for the entire application, use WebMvcConfigurer.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
}
Implementing Exception Handling
Exceptions that occur within a Controller are basically handled with @ControllerAdvice. If you want to detect exceptions in an Interceptor, check the Exception parameter in the afterCompletion method.
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
if (ex != null) {
logger.error("Exception occurred during Controller execution", ex);
}
}
If an exception occurs within a Filter, handle it within that Filter or delegate to Spring’s ErrorController.
For detailed exception handling implementation patterns, see Spring Boot REST API Exception Handling.
Controlling Execution Order
When using multiple Filters, specify the order with the @Order annotation.
@Component
@Order(1)
public class FirstFilter extends OncePerRequestFilter { /* ... */ }
@Component
@Order(2)
public class SecondFilter extends OncePerRequestFilter { /* ... */ }
If you want to control it more explicitly, use FilterRegistrationBean.
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new RequestLoggingFilter());
registrationBean.setOrder(1);
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
}
The execution order of Interceptors is determined by the order in which addInterceptor is called.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(firstInterceptor); // Runs first
registry.addInterceptor(secondInterceptor); // Runs later
}
Criteria for Choosing
Here’s a summary of decision criteria when you’re unsure in practice.
Cases to choose Filter
- Processing is needed before reaching Spring MVC
- You want to process all requests (including 404s)
- You want to use servlet specification features (such as request wrappers)
Cases to choose Interceptor
- Controller method information is needed
- You want to do dynamic processing using annotation information
- There are many dependencies on Spring Beans
- You want fine-grained control over application scope by URL
When in doubt, it’s good to prefer Interceptor. It has high affinity with the Spring ecosystem and is easy to test.
If you need to implement more advanced cross-cutting concerns, also consider using AOP. It is explained in detail in What is Spring Boot AOP.
Summary
Filter and Interceptor differ in execution timing and the information they can access. Filter operates at the servlet level, while Interceptor operates at the Spring MVC level.
By choosing appropriately according to the feature you want to implement, such as authentication checks or request logging, you can write maintainable code. Basically, prefer Interceptor, and decide to use Filter when processing is needed before reaching DispatcherServlet — this will keep you from getting confused.