Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6Kg9RWSTJzyz6KP9X
filter-interceptor-aop
Companion module for Filter vs HandlerInterceptor vs AOP in Spring Boot 4: Which Runs When
on ankurm.com. A servlet Filter, a Spring MVC HandlerInterceptor and an @Aspect all wrap a request; this module
prints the order they really run in, on a real Tomcat and under MockMvc, and then breaks the assumptions around it
(errors, 404, static files, async, a body read twice, Spring Security).
mvn test regenerates every transcript in output/ (19 tests). Each test writes its transcript and asserts the same
facts, so a number quoted in the article is an assertion that fails the build when it stops being true.
Versions
| Spring Boot | 4.1.2 |
| Spring Framework | 7.0.10 |
| Spring Security | 7.1.2 |
| Tomcat (embedded) | 11.0.27 |
| AspectJ weaver | 1.9.25.1 |
| JDK | Eclipse Temurin 25.0.5 (LTS) |
The AOP starter is spring-boot-starter-aspectj. spring-boot-starter-aop has no 4.x release (its last version is 3.5.16).
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn test # 19 tests, rewrites output/01..19
./scripts/run.sh # app on :8080
curl localhost:8080/diag/filters # Tomcat's real filter chain, in order
./scripts/run.sh security # same app behind Spring Security 7 (HTTP Basic demo:demo)
./scripts/run-all.sh # tests + the scripts behind output/20, 21 (Maven Central) and 22 (jars in ~/.m2)
What is where
| Package | Contents |
|---|---|
trace |
Trace: every component writes one line here, in the order it really runs (delete before shipping) |
filter |
RawFilter (plain Filter, @Order(5)), AccessLogFilter (OncePerRequestFilter through a FilterRegistrationBean, order 10), ApiKeyFilter (auth, 20), VisibilityFilter (25), BodyFilter (request/response wrappers, 30), FaultFilter (throws, 40) |
interceptor |
TraceInterceptor (A and B), GateInterceptor (returns false), RoleInterceptor (reads @RequiresRole), TimingInterceptor, LateHeaderInterceptor, VisibilityInterceptor, WebConfig |
aop |
ControllerAspect, ServiceAspect, VisibilityAspect |
service |
GreetingService (self-invocation, private method, new Helper()), FixedGreetingService (the @Lazy self-reference fix) |
web |
HelloController, ApiExceptionHandler, DiagController (/diag/filters), TracingDispatcherServlet |
security |
SecurityDemoConfig, only with the security profile |
Endpoints
| Endpoint | Shows |
|---|---|
GET /api/hello/{name} |
the full happy-path order |
GET /api/boom, /api/service-boom, /api/unhandled |
handled, handled-from-service and unhandled exceptions |
POST /api/echo + X-Body-Mode: consume|cache|consume-wrapped|replay |
the request body read twice |
GET /api/hello/{name} + X-Resp-Mode: copy|forget |
ContentCachingResponseWrapper and copyBodyToResponse() |
GET /api/secure/data + X-Api-Key: demo-key |
a filter rejecting a request |
GET /api/admin/report + X-Role: admin |
an interceptor reading an annotation |
GET /api/gated/hello?mode=write|silent |
preHandle returning false |
GET /api/async-callable, /api/async-deferred |
async dispatch |
GET /api/late/body |
a header added in postHandle |
GET /api/visibility/{id} |
what each layer can see |
GET /api/twice/{name}, /api/private/{name}, /api/helper/{name}, /api/twice-fixed/{name} |
what an aspect does not see |
Index of captured output
| File | Contents |
|---|---|
01-versions-and-jars.txt |
versions, the AOP starter's pom, jars, Framework 7 API facts read by reflection |
02-filter-chain-order.txt |
Tomcat's real filter chain and dispatcher types |
03-happy-path.txt |
one request, every layer, in order |
04-exceptions.txt |
handled, service, unhandled and filter exceptions |
05-static-404-error-dispatch.txt |
static resource, 404, 405 and the ERROR dispatch |
06-short-circuits.txt |
filter 401, interceptor 403, preHandle false |
07-async.txt |
Callable and DeferredResult, with threads |
08-request-body.txt |
the body read twice, five ways, and the response wrapper |
09-visibility.txt |
what a filter, an interceptor and an aspect can see |
10-timing.txt |
elapsed time per layer (indicative) |
11-late-header.txt |
a header set after the response is committed |
12-no-handler.txt |
spring.web.resources.add-mappings=false |
13-handler-cast.txt |
the unchecked HandlerMethod cast |
14-path-patterns.txt |
which addPathPatterns patterns match which URLs |
15-aop-traps.txt |
self-invocation, private method, new, and the fix |
16-security-order.txt |
Spring Security's chain among the servlet filters |
17-security-double-registration.txt |
a filter bean that runs twice |
18-security-double-registration-fixed.txt |
the same, fixed |
19-mockmvc-vs-server.txt |
the same requests through MockMvc and Tomcat |
20-central-versions.txt |
Maven Central metadata (script) |
21-framework-6.2-vs-7.0.txt |
javap of the interceptor and path-matching API in 6.2.19 vs 7.0.10 (script) |
22-boot-bytecode-facts.txt |
bytecode facts: default filter dispatcher types, the dispatcherServlet back-off, the Security filter order constant, TestDispatcherServlet (script) |
Timing in 10-timing.txt varies between runs; everything else is identical on a rerun.
Thread names in 07-async.txt are relabelled (tomcat-1, mvc-task-1) so the file is reproducible.