Spring Boot 中 Filter、Interceptor、AOP 到底谁先执行?

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 返回 falsepostHandleafterCompletion 都不会被调用。但 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"

控制台输出:

逐行解读:

  1. Filter BEFORE 最先执行------请求一进来就到了 Servlet 容器层
  2. Interceptor preHandle 接着执行------Spring MVC 收到 DispatcherServlet 转发的请求
  3. AOP @Around / @Before------AOP 代理拦截了 Controller 方法调用
  4. Controller------业务逻辑在这里执行
  5. AOP @AfterReturning------方法正常返回后,AOP 做后置处理
  6. Interceptor postHandle / afterCompletion------Spring MVC 完成视图渲染(REST 接口没有视图,但也走这个流程)
  7. 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 的 postHandleafterCompletion 里,或者 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 方法做统一增强 → InterceptorAOP 都行,看你要不要"进入 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 内部的调度日志,排查问题事半功倍。


总结

  1. 执行顺序是 Filter → Interceptor → AOP → Controller,洋葱模型,层层包裹、原路返回。
  2. Interceptor 的 preHandle 返回 false 后,postHandle 和 afterCompletion 不执行,但 Filter 的 AFTER 照走不误------这是 Servlet 和 Spring MVC 两层独立生命周期决定的。
  3. 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 → 原路返回的完整执行链。


如果你也遇到过被"诡异的顺序问题"折磨的经历,欢迎在评论区和大家分享。

相关推荐
暗冰ཏོ1 小时前
Spring Boot + Vue3 订单支付模块技术实现:支付单、回调验签、幂等处理与余额扣减
前端·spring boot·后端·vue·微信支付·支付宝支付
Csvn1 小时前
Python 开发技巧 · 类型注解进阶 —— 从 `TypeVar` 到 `Protocol`,让类型检查真正帮你抓 bug
后端·python
爷_1 小时前
AI Coding Agent 沙箱的设计原理解析
前端·人工智能·后端
学渣超1 小时前
微服务日志智能诊断系统(八) 全链路追踪——request_id驱动的跨服务诊断
java·后端·agent
爱勇宝1 小时前
第 3 章:别把团队设计成一场内卷竞赛
前端·后端·程序员
ACGkaka_1 小时前
Spring Boot 实战(四十一):集成 BPMN(Flowable+bpmn-js)
java·spring boot·后端
ljs6482739512 小时前
智慧商城(Smart Mall):基于 Spring Boot + Vue 3 的现代化电商平台实战
linux·vue.js·spring boot·后端
yeffky2 小时前
Vibe Coding一个能办事的 AI Agent:Spring Boot + DeepSeek + LangGraph4j 六节点 ReAct 项目实战
人工智能·后端·agent
乒乓狂魔14786739970002 小时前
OpenClaw 终于可以读懂 OpenTelemetry APM 了
后端