Spring Boot 中 Filter、Interceptor、AOP 到底谁先执行?
环境:Spring Boot 3.2.7 | JDK 21 | Servlet 6.0 | 所有日志输出均来自本地实际运行
引言
看到一个"诡异"的线上问题。
在项目里加了一个 Filter 用来记录每个请求的耗时,一个 Interceptor 用来校验请求头里的 token。逻辑很简单:token 不合法就返回 401,合法就放行。
测试的时候发现一个奇怪的现象------明明 Interceptor 已经返回 401 拦截了请求,日志里却仍然输出了 Filter 的"请求完成"记录,而且耗时统计里也包含了被拦截的请求。
"Filter 不是应该包在 Interceptor 外面吗?Interceptor 把请求拦住了,Filter 不是也该停下来吗?"
这个问题的答案,就是本文要讲的东西------Filter、Interceptor、AOP 三者的执行顺序,和你直觉想的不太一样。
本文会用一个 Spring Boot 3.2.7 的 Demo 把执行顺序逐行打印出来,让你亲眼看到每一步。完整代码已上传到 Git 仓库,你可以拉下来自己跑一遍。
环境信息
| 组件 | 版本 |
|---|---|
| Spring Boot | 3.2.7 |
| JDK | 21 |
| Servlet API | 6.0(Tomcat 10.1) |
| Spring AOP | 6.1.10 |
前置知识:三者各是什么
在进入 Demo 之前,用三句话定位各自的角色:
css
┌────────────────────────────────────────────────────┐
│ Servlet 容器 │
│ ┌──────────────────────────────────────────────┐ │
│ │ Filter 层 │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ DispatcherServlet │ │ │
│ │ │ ┌──────────────────────────────────┐ │ │ │
│ │ │ │ Interceptor 层 │ │ │ │
│ │ │ │ ┌────────────────────────────┐ │ │ │ │
│ │ │ │ │ AOP 层 │ │ │ │ │
│ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ Controller │ │ │ │ │ │
│ │ │ │ │ └──────────────────────┘ │ │ │ │ │
│ │ │ │ └────────────────────────────┘ │ │ │ │
│ │ │ └──────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
- Filter(过滤器):工作在 Servlet 容器层,在请求到达 DispatcherServlet 之前就执行了。它不管你要访问哪个 Controller、用哪个 Handler------只要 URL 匹配,它就走。
- Interceptor(拦截器):工作在 Spring MVC 层,由 DispatcherServlet 调度。它知道你要访问哪个 Handler(Controller 方法),可以在 Handler 执行前、执行后、视图渲染后三个时间点插入逻辑。
- AOP(切面):工作在 Spring Bean 层,代理方法调用。它比 Interceptor 更"内层"------如果 Interceptor 把请求拦住了,AOP 根本不会触发。
💡 一句话记:Filter 管"进不进这个应用",Interceptor 管"进不进这个 Controller",AOP 管"方法执行前后做什么"。
项目结构
先看 Demo 的整体结构:
python
spring-filter-interceptor/
├── pom.xml
└── src/main/java/com/coderplus/
├── Application.java # Spring Boot 入口
├── config/WebConfig.java # 注册 Interceptor
├── controller/OrderController.java # 订单接口(业务层)
├── filter/RequestLoggingFilter.java # 请求日志过滤器(Servlet 层)
├── interceptor/AuthInterceptor.java # 权限校验拦截器(Spring MVC 层)
└── aspect/LoggingAspect.java # 方法日志切面(Spring AOP 层)
四个关键组件,对应四个层次。
组件实现
1. Filter:记录请求耗时
java
@Component
@Order(1)
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String fullPath = req.getRequestURI()
+ (req.getQueryString() != null ? "?" + req.getQueryString() : "");
System.out.println("[Filter] BEFORE: " + fullPath);
long start = System.currentTimeMillis();
chain.doFilter(request, response); // ★ 调用才会继续往下走
long elapsed = System.currentTimeMillis() - start;
System.out.println("[Filter] AFTER : " + fullPath
+ " | 总耗时 " + elapsed + "ms");
}
}
Filter 的逻辑很简单------在 chain.doFilter() 之前和之后各打一行日志,记录请求耗时。关键点只有一行:chain.doFilter() 的调用,不调用它,整个请求链就断了。
2. Interceptor:校验 Token
java
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
System.out.println("[Interceptor] preHandle: token=" + token);
if (token == null || token.isBlank()) {
System.out.println("[Interceptor] preHandle → 返回 false,拦截请求");
response.setStatus(401);
return false; // ★ 拦截请求
}
System.out.println("[Interceptor] preHandle → 返回 true,放行");
return true; // ★ 放行
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
System.out.println("[Interceptor] postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
System.out.println("[Interceptor] afterCompletion"
+ (ex != null ? " | 异常: " + ex.getMessage() : ""));
}
}
三个方法的调用时机:
preHandle:Controller 方法执行前 。返回true放行,false拦截。postHandle:Controller 方法执行后 、视图渲染前 。只有preHandle返回true才会调用。afterCompletion:整个请求完成后 (视图渲染完毕)。同样只有preHandle返回true才会调用。
💡 关键规则 :如果
preHandle返回false,postHandle和afterCompletion都不会被调用。但 Filter 的 AFTER 阶段呢?答案在后面。
3. Interceptor 的注册
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截所有 /api/ 路径
.excludePathPatterns("/api/health"); // 排除健康检查
}
}
⚠️ 常见踩坑 :Filter 加个
@Component就自动生效了,但 Interceptor 除了@Component,还必须在 WebMvcConfigurer 中显式注册 。只加@Component不注册,拦截器不会生效。
4. AOP:方法级日志
java
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.coderplus.controller..*(..))")
public void controllerLayer() {}
@Around("controllerLayer()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("[AOP] @Around - 方法: "
+ joinPoint.getSignature().toShortString());
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
System.out.println("[AOP] @Around - 耗时: "
+ (System.currentTimeMillis() - start) + "ms");
return result;
}
}
AOP 使用 @Around 通知包裹 Controller 方法,分别在方法执行前后打日志。
5. Controller:业务层
java
@RestController
@RequestMapping("/api")
public class OrderController {
@GetMapping("/orders")
public List<Map<String, Object>> getOrders(
@RequestParam(defaultValue = "1") Long userId) {
System.out.println("[Controller] OrderController.getOrders(userId="
+ userId + ")");
return List.of(
Map.of("orderId", "ORD20260709001", "amount", 99.90, "status", "已支付"),
Map.of("orderId", "ORD20260708015", "amount", 256.00, "status", "已发货"),
Map.of("orderId", "ORD20260701042", "amount", 18.50, "status", "已完成")
);
}
}
实验:亲眼看看执行顺序
启动应用(mvn spring-boot:run),调用同一个接口两次------一次带 token,一次不带。
场景一:正常请求(带 token)
bash
$ curl http://localhost:8080/api/orders?userId=1 \
-H "Authorization: Bearer xxx"
控制台输出:

逐行解读:
- Filter BEFORE 最先执行------请求一进来就到了 Servlet 容器层
- Interceptor preHandle 接着执行------Spring MVC 收到 DispatcherServlet 转发的请求
- AOP @Around / @Before------AOP 代理拦截了 Controller 方法调用
- Controller------业务逻辑在这里执行
- AOP @AfterReturning------方法正常返回后,AOP 做后置处理
- Interceptor postHandle / afterCompletion------Spring MVC 完成视图渲染(REST 接口没有视图,但也走这个流程)
- Filter AFTER------Servlet 容器完成响应,控制权回到 Filter
执行链确认了:Filter → Interceptor → AOP → Controller → AOP → Interceptor → Filter,层层包裹,洋葱模型。
场景二:被拦截的请求(不带 token)
bash
$ curl http://localhost:8080/api/orders?userId=1
控制台输出:

这一次只有 4 行日志。注意看少了什么:
- ❌ AOP 没有触发:Interceptor 返回 false 后,Controller 方法根本没被调用,AOP 代理自然也收不到通知。
- ❌ postHandle 没有触发:符合 Spring 规范------preHandle 返回 false,postHandle 不调用。
- ❌ afterCompletion 没有触发:同上。
- ✅ Filter AFTER 仍然执行了 :这就是同事遇到的"诡异"问题!
核心发现:Filter 为什么"停不下来"
回看 Filter 的代码:
java
chain.doFilter(request, response); // ← 这里之后
// ↓ 无论如何都会执行到这里 ↓
System.out.println("[Filter] AFTER : ...");
chain.doFilter() 是一个同步调用 。当 Interceptor 在内部返回 false 时,Spring MVC 直接结束处理,chain.doFilter() 正常返回------它不知道里面发生了什么,也不关心。Filter 只知道"调用完成了",然后继续执行自己的 AFTER 逻辑。
这就导致了一个实际生产中的坑:
如果你在 Filter 的 AFTER 里做了"请求成功才算"的操作(比如业务指标打点、响应头改写),被拦截的请求也会触发这些操作,数据就脏了。
正确的做法是:把"只对成功请求做"的逻辑放在 Interceptor 的 postHandle 或 afterCompletion 里,或者 Filter 的 AFTER 里检查 response 状态码。
什么时候用 Filter,什么时候用 Interceptor?
| 维度 | Filter | Interceptor |
|---|---|---|
| 所在层级 | Servlet 容器 | Spring MVC |
| 能访问 Spring Bean | ❌ 不能(需要 root context) | ✅ 能(本身就是 Spring Bean) |
| 知道目标 Handler | ❌ 不知道 | ✅ 知道是哪个 Controller 方法 |
| 控制请求粒度 | 只能按 URL 模式匹配 | 可按 URL + Handler + 注解匹配 |
| 适用场景 | 编码设置、CORS、安全头、请求体包装 | 权限校验、日志、参数处理、登录检查 |
选择指南:
- 需要操作
HttpServletRequest/HttpServletResponse本身的事 → Filter(如 GZip 压缩、字符编码、CORS) - 需要知道"这个请求要去哪个 Controller"才能做判断 → Interceptor (如权限注解
@RequireRole、限流) - 需要对所有 Controller 方法做统一增强 → Interceptor 或 AOP 都行,看你要不要"进入 Controller 之后才执行"
- 需要在 Filter 和业务代码之间共享数据 → 用
request.setAttribute(),Interceptor 和 Controller 都能读到
如何排查这类问题
核心原因是日志看不出执行顺序。如果一开始就在每个组件里打上清晰的日志,很快就能定位。
建议在项目中给 Filter 和 Interceptor 统一加上类似本文 Demo 的日志格式:
css
[Filter] BEFORE: {method} {uri}
[Interceptor] preHandle → {result}
[Controller] {class}.{method}({args})
[Interceptor] afterCompletion
[Filter] AFTER : {uri} | {elapsed}ms
配合 logging.level.org.springframework.web=DEBUG,可以看到 DispatcherServlet 内部的调度日志,排查问题事半功倍。
总结
- 执行顺序是 Filter → Interceptor → AOP → Controller,洋葱模型,层层包裹、原路返回。
- Interceptor 的
preHandle返回false后,postHandle 和 afterCompletion 不执行,但 Filter 的 AFTER 照走不误------这是 Servlet 和 Spring MVC 两层独立生命周期决定的。 - Filter 用
@Component就生效,Interceptor 除了@Component还要在 WebMvcConfigurer 中注册------这是新手最容易踩的坑之一。
完整源码
本文 Demo 的完整代码已上传到 Gitee:
arduino
https://gitee.com/gcchech/articles-demo
项目根目录是 Maven 多模块工程。进入 spring-filter-interceptor/ 目录:
bash
mvn spring-boot:run # 启动 Demo
curl http://localhost:8080/api/orders?userId=1 -H "Authorization: Bearer xxx" # 正常请求
curl http://localhost:8080/api/orders?userId=1 # 被拦截
观察控制台输出,你就能亲眼看到 Filter → Interceptor → AOP → Controller → 原路返回的完整执行链。
如果你也遇到过被"诡异的顺序问题"折磨的经历,欢迎在评论区和大家分享。