The full picture
| Filter | Interceptor | AOP aspect | |
|---|---|---|---|
| Layer | Servlet container | Spring MVC (inside DispatcherServlet) | Any bean method |
| Knows the matched handler? | ❌ | ✅ (HandlerMethod, annotations) | n/a — knows the method |
| Can wrap request/response body | ✅ (wrappers) | ❌ (response may be committed) | ❌ |
| Sees non-MVC traffic (static, /error) | ✅ | mapping-dependent | ❌ |
| Typical residents | Security chain, CORS, gzip, MDC/correlation | tenant context, deprecation headers, per-handler metrics | transactions, retries, caching, domain audit |
- Interceptor hooks:
preHandle(veto-capable),postHandle(before view render; useless for @ResponseBody bodies — already written),afterCompletion(finally — cleanup, timing). - Order story end-to-end: container filters → security filter chain → DispatcherServlet → interceptors(pre) → argument resolvers → controller → advice(on error) → interceptors(after) → filters unwind. Being able to narrate this is the senior answer.
- Caching request bodies for logging requires
ContentCachingRequestWrapperin a filter — bodies are one-shot streams; naive logging breaks deserialization downstream. - Exceptions in filters bypass @ControllerAdvice (previous question's warning) — keep filter logic minimal and fail into well-defined responses.