2126 字
约 7 分钟
18
S07-X03 · 过滤器与拦截器:请求链上的两道关卡

同一个需求——"给所有请求加个 traceId"——用 Filter 和 HandlerInterceptor 都能实现,效果却不同:前者能包装请求体,后者能读到 HandlerMethod 上的注解;前者在 DispatcherServlet 之外,后者在它之内。

选错地方代码能跑,但会缺东西:拿不到方法参数、拿不到注解、清理不执行、异步请求里跑两遍。


1. 两道关卡在链上的位置

一次请求进 Tomcat 之后的完整顺序:

阶段 组件 能做什么
1 FilterChain 中的各 Filter 包装/替换 ServletRequest,改编码、改 header、短路响应
3 HandlerMapping 选 handler,确定 HandlerExecutionChain
4 Interceptor.preHandle 拿 HandlerMethod 判权限、埋 traceId
5 HandlerAdapter + 参数解析 + 消息转换 真正调用 controller 方法
6 Interceptor.postHandle → 视图渲染 / 写响应 序列化 JSON
8 Interceptor.afterCompletion 无论成功失败都要做的清理
9 Filter 的 doFilter 后半段 记录总耗时、还原 MDC

两个关键事实:Filter 在 DispatcherServlet 之外,所以它看不到"哪个方法被调用";Interceptor 由 HandlerMapping 装配,所以它能拿到 HandlerMethod——方法、类、注解、参数类型全在手上。

维度 Filter HandlerInterceptor
归属 Servlet(jakarta.servlet) Spring MVC
注册方式 FilterRegistrationBean、@Bean Filter、web.xml WebMvcConfigurer.addInterceptors
顺序控制 @Order / Ordered / 注册顺序 注册顺序(registry.addInterceptor 的调用次序)
能否拿到 handler 否 是,HandlerMethod
改写请求/响应体 能,包装 HttpServletRequestWrapper 不能,只能读与设 attribute
能否作用于静态资源 能,在 MVC 之前 取决于 HandlerMapping
异常能否被 @ControllerAdvice 处理 不能(在 MVC 之外) 能,交给 HandlerExceptionResolver
典型职责 编码、CORS、请求包装、压缩、审计 鉴权、注解解析、日志、权限校验

2. Filter:三个方法与放行语义

@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class TraceFilter implements Filter {
    @Override public void init(FilterConfig cfg) { }         // 容器启动时一次
    @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        long t0 = System.nanoTime();
        try {
            chain.doFilter(req, res);                        // 放行:交给下一个 Filter 或 Servlet
        } finally {
            log.info("cost={}ms", (System.nanoTime() - t0) / 1_000_000);
        }
    }

    @Override public void destroy() { }
}

chain.doFilter 是唯一放行入口。忘了调用,请求静默回空响应、状态码 200——最容易被误判为"接口返回空"。

写法 结果
调 chain.doFilter 且不写响应 正常放行
不调 chain.doFilter,自己写完响应 请求被拦截,后续 Filter 与 Servlet 都不执行
不调 chain.doFilter,也不写响应 客户端收到空 200,链路静默中断
调两次 chain.doFilter 下游被执行两次,常见于转发场景

顺序有两种来源:

// 方式一:@Bean 注册,order 显式指定
@Bean
public FilterRegistrationBean<TraceFilter> traceFilter() {
    FilterRegistrationBean<TraceFilter> bean = new FilterRegistrationBean<>(new TraceFilter());
    bean.addUrlPatterns("/*");
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

另一种是让 Filter 被组件扫描(@Component + @Order),顺序缺省为 LOWEST_PRECEDENCE。同一个 Filter 不要两处都注册,否则它会进链两次,@Order 也救不了。

2.1 为什么需要 OncePerRequestFilter

Filter 默认是"每次进入 FilterChain 就执行"。而 forward、include、以及容器的 error dispatch 都会再次进入同一条链,于是同一个请求里 Filter 跑了多遍——埋点翻倍、MDC 被清两次。

public class IdempotentFilter extends OncePerRequestFilter {
    @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                              FilterChain chain) throws ServletException, IOException {
        chain.doFilter(req, res);      // 同一请求内保证只进来一次
    }
    @Override protected boolean shouldNotFilterAsyncDispatch() { return false; } // 需要参与异步派发时
}

它的实现是往 request attribute 里写一个标记位,第二次进来直接放行。注意 shouldNotFilterAsyncDispatch() 默认返回 true,即默认不参与异步派发;如果清理逻辑必须在异步线程结束时执行,要显式改回 false。


3. HandlerInterceptor:三个方法的时机

public class AuthInterceptor implements HandlerInterceptor {
    @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        if (!(handler instanceof HandlerMethod hm)) return true;      // 静态资源等放行
        RequireRole anno = hm.getMethodAnnotation(RequireRole.class);
        if (anno == null) return true;
        if (!hasRole(req, anno.value())) {
            res.setStatus(403);
            return false;            // 中断,后续不再执行
        }
        return true;
    }
    @Override public void postHandle(HttpServletRequest req, HttpServletResponse res,
                                     Object handler, ModelAndView mv) { }

    @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
                                          Object handler, Exception ex) {
        MDC.clear();                 // 成功、失败都走这里
    }
}
方法 时机 异常时是否执行 返回 false 的后果
preHandle handler 调用前 — 中断请求,后续 preHandle/postHandle 不执行
postHandle handler 返回后、渲染前 不执行(有异常时跳过) —
afterCompletion 请求完成后 执行,ex 非空 —

两个细节:preHandle 返回 false 时,已通过的拦截器仍会收到 afterCompletion;postHandle 在 handler 抛异常时被跳过,所以"必须执行"的清理只能写在 afterCompletion。

注册靠 WebMvcConfigurer,顺序即执行顺序:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**").order(1);
        registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**").order(2);
    }
}

order 不写时按注册顺序;excludePathPatterns 的匹配是 Ant 风格,/api/** 不会自动排除 /api/public/**,必须显式写。


4. 异常与 @ControllerAdvice 的关系

抛出位置 谁来处理 能否命中 @ExceptionHandler
Filter.doFilter 容器 / ErrorPageFilter 不能,此时还没进 MVC
Interceptor.preHandle DispatcherServlet.processHandlerException 能,走 HandlerExceptionResolver 链
controller 方法 同上 能
HttpMessageConverter 序列化 同上(部分场景) 能
@RestControllerAdvice
public class GlobalHandler {
    @ExceptionHandler(ForbiddenException.class)
    public ResponseEntity<String> forbidden(ForbiddenException e) {
        return ResponseEntity.status(403).body(e.getMessage());
    }
}

结论:想在拦截器里抛业务异常并用全局 advice 统一成 JSON,是可行的;想在 Filter 里这么做,不会生效——它已经在 MVC 之外,需要自己写响应体。


5. 分工建议

需求 放哪 原因
字符编码、请求体解密、gzip Filter 需要在 Servlet 之前包装流
CORS 头(统一出口) Filter 或 Spring CORS 配置,只选一处 两处都配会产生重复响应头
请求体多次读取 Filter + ContentCachingRequestWrapper 流只能读一次,必须在最外层缓存
登录态校验、角色判断 Interceptor 需要 HandlerMethod 与注解
全局异常转 JSON @RestControllerAdvice MVC 内统一处理

一句话原则:要动"字节"(流、编码、header)放 Filter,要动"语义"(哪个方法、什么注解、什么角色)放 Interceptor。


6. 常见坑

坑 现象 原因 处理
preHandle 写完响应但没 return false 响应体被拼两遍,或 IllegalStateException 写了响应仍继续放行 写完必须 return false
两处都配 CORS 报 Multiple values for Access-Control-Allow-Origin CorsFilter 与 addCorsMappings 各加一次 只留一处
OPTIONS 被鉴权拦截 预检 403,实际请求根本没发出 鉴权拦截器拦了预检 放行 OPTIONS,或在 Filter 层先处理 CORS
异步请求 afterCompletion 触发两次 MDC 清理时机混乱 异步派发导致拦截器二次执行 实现 AsyncHandlerInterceptor,在 afterConcurrentHandlingStarted 里处理
Filter 里读不到 HandlerMethod 强转失败或拿到 null 位置在 MVC 之外 换 Interceptor
OncePerRequestFilter 没生效 转发场景仍执行两次 不是 OncePerRequestFilter 子类 继承它,别直接实现 Filter

7. 边界:什么时候不用这两者

场景 更合适的做法 理由
只想给 controller 方法加统一行为 @Aspect 切面 能拿到方法参数与返回值
只想改响应体 ResponseBodyAdvice 在消息转换层,比包装 response 干净
需要按注解做权限 @PreAuthorize(方法级安全) 现成机制,不必手写拦截器
需要跨服务传递上下文 网关或客户端拦截器 客户端侧才是正确位置

8. 自测

  1. 同一个请求里 Filter 与 HandlerInterceptor 谁先执行?afterCompletion 与 Filter 的后半段谁先?
  2. preHandle 返回 false 时,已经通过的拦截器还会收到 afterCompletion 吗?
  3. 为什么 Filter 里抛的异常无法被 @ControllerAdvice 转成 JSON?
  4. OncePerRequestFilter 解决的是什么重复执行问题?异步派发默认参与吗?
  5. 想读请求体三次,该用 Filter 还是 Interceptor?为什么?
  6. CORS 同时在 CorsFilter 和 addCorsMappings 配了会发生什么?
S07-X03 · 过滤器与拦截器:请求链上的两道关卡
https://www.clxhxhhr.top/posts/5809/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。