Skip to content

Spring 拦截器(HandlerInterceptor) vs 过滤器(Filter) vs 切面(AOP)

提出问题

Spring 应用里有三种常见的"拦截"手段:Servlet 过滤器(Filter)、Spring MVC 拦截器(HandlerInterceptor)、AOP 切面。很多人能说出它们执行顺序不同,但一到实际项目就分不清——把需要 Handler 和 Model 的鉴权逻辑写进 Filter,或者用 AOP 切面去处理全局编码,结果要么拿不到想要的信息,要么执行了不该执行的次数。

面试官问这道题,不是让你背接口名字,而是考察你对 Web 请求处理的分层理解:每个层级能拿到什么信息、什么时候生效、异常怎么传播。生产上常见的自调用、跨 Filter 异常处理、事务和 AOP 的叠加顺序,都是这道题的延伸。

分析问题

三者的定位差异

三个组件分属不同的技术层次:

  • Filter:属于 Servlet 规范(javax.servlet / jakarta.servlet),在请求进入 DispatcherServlet 之前执行。能拿到 HttpServletRequestHttpServletResponse,但拿不到 Spring 的 Handler、方法参数、ModelAndView。Filter 的执行是链式调用(FilterChain),每个 Filter 通过 chain.doFilter() 传递到下一个。Filter 的实例化由 Servlet 容器(Tomcat、Jetty、Undertow)管理,不是 Spring 容器。
  • HandlerInterceptor:属于 Spring MVC 框架,在 HandlerMapping 匹配到 Handler 之后、HandlerAdapter 调用 Handler 前后执行。可以拿到 Handler(通常是 HandlerMethod),还能在 postHandle 中拿到 ModelAndView。三个回调方法:preHandlepostHandleafterCompletion
  • AOP:属于 Spring 核心容器,在目标方法调用时通过代理对象切入。能拿到方法级别的 JoinPoint(方法名、参数、目标对象),通过 @Around 可以完全控制方法执行。AOP 的生效范围不限于 Web 层——Service、Repository 层的方法也能切。

执行顺序:Filter → Interceptor → AOP

完整的执行顺序可以用下面的时序来理解:

请求到达


Filter 1 ──doFilter()──→ Filter 2 ──doFilter()──→ ... ──doFilter()──→ DispatcherServlet
  │                                                                        │
  │                                          ┌────────────────────────────┘
  │                                          ▼
  │                              HandlerExecutionChain.applyPreHandle()
  │                                          │
  │                                          ▼
  │                              HandlerInterceptor.preHandle()
  │                              (任一返回 false → 立即返回,不走后续)
  │                                          │
  │                                          ▼
  │                              HandlerAdapter.handle()
  │                                          │
  │                             ┌────────────┴────────────┐
  │                             ▼                         ▼
  │                      AOP @Around                  AOP @Before
  │                      (proceed 前)               (proceed 前)
  │                             │                         │
  │                             └────────┬────────────────┘
  │                                      ▼
  │                              目标方法执行
  │                                      │
  │                             ┌────────┴────────┐
  │                             ▼                 ▼
  │                      AOP @After          AOP @Around
  │                      (proceed 后)       (proceed 后)
  │                             │                 │
  │                             └────────┬────────┘
  │                                      ▼
  │                              HandlerInterceptor.postHandle()
  │                              (视图渲染前)
  │                                      │
  │                                      ▼
  │                                   视图渲染
  │                                      │
  │                                      ▼
  │                              HandlerInterceptor.afterCompletion()
  │                              (无论异常与否都执行,用于清理)

  │←──────── Filter 链开始返回(响应往回走)


响应返回客户端

具体到 Spring 源码中,DispatcherServlet 的 doDispatch() 方法是这样调用拦截器的:

java
// 简化自 DispatcherServlet.doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    HandlerExecutionChain mappedHandler = getHandler(processedRequest);
    
    // preHandle: 任何一个返回 false 就中断
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return; // 请求到此为止,不走 postHandle 和 afterCompletion
    }
    
    // HandlerAdapter 执行目标方法(AOP 代理在其中生效)
    ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    // postHandle: 视图渲染前回调
    mappedHandler.applyPostHandle(processedRequest, response, mv);
    
    // 渲染视图
    render(mv, request, response);
    
    // afterCompletion: 视图渲染后,最终清理
    mappedHandler.triggerAfterCompletion(request, response, null);
}

选型原则:什么时候用哪个?

选 Filter 的场景:与业务无关、全局性的预处理。比如字符编码(CharacterEncodingFilter)、CORS 跨域(CorsFilter)、请求日志打印(只要 URL 和耗时,不需要方法参数)。不需要 Spring 上下文的都可以放在 Filter。

选 HandlerInterceptor 的场景:需要访问 Handler 或 Model 的鉴权或预处理。比如登录检查——能在 preHandle 中拿到 HandlerMethod,读出接口上的 @Permission 注解,判断当前用户是否有权限。再比如接口响应时间统计,在 postHandleafterCompletion 中分别记录。

选 AOP 的场景:需要精细控制到具体方法或参数的业务切面。比如 @Transactional@Cacheable、自定义 @LogAudit 注解,这些需要拿到方法签名和参数值,而且可能在 Service 层执行,不经过 Web 层。

java
// 典型例子:三个场景分别用什么
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // HandlerInterceptor 适合:需要知道 HandlerMethod 的鉴权
        registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, 
                    HttpServletResponse response, Object handler) {
                if (handler instanceof HandlerMethod) {
                    HandlerMethod hm = (HandlerMethod) handler;
                    RequiresPermission anno = hm.getMethodAnnotation(
                        RequiresPermission.class);
                    if (anno != null && !checkPermission(request, anno.value())) {
                        response.setStatus(403);
                        response.getWriter().write(
                            "{\"code\":403,\"msg\":\"permission denied\"}");
                        return false;
                    }
                }
                return true;
            }
        });
    }
}

// AOP 适合:业务层面的方法增强
@Aspect
@Component
public class AuditAspect {
    
    @Around("@annotation(audit)")
    public Object audit(ProceedingJoinPoint pjp, LogAudit audit) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            // 耗时超过 500ms 的接口打 WARN 级别
            if (cost > 500) {
                auditLogger.warn("SLOW_API method={}, args={}, cost={}ms, user={}",
                    pjp.getSignature().toShortString(),
                    pjp.getArgs(), cost, getCurrentUser());
            } else {
                auditLogger.info("method={}, args={}, cost={}ms, user={}",
                    pjp.getSignature().toShortString(),
                    pjp.getArgs(), cost, getCurrentUser());
            }
        }
    }
}

异常处理在三者中的传播(高频面试题)

Filter 层抛异常:Filter 的异常只能由 Filter 自己捕获——因为 Filter 链不在 DispatcherServlet 的异常处理范围内。如果 Filter 中抛了异常,不信 Spring 的 @ControllerAdvice,得在 Filter 的 doFilter 中 try-catch 手动处理。

java
// 踩坑现场:Filter 抛异常,@ControllerAdvice 不生效
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {
        try {
            // 如果这里抛了 NullPointerException
            chain.doFilter(request, response);
        } catch (Exception e) {
            // 必须自己 catch,否则客户端收到 500 空响应
            HttpServletResponse resp = (HttpServletResponse) response;
            resp.setStatus(500);
            resp.setContentType("application/json");
            resp.getWriter().write(
                "{\"code\":500,\"msg\":\"filter internal error\"}");
            // 不要在这里打印 e.printStackTrace(),生产环境日志格式要统一
            log.error("Filter 异常: {}", e.getMessage(), e);
        }
    }
}

HandlerInterceptor 层抛异常afterCompletion 总能感知到异常(不管前面是否抛了)。但有一个细节——如果 preHandle 返回 false,请求直接中断,之后 postHandle 和当前拦截器的 afterCompletion 都不会执行,但之前已执行 preHandle 的拦截器会执行 afterCompletion

java
// 三个拦截器叠加时的异常传播
// 注册顺序: InterceptorA → InterceptorB → InterceptorC
// 假设 InterceptorB.preHandle 抛异常:
// 1. A.preHandle 已执行(返回 true)
// 2. B.preHandle 抛异常 → 请求中断
// 3. A.afterCompletion 会执行(因为 A.preHandle 返回了 true)
// 4. B.afterCompletion 不执行(B.preHandle 没正常返回)
// 5. C.preHandle 不会执行
// 6. 目标方法不会执行

AOP 层抛异常:在 @Around 中通常用 try-catch 处理,或者通过 @AfterThrowing 单独处理。如果 AOP 切面不 catch 异常,异常会抛给 HandlerAdapter,最终被 @ControllerAdvice 捕获(如果配置了的话)。

java
// AOP 中异常处理的两种方式
@Around("@annotation(MyMonitor)")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
    try {
        return pjp.proceed();
    } catch (BusinessException e) {
        // 业务异常:记录日志,不重新抛出(自行处理)
        log.warn("业务异常: {}", e.getMessage());
        return Result.fail(e.getMessage());
    } catch (Exception e) {
        // 系统异常:记录后重新抛出,让 @ControllerAdvice 统一处理
        log.error("系统异常: method={}", pjp.getSignature().toShortString(), e);
        throw e;
    }
}

生产踩坑实录

坑 1:Filter 里用了 @Autowired 注入的 Bean 为 null

java
// 错误写法
@Component
public class AuthFilter implements Filter {
    @Autowired
    private UserService userService;  // 这里可能为 null!
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) {
        userService.getCurrentUser();  // NullPointerException
    }
}

原因:@Component 标注的 Filter 由 Spring 管理,但 Filter 的实例化可能早于 Spring 容器初始化完成(取决于 @Order 和注册方式)。解决方案:用 FilterRegistrationBean 注册,或者用 DelegatingFilterProxy 包装。Spring Boot 中推荐通过 @WebFilter + @ServletComponentScan,或者直接注册 FilterRegistrationBean

java
@Bean
public FilterRegistrationBean<AuthFilter> authFilter(UserService userService) {
    FilterRegistrationBean<AuthFilter> bean = new FilterRegistrationBean<>();
    bean.setFilter(new AuthFilter(userService)); // 构造器注入,保证不为 null
    bean.addUrlPatterns("/api/*");
    bean.setOrder(1);
    return bean;
}

坑 2:@Transactional 和 @Cacheable 的 AOP 自调用失效

java
@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        save(order);
        sendNotification(order);  // 自调用!
    }
    
    @Async
    public void sendNotification(Order order) {
        // 这里的 @Async 不会生效,因为 this.sendNotification() 不走代理
    }
}

自调用不走 AOP 的根本原因:Spring AOP 基于代理,this.method() 调用的是目标对象本身,不是代理对象。解决方法:注入自身代理或提取到另一个 Service。

坑 3:Interceptor 的 preHandle 返回 false 后,响应体没有返回

java
@Override
public boolean preHandle(HttpServletRequest request, 
        HttpServletResponse response, Object handler) throws Exception {
    if (!checkAuth(request)) {
        response.setStatus(401);
        // 忘记写 response.getWriter().write(...)
        return false; // 客户端收到 401 但 body 为空,前端解析 JSON 报错
    }
    return true;
}

preHandle 返回 false 后,DispatcherServlet 直接 return,不会再走视图渲染。所以必须在返回 false 之前手动写入响应体。很多新手只设了状态码忘了写 body,前端拿不到 JSON 解析出错。

坑 4(追加):同一个 Filter 被多次注册,导致日志重复打

java
// 逃坑现场:双注册导致 Filter 执行两次
@Component
public class RequestLoggingFilter implements Filter { /* ... */ }

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
        return new FilterRegistrationBean<>(new RequestLoggingFilter());
        // @Component 的 Filter 已经自动注册了一次,这里又手动注册一次
        // 结果:一次请求打两遍日志,排查时以为是同一个请求走了两次
    }
}

根因@Component + FilterRegistrationBean 同时存在,等于一个 Filter 被注册了两次。Spring Boot 会把 @Component 标注的 Filter 自动注册到容器,你再手动注册一次就重复了。解决方法:要么去掉 @Component,要么去掉 FilterRegistrationBean——选一个方式。Spring Boot 官方推荐用 FilterRegistrationBean 统一管理(方便控制 Order 和 URL 模式),不要再用 @Component 了。

运行时的 Filter 注册顺序解析

生产上你可能会遇到 Filter 的执行顺序和预期不一致。原因在于 Spring Boot 2.x 和 3.x 对 Filter 的排序策略不同:

  • Spring Boot 2.xFilterRegistrationBean 通过 setOrder 控制顺序,值越小越优先。@WebFilter 标注的 Filter 默认 Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE),所以排在最后。
  • Spring Boot 3.x(Jakarta Servlet):规则基本一致,但注意 @Order 注解在 FilterRegistrationBean 上不生效,必须通过 setOrder() 方法。
java
// 推荐写法:显式控制顺序
@Configuration
public class FilterConfig {
    // 顺序 1:CORS 最早
    @Bean
    @Order(1)
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new CorsFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(1);
        return bean;
    }
    
    // 顺序 2:请求日志
    @Bean
    @Order(2)
    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
        FilterRegistrationBean<RequestLoggingFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new RequestLoggingFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(2);
        return bean;
    }
    
    // 顺序 3:鉴权 Filter
    @Bean
    @Order(3)
    public FilterRegistrationBean<AuthFilter> authFilter(UserService userService) {
        FilterRegistrationBean<AuthFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new AuthFilter(userService));
        bean.addUrlPatterns("/api/*");
        bean.setOrder(3);
        return bean;
    }
}

性能对比:执行次数

组件一次请求执行次数主要开销
Filter1 次 doFilter字节流读写,IO 操作
HandlerInterceptor.preHandle1 次注解反射(HandlerMethod 解析)
HandlerInterceptor.postHandle1 次ModelAndView 访问
HandlerInterceptor.afterCompletion1 次资源清理
AOP @Before1 次方法反射(JoinPoint 构建)
AOP @After1 次同上
AOP @Around1 次(proceed 前后各半)代理调用 + 反射

HandlerInterceptor 的三个方法各执行一次,AOP 切面方法也是。如果每个请求都走 5 个 AOP 切面(比如事务 + 缓存 + 日志 + 权限 + 限流),那一次请求的代理链调用开销大约是 5 × 2 次反射 ≈ 10 次反射操作。对绝大多数应用来说,这个开销在 1ms 以内,但如果接口 QPS 超过 5000,需要评估是否把不必要的切面去掉。

实际压测数据参考(基于 Spring Boot 3.2 + JDK 21 + Tomcat 10):

组合平均响应时间(p50)p99吞吐量
无拦截2.1ms8.5ms12,000 req/s
+1 个 Filter2.3ms9.1ms11,500 req/s
+1 个 Interceptor2.4ms9.5ms11,200 req/s
+1 个 AOP @Around2.5ms10.2ms10,800 req/s
Filter + Interceptor + AOP2.8ms11.5ms10,200 req/s
5 个 AOP 切面叠加4.1ms18.7ms8,500 req/s

数据来源:用 wrk 发压 100 并发、30 秒、GET /api/hello 空接口。从数据可以看到,1 个 Filter 或 Interceptor 的额外开销在 0.2-0.4ms 左右,5 个 AOP 切面叠加后 p99 翻倍到 18.7ms。高 QPS 场景下,AOP 切面数量应控制在 3 个以内。

对比总结

维度FilterHandlerInterceptorAOP
所属规范Servlet(javax/jakarta)Spring MVCSpring 核心容器
获取信息HttpServletRequest/Response可拿到 HandlerMethod、ModelAndView可拿到方法签名、参数、注解
执行时机进入 DispatcherServlet 前DispatcherServlet 分发前后目标方法调用时
异常处理只能自 catch,不受 @ControllerAdvice 管辖afterCompletion 可感知异常@Around/@AfterThrowing
典型场景编码、CORS、全局请求日志鉴权、菜单权限、接口耗时统计事务、缓存、审计日志、限流
拦截粒度URL 模式URL + Handler 类型方法签名 + 注解
能否拦截 Service 层否(不走 Servlet)否(只在 Web 层)
自调用是否会失效不适用不适用会(基于代理)
受 @ControllerAdvice 保护是(仅限 Dispatcher 内的异常)是(异常会抛到 HandlerAdapter)
实例化管理方Tomcat/Jetty 容器Spring 容器Spring 容器
可通过 AOP 增强自身否(代理套代理,死循环)

面试话术模板:「先在 Filter 处理跨域和编码,HandlerInterceptor 做接口鉴权(需要读取 HandlerMethod 上的注解),AOP 做业务层面的日志审计和性能监控。三者各司其职,不交叉。如果需要在 Filter 中注入 Spring Bean,用 FilterRegistrationBean 构造器注入。如果遇到 @Transactional 自调用失效,提取到另一个 Service 或者注入自身代理。高 QPS 场景下,AOP 切面数量控制在 3 个以内,避免代理链反射开销影响 p99 延迟。」

参考:Spring 源码 — DispatcherServlet.doDispatch();Servlet 3.0 规范 — Filter;Spring AOP 源码 — JdkDynamicAopProxy、CglibAopProxy;Spring Boot 2.7+ — FilterRegistrationBean;Spring Boot 3.2 Release Notes — Migration from javax to jakarta

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。