Why this matters
- Placing logic in the wrong layer (security in a controller, logging in a filter) creates maintenance nightmares.
- Filters run before Spring MVC; interceptors run after handler mapping — knowing the order prevents duplicate processing.
- The middleware stack is where correlation IDs, request logging, and authentication happen.
The request pipeline
Layers from outer to inner
- Servlet Filter — Servlet spec; runs before Spring. Compression, CORS headers, request logging.
- Security Filter Chain — Spring Security; authentication and authorization.
- DispatcherServlet — Spring MVC entry point; maps URL to handler.
- HandlerInterceptor — Pre/post/after-completion around controller execution.
- Controller — Your BookStore endpoint method.
Servlet filter
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String CORRELATION_HEADER = "X-Correlation-Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String correlationId = request.getHeader(CORRELATION_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
response.setHeader(CORRELATION_HEADER, correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("correlationId");
}
}
}
OncePerRequestFilter guarantees one execution per request, even with forwards.
Handler interceptor
@Component
public class RequestTimingInterceptor implements HandlerInterceptor {
private static final String START_TIME = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute(START_TIME, System.currentTimeMillis());
return true; // continue processing
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long start = (long) request.getAttribute(START_TIME);
long elapsed = System.currentTimeMillis() - start;
log.info("{} {} completed in {}ms",
request.getMethod(), request.getRequestURI(), elapsed);
}
}
Register the interceptor:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestTimingInterceptor())
.addPathPatterns("/api/**");
}
}
Filter vs interceptor
| Feature | Servlet Filter | HandlerInterceptor |
|---|---|---|
| Spec | Servlet (Jakarta) | Spring MVC |
| Runs before | DispatcherServlet | Handler mapping |
| Has access to | Raw request/response | Handler method, ModelAndView |
| Use for | Encoding, correlation IDs | Timing, auth checks, model attributes |
| Registered via | @Component or FilterRegistrationBean | WebMvcConfigurer.addInterceptors() |
Spec
Servlet FilterServlet (Jakarta)HandlerInterceptorSpring MVCRuns before
Servlet FilterDispatcherServletHandlerInterceptorHandler mappingHas access to
Servlet FilterRaw request/responseHandlerInterceptorHandler method, ModelAndViewUse for
Servlet FilterEncoding, correlation IDsHandlerInterceptorTiming, auth checks, model attributesRegistered via
Servlet Filter@Component or FilterRegistrationBeanHandlerInterceptorWebMvcConfigurer.addInterceptors()
Filter ordering
When multiple filters exist, order matters:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<CorrelationIdFilter> correlationFilter() {
FilterRegistrationBean<CorrelationIdFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new CorrelationIdFilter());
bean.addUrlPatterns("/api/*");
bean.setOrder(1); // runs first
return bean;
}
}
Spring Security filters have their own ordered chain (order ~-100). Custom filters typically run before or after security depending on purpose.
Security filter chain position
Spring Security installs ~15 filters in a specific order:
Default security filter order
SecurityContextPersistenceFilter— Load/save security contextUsernamePasswordAuthenticationFilter— Form loginBearerTokenAuthenticationFilter— JWT validationAuthorizationFilter— Access decisionExceptionTranslationFilter— Convert auth exceptions to HTTP responses
Custom security filters slot into this chain via SecurityFilterChain configuration — covered in the security module.
@ControllerAdvice is not middleware
@ControllerAdvice handles exceptions and model attributes after the controller runs (or instead of it). It is not part of the inbound filter/interceptor chain — do not use it for request preprocessing.
Practical BookStore stack
| Concern | Layer | Implementation |
|---|---|---|
| Correlation ID | Servlet Filter | CorrelationIdFilter |
| Authentication | Security Filter | JWT bearer token filter |
| Request timing | Interceptor | RequestTimingInterceptor |
| Exception mapping | ControllerAdvice | BookStoreExceptionHandler |
| Audit logging | AOP Aspect | @Audited annotation |
Correlation ID
LayerServlet FilterImplementationCorrelationIdFilterAuthentication
LayerSecurity FilterImplementationJWT bearer token filterRequest timing
LayerInterceptorImplementationRequestTimingInterceptorException mapping
LayerControllerAdviceImplementationBookStoreExceptionHandlerAudit logging
LayerAOP AspectImplementation@Audited annotation
Quick recall
Everything you need if you only revisit this box.
- Request flow: Filter → Security Chain → DispatcherServlet → Interceptor → Controller.
- Servlet filters operate on raw HTTP; interceptors operate on mapped handler methods.
OncePerRequestFilterprevents duplicate filter execution on forwards.- Register interceptors via
WebMvcConfigurer.addInterceptors(). - Use filters for universal concerns (correlation IDs); interceptors for MVC-specific ones (timing).
@ControllerAdvicehandles exceptions, not inbound request preprocessing.
Test yourself
Answer these before moving on — recall is what makes it stick.