Filter、Interceptor 与 AOP 的区别与选型
"登录校验该用 Filter 还是 Interceptor""记录接口耗时用 AOP 还是拦截器""为什么 Filter 里拿不到 @RequestMapping 的信息"——这些问题本质上是同一个:三者的执行位置不同,能拿到的东西自然不同。本文把三者放进同一条请求链路,逐一说明它们的归属、时机与能力边界,并给出选型清单。适合需要搭建统一鉴权、日志、限流层的后端开发阅读。
一、一条请求的完整链路
以 Spring Boot 3 + 内嵌 Tomcat 为例,一个 HTTP 请求的经过顺序是:
Tomcat NIO Connector
→ FilterChain(Servlet 规范)
→ CharacterEncodingFilter
→ FormContentFilter
→ 自定义 Filter
→ DispatcherServlet#service
→ HandlerExecutionChain
→ HandlerInterceptor#preHandle
→ HandlerMethod(AOP 代理对象)
→ MethodInterceptor(@Around 等通知)
→ Controller 方法
→ MethodInterceptor 返回
→ HandlerInterceptor#postHandle
→ HandlerInterceptor#afterCompletion
→ Filter 出栈
→ 响应写出
关键结论:Filter 在 Servlet 层,Interceptor 在 Spring MVC 层,AOP 在方法调用层。越靠外,拿到的信息越少但覆盖面越广;越靠内,信息越丰富但作用范围越窄。
二、Filter
2.1 归属与时机
Filter 是 Servlet 规范定义的接口,由 Servlet 容器管理,Spring 只是提供 DelegatingFilterProxy 和 FilterRegistrationBean 来注册它。它在 DispatcherServlet 之前执行,请求和响应都会经过。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
response.setHeader("X-Trace-Id", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
OncePerRequestFilter 保证在 FORWARD、INCLUDE 等分发场景下只执行一次,避免重复包装请求。直接实现 Filter 接口时,异步分发会导致多次执行。
2.2 能力边界
- 能拿到
HttpServletRequest、HttpServletResponse,可包装请求体、修改响应头。 - 拿不到
HandlerMethod,不知道最终会调用哪个 Controller 方法。 - 能拦截静态资源、404 请求,因为它在
DispatcherServlet之前。 - 抛出异常不会进入 Spring MVC 的
HandlerExceptionResolver,除非异常发生在DispatcherServlet内部。Filter里抛的异常由 Servlet 容器处理,跳转到/error。
三、Interceptor
3.1 归属与时机
HandlerInterceptor 是 Spring MVC 的接口,由 HandlerMapping 返回的 HandlerExecutionChain 持有,在 DispatcherServlet#doDispatch 中被调用:
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable Exception ex) throws Exception {
}
}
执行规则:
preHandle按order正序执行,返回false中断后续流程。postHandle按逆序执行,仅在preHandle全部通过且方法正常返回时执行。afterCompletion按逆序执行,无论成功或异常都会执行(前提是对应的preHandle已返回true)。
3.2 能力边界
handler参数在正常情况下就是HandlerMethod,可以读到方法上的注解、参数类型、所属类。- 拿不到请求体(
@RequestBody的原始流),因为请求体在HandlerAdapter调用时才被读取。 - 可以拿到
ModelAndView,适合做视图相关的处理。 - 抛出的异常会进入
HandlerExceptionResolver,可被@ExceptionHandler捕获。
基于 HandlerMethod 做注解驱动的鉴权:
@Component
public class PermissionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequirePermission annotation =
handlerMethod.getMethodAnnotation(RequirePermission.class);
if (annotation == null) {
return true;
}
String required = annotation.value();
Set<String> owned = currentUserPermissions();
if (!owned.contains(required)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"无权限\"}");
return false;
}
return true;
}
}
四、AOP
4.1 归属与时机
AOP 作用于 Bean 的方法调用,与 HTTP 请求无关。它由 AbstractAutoProxyCreator 在 Bean 初始化后生成代理,调用时经过 MethodInterceptor 链。
@Aspect
@Component
public class CostAspect {
private static final Logger log = LoggerFactory.getLogger(CostAspect.class);
@Around("@annotation(com.example.metrics.Metered)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long cost = (System.nanoTime() - start) / 1_000_000;
log.info("{}.{} cost={}ms",
pjp.getSignature().getDeclaringType().getSimpleName(),
pjp.getSignature().getName(), cost);
}
}
}
4.2 能力边界
- 能拿到方法参数、返回值、异常,可修改入参和返回值。
- 不依赖 Web 环境,Service、Repository、定时任务都能用。
- 只拦截经过代理的调用,自调用失效。
- 拿不到
HttpServletRequest,除非通过RequestContextHolder获取(要求当前线程处于请求上下文)。 - 无法拦截
private、final、静态方法。
五、三者对比
| 维度 | Filter | Interceptor | AOP |
| --- | --- | --- | --- |
| 规范归属 | Servlet | Spring MVC | Spring AOP |
| 执行位置 | DispatcherServlet 之前 | DispatcherServlet 内部 | Bean 方法调用 |
| 作用对象 | 所有 HTTP 请求 | MVC 处理的请求 | 任意 Spring Bean 方法 |
| 能否拿到 HandlerMethod | 否 | 是 | 否(但能拿到方法签名) |
| 能否拿到请求体 | 是(可包装) | 否 | 否(需 RequestContextHolder) |
| 能否拦截静态资源 | 是 | 否 | 否 |
| 异常是否进入 @ExceptionHandler | 否 | 是 | 是 |
| 是否依赖 Web 环境 | 是 | 是 | 否 |
| 自调用是否生效 | 不适用 | 不适用 | 否 |
| 典型用途 | 编码、CORS、TraceId、请求包装 | 登录态、权限、接口日志 | 事务、缓存、重试、方法耗时 |
六、三者如何共享数据
一条请求上,三者经常需要传递上下文,比如 Filter 生成的 traceId、Interceptor 解析出的用户身份、AOP 记录的耗时。传递方式有三种:
// 1. ThreadLocal:最常用,需在请求结束时清理
public final class UserContext {
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
public static void set(Long id) { USER_ID.set(id); }
public static Long get() { return USER_ID.get(); }
public static void clear() { USER_ID.remove(); }
}
// 2. 请求属性:随请求生命周期,天然隔离
request.setAttribute("userId", userId);
// 3. MDC:专用于日志,会自动附加到每条日志
MDC.put("traceId", traceId);
ThreadLocal 必须清理,否则线程池复用时会把上一个请求的用户身份带给下一个请求,属于安全事故级别的问题。Filter 的 finally、Interceptor 的 afterCompletion、AOP 的 finally 都是合适的清理位置,取决于在哪儿设置。
七、选型清单
按需求对号入座:
- 需要在请求体被读取前处理:用
Filter。例如统一包装ContentCachingRequestWrapper以便多次读取。 - 需要拦截静态资源或 404:用
Filter。Interceptor只处理被DispatcherServlet接管的请求。 - 需要读取方法注解做鉴权:用
Interceptor。HandlerMethod#getMethodAnnotation是最直接的方式。 - 需要按 Controller 方法维度统计耗时并记录异常:用
Interceptor,afterCompletion的ex参数能拿到异常。 - 需要作用于 Service 层、定时任务、MQ 消费者:用 AOP。
Interceptor根本不覆盖这些场景。 - 需要修改方法返回值或实现重试:用 AOP,只有它能拿到返回值并重新调用
proceed()。 - 需要跨域、编码、压缩:用
Filter,这些是 Servlet 规范层的能力。
一个常见组合:Filter 生成 TraceId 并放入 MDC,Interceptor 做鉴权与访问日志,AOP 做 Service 层的方法耗时与重试。三者各管一段,不重叠。
八、一个组合实战
需求:所有接口生成 TraceId,/api/** 需要登录,Service 层的写操作记录耗时。三者各司其职:
// 1. Filter:最外层生成 TraceId,覆盖所有请求
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
FilterChain chain) throws ServletException, IOException {
String traceId = UUID.randomUUID().toString().substring(0, 8);
MDC.put("traceId", traceId);
resp.setHeader("X-Trace-Id", traceId);
try {
chain.doFilter(req, resp);
} finally {
MDC.remove("traceId");
}
}
}
// 2. Interceptor:需要 HandlerMethod,做登录校验
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp,
Object handler) throws IOException {
if (!(handler instanceof HandlerMethod)) {
return true;
}
String token = req.getHeader("Authorization");
if (token == null || !tokenService.verify(token)) {
resp.setStatus(401);
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false;
}
UserContext.set(tokenService.parseUserId(token));
return true;
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse resp,
Object handler, Exception ex) {
UserContext.clear(); // 必须清理 ThreadLocal
}
}
// 3. AOP:不依赖 Web,覆盖 Service 方法
@Aspect
@Component
@Order(2)
public class ServiceCostAspect {
@Around("execution(* com.example.service..*.save*(..)) "
+ "|| execution(* com.example.service..*.update*(..))")
public Object cost(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
log.info("{} cost={}ms", pjp.getSignature().toShortString(),
(System.nanoTime() - start) / 1_000_000);
}
}
}
三层顺序是:TraceFilter → LoginInterceptor → ServiceCostAspect。TraceId 在 Filter 生成,拦截器和切面的日志都能带上;登录态在 Interceptor 设置,切面可以读到 UserContext;耗时统计放在切面,能覆盖 Service 被定时任务、MQ 消费者调用的场景。
九、常见坑 / 最佳实践
- 在 Filter 里读取请求体导致 Controller 拿到空流。
getInputStream只能读一次。需要读取时用ContentCachingRequestWrapper包装并重写getInputStream,或改用Interceptor。 - Interceptor 的
preHandle返回false但没写响应。请求会一直挂起直到客户端超时。返回false前必须写出状态码或响应体。 - Filter 里抛异常期待被
@RestControllerAdvice捕获。Filter在DispatcherServlet之外,异常不会进入HandlerExceptionResolver。需要在 Filter 内部自己处理,或改用HandlerExceptionResolver级别的组件。 @Order与注册顺序混淆。Filter的@Order只在通过FilterRegistrationBean或@Component自动注册时生效;Interceptor的顺序由InterceptorRegistry#order决定,两者互不影响。- AOP 切面里依赖
RequestContextHolder。异步线程中RequestContextHolder默认取不到请求,需要RequestContextHolder.setRequestAttributes(attributes, true)显式传递,或改用TaskDecorator。 - 在 AOP 中修改返回值却返回了
null。@Around必须返回pjp.proceed()的结果(或明确的新值),漏写return会让方法返回null,且编译期不报错。
十、小结
Filter在 Servlet 层,覆盖所有请求,拿不到HandlerMethod,适合编码、CORS、TraceId、请求包装。Interceptor在 Spring MVC 层,能拿到HandlerMethod与ModelAndView,适合鉴权、接口日志、访问统计。AOP在 Bean 方法层,不依赖 Web 环境,适合事务、缓存、重试、Service 耗时。- 越靠外覆盖面越广、信息越少;越靠内信息越丰富、作用范围越窄。
- 选型的判断标准是"需要什么信息"和"需要覆盖哪些调用",而不是"哪个更常用"。
- 点赞
- 收藏
- 关注作者
评论(0)