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 之前执行。能拿到HttpServletRequest和HttpServletResponse,但拿不到 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。三个回调方法:preHandle、postHandle、afterCompletion。 - 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() 方法是这样调用拦截器的:
// 简化自 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 注解,判断当前用户是否有权限。再比如接口响应时间统计,在 postHandle 和 afterCompletion 中分别记录。
选 AOP 的场景:需要精细控制到具体方法或参数的业务切面。比如 @Transactional、@Cacheable、自定义 @LogAudit 注解,这些需要拿到方法签名和参数值,而且可能在 Service 层执行,不经过 Web 层。
// 典型例子:三个场景分别用什么
@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 手动处理。
// 踩坑现场: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。
// 三个拦截器叠加时的异常传播
// 注册顺序: 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 捕获(如果配置了的话)。
// 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
// 错误写法
@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:
@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 自调用失效
@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 后,响应体没有返回
@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 被多次注册,导致日志重复打
// 逃坑现场:双注册导致 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.x:
FilterRegistrationBean通过setOrder控制顺序,值越小越优先。@WebFilter标注的 Filter 默认Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE),所以排在最后。 - Spring Boot 3.x(Jakarta Servlet):规则基本一致,但注意
@Order注解在FilterRegistrationBean上不生效,必须通过setOrder()方法。
// 推荐写法:显式控制顺序
@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;
}
}性能对比:执行次数
| 组件 | 一次请求执行次数 | 主要开销 |
|---|---|---|
| Filter | 1 次 doFilter | 字节流读写,IO 操作 |
| HandlerInterceptor.preHandle | 1 次 | 注解反射(HandlerMethod 解析) |
| HandlerInterceptor.postHandle | 1 次 | ModelAndView 访问 |
| HandlerInterceptor.afterCompletion | 1 次 | 资源清理 |
| AOP @Before | 1 次 | 方法反射(JoinPoint 构建) |
| AOP @After | 1 次 | 同上 |
| AOP @Around | 1 次(proceed 前后各半) | 代理调用 + 反射 |
HandlerInterceptor 的三个方法各执行一次,AOP 切面方法也是。如果每个请求都走 5 个 AOP 切面(比如事务 + 缓存 + 日志 + 权限 + 限流),那一次请求的代理链调用开销大约是 5 × 2 次反射 ≈ 10 次反射操作。对绝大多数应用来说,这个开销在 1ms 以内,但如果接口 QPS 超过 5000,需要评估是否把不必要的切面去掉。
实际压测数据参考(基于 Spring Boot 3.2 + JDK 21 + Tomcat 10):
| 组合 | 平均响应时间(p50) | p99 | 吞吐量 |
|---|---|---|---|
| 无拦截 | 2.1ms | 8.5ms | 12,000 req/s |
| +1 个 Filter | 2.3ms | 9.1ms | 11,500 req/s |
| +1 个 Interceptor | 2.4ms | 9.5ms | 11,200 req/s |
| +1 个 AOP @Around | 2.5ms | 10.2ms | 10,800 req/s |
| Filter + Interceptor + AOP | 2.8ms | 11.5ms | 10,200 req/s |
| 5 个 AOP 切面叠加 | 4.1ms | 18.7ms | 8,500 req/s |
数据来源:用 wrk 发压 100 并发、30 秒、GET /api/hello 空接口。从数据可以看到,1 个 Filter 或 Interceptor 的额外开销在 0.2-0.4ms 左右,5 个 AOP 切面叠加后 p99 翻倍到 18.7ms。高 QPS 场景下,AOP 切面数量应控制在 3 个以内。
对比总结
| 维度 | Filter | HandlerInterceptor | AOP |
|---|---|---|---|
| 所属规范 | Servlet(javax/jakarta) | Spring MVC | Spring 核心容器 |
| 获取信息 | 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