Spring Boot: Filters & HandlerInterceptors
Filters and HandlerInterceptors are two mechanisms for intercepting HTTP requests in Spring Boot. Filters are part of the Servlet specification and run very early in the request pipeline. HandlerInterceptors are Spring-specific and run after the DispatcherServlet has selected a handler, giving them access to Spring MVC context.
Contents
Servlet Filter vs HandlerInterceptor
| Feature | Servlet Filter | HandlerInterceptor |
|---|---|---|
| Specification | Jakarta Servlet API | Spring MVC |
| Runs relative to DispatcherServlet | Before DispatcherServlet | After DispatcherServlet, around handler |
| Access to handler / controller | No | Yes (via HandlerMethod) |
| Access to ModelAndView | No | Yes (in postHandle) |
| Can short-circuit response | Yes | Yes (return false from preHandle) |
| URL pattern matching | Servlet-level patterns | Spring MVC patterns (Ant, regex) |
| Spring bean injection | Via @Component or registration bean | Full Spring bean |
Implementing a Filter
Extend OncePerRequestFilterjakarta.servlet.Filter
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component // auto-registered for all URLs; use FilterRegistrationBean for more control
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
log.info("--> {} {}", request.getMethod(), request.getRequestURI());
chain.doFilter(request, response); // continue filter chain
long duration = System.currentTimeMillis() - startTime;
log.info("<-- {} {} {} ({}ms)",
request.getMethod(), request.getRequestURI(),
response.getStatus(), duration);
}
}
Registering with FilterRegistrationBean
Use FilterRegistrationBean
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> registration =
new FilterRegistrationBean<>(new RequestLoggingFilter());
registration.addUrlPatterns("/api/*"); // apply only to /api/ paths
registration.setOrder(1); // lower number = higher priority
registration.setName("requestLoggingFilter");
return registration;
}
@Bean
public FilterRegistrationBean<CorrelationIdFilter> correlationFilter() {
FilterRegistrationBean<CorrelationIdFilter> registration =
new FilterRegistrationBean<>(new CorrelationIdFilter());
registration.addUrlPatterns("/*");
registration.setOrder(0); // runs before logging filter
return registration;
}
}
@ComponentFilterRegistrationBean@ComponentHandlerInterceptor (preHandle / postHandle / afterCompletion)
An interceptor can run code before the controller, after it, and after the response is complete.
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class AuditInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuditInterceptor.class);
/**
* Called before the handler method. Return false to abort the request.
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod hm) {
log.debug("Pre-handle: {}.{}",
hm.getBeanType().getSimpleName(), hm.getMethod().getName());
}
// Example: check for API key header
String apiKey = request.getHeader("X-API-Key");
if (apiKey == null || !isValidApiKey(apiKey)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API key");
return false; // abort — controller is NOT called
}
request.setAttribute("startTime", System.currentTimeMillis());
return true; // continue processing
}
/**
* Called after the handler returns, before the view is rendered.
* Not called if preHandle returns false or an exception was thrown.
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// Modify model data here if needed
}
/**
* Called after the complete request is finished (view rendered or response committed).
* Always called if preHandle returned true, even on exceptions.
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
long duration = System.currentTimeMillis() - (Long) request.getAttribute("startTime");
log.info("{} {} completed in {}ms — status {}",
request.getMethod(), request.getRequestURI(), duration, response.getStatus());
if (ex != null) {
log.error("Request threw exception", ex);
}
}
}
Registering Interceptors with addInterceptors
Register interceptors in a WebMvcConfigurer
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final AuditInterceptor auditInterceptor;
private final RateLimitInterceptor rateLimitInterceptor;
public WebMvcConfig(AuditInterceptor auditInterceptor,
RateLimitInterceptor rateLimitInterceptor) {
this.auditInterceptor = auditInterceptor;
this.rateLimitInterceptor = rateLimitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(auditInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/health", "/api/public/**")
.order(1);
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.order(2);
}
}
Use Cases (Logging, Auth, Rate Limiting)
Typical filters add a correlation ID to every request, check authentication or limit request rates.
// Request correlation ID filter
public class CorrelationIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String correlationId = Optional.ofNullable(req.getHeader("X-Correlation-ID"))
.orElse(UUID.randomUUID().toString());
MDC.put("correlationId", correlationId);
res.setHeader("X-Correlation-ID", correlationId);
try {
chain.doFilter(req, res);
} finally {
MDC.remove("correlationId");
}
}
}
// Simple in-memory rate limiter interceptor (per IP, 100 requests / minute)
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res,
Object handler) throws Exception {
String ip = req.getRemoteAddr();
int count = requestCounts.computeIfAbsent(ip, k -> new AtomicInteger(0))
.incrementAndGet();
if (count > 100) {
res.setStatus(429); // Too Many Requests
res.getWriter().write("{\"error\":\"Rate limit exceeded\"}");
return false;
}
return true;
}
}