过滤器 (Filter) 与拦截器 (Interceptor) 的区别及使用场景

概述

在 Java Web 开发的日常工作中,过滤器和拦截器是两个出场率极高的概念。它们都能在请求到达目标资源前后执行额外的逻辑,因此初学者很容易把它们混为一谈。然而,两者的设计层次、工作原理、作用范围和适用场景有着本质区别。

过滤器是 Servlet 规范中定义的标准组件,属于 Java EE 的基础设施层。它直接工作在 Servlet 容器内部,任何一个 HTTP 请求只要命中了你所配置的 URL 模式,过滤器就会被触发,与你是否使用 Spring 框架无关。这意味着即便你将来把项目从 Spring 迁移到其他框架,过滤器依然可以正常工作,因为它不依赖任何上层框架。

拦截器则是 Spring MVC 框架提供的扩展机制,它工作在框架层,依赖 Spring 的 IoC 容器。一个请求必须先经过过滤器的层层把关,最终到达 Spring 的核心分发器 DispatcherServlet,拦截器链才有机会介入。因此拦截器天然是 Spring 生态的一员,可以无缝使用依赖注入、AOP 等 Spring 特性。

理解这两者的区别,是写出结构清晰、职责分明的 Web 后端的基石。本文将从实现原理、执行流程、回调接口、使用场景四个维度进行详细拆解。


一、实现原理的深层差异

1.1 过滤器:基于函数回调的责任链

过滤器的底层设计模式是责任链模式。Servlet 容器在接收到请求后,会把所有匹配当前 URL 的过滤器组装成一条 FilterChain,然后依次调用每个过滤器的 doFilter() 方法。每个过滤器内部必须显式调用 chain.doFilter(request, response) 才能把请求传递给下一个过滤器或最终的 Servlet。如果在某个过滤器中不调用 chain.doFilter(),请求就会被直接阻断,不会再继续向下传递。

这个设计决定了过滤器的执行是"先入后出"的栈式调用:第一个过滤器的 pre 逻辑最先执行,但 post 逻辑最后执行。举个例子,假设配置了三个过滤器 FilterA、FilterB、FilterC,实际的执行顺序会是:FilterA 的 pre -> FilterB 的 pre -> FilterC 的 pre -> 目标 Servlet -> FilterC 的 post -> FilterB 的 post -> FilterA 的 post。这种对称结构非常适合需要在请求到达前和响应返回后各做一次处理的场景,比如记录请求耗时。

过滤器的另一个重要特性是它可以完全绕过 Spring 的 DispatcherServlet。如果一个请求访问的是 /static/logo.png 这样的静态资源,并且你配置的过滤器匹配了这个路径,那么过滤器仍然会执行,但它内部的 chain.doFilter() 最终传递到的可能是 Tomcat 的 DefaultServlet 而非 Spring 的 DispatcherServlet。这一点在实际开发中非常关键,因为很多时候我们确实需要对静态资源做统一处理,比如添加缓存头、检查 Referer 防盗链等。

1.2 拦截器:基于反射的动态代理

拦截器的底层实现依赖于 Java 的动态代理(JDK Proxy 或 CGLIB)和反射机制。Spring MVC 的 DispatcherServlet 在根据请求 URL 找到对应的 Handler(即 Controller 方法)之后,并不会直接调用它,而是先检查是否有配置了与该 URL 匹配的拦截器。如果有,DispatcherServlet 会依次调用每个拦截器的 preHandle() 方法。

与过滤器不同的是,拦截器的 preHandle() 方法有一个 Object handler 参数,这个参数就是即将处理该请求的 HandlerMethod 对象。通过反射你可以从这个对象中获取到目标 Controller 的类名、方法名、方法上的注解等信息。这种能力使得拦截器可以做很多过滤器做不到的事情,比如根据方法上的自定义注解来决定是否放行,或者根据 Controller 的名称来动态记录不同模块的操作日志。

preHandle() 返回 true 之后,DispatcherServlet 才会真正调用目标 Controller 方法。Controller 执行完毕后,如果返回的是视图(ModelAndView),拦截器的 postHandle() 方法会在视图渲染之前被调用,你可以在这里对模型数据做最后的修改。最后,无论 Controller 执行是否抛出异常,afterCompletion() 都会被调用,非常适合做资源清理工作。


二、执行流程的完整时序

为了更直观地理解两者的关系,下面用一张时序图展示一个请求从进入容器到返回响应的完整旅程:

客户端请求
    |
    v
+------------------+
|  Servlet 容器      |
|  (Tomcat/Jetty)   |
+------------------+
    |
    | 1. 构造 HttpServletRequest / HttpServletResponse
    v
+------------------+
|  Filter 1         |  pre:  记录请求到达时间
+------------------+
    |
    v
+------------------+
|  Filter 2         |  pre:  设置请求编码为 UTF-8
+------------------+
    |
    v
+------------------+
|  Filter 3         |  pre:  检查是否有恶意参数,拦截 SQL 注入
+------------------+
    |
    | 2. 经过所有 Filter 的 doFilter 链路后抵达
    v
+-------------------+
|  DispatcherServlet |  3. 根据 URL 查找 Handler (Controller)
+-------------------+
    |
    v
+-------------------+
|  Interceptor 1     |  preHandle: 校验 Token 是否有效
+-------------------+
    |
    v
+-------------------+
|  Interceptor 2     |  preHandle: 检查用户是否有该接口的权限
+-------------------+
    |
    v
+-------------------+
|  Controller 方法    |  4. 执行核心业务逻辑
+-------------------+
    |
    v
+-------------------+
|  Interceptor 2     |  postHandle: 对返回数据脱敏
+-------------------+
    |
    v
+-------------------+
|  Interceptor 1     |  postHandle: (无操作)
+-------------------+
    |
    v
+-------------------+
|  DispatcherServlet |  5. 视图渲染或直接写入响应体
+-------------------+
    |
    | 6. 响应开始原路返回
    v
+------------------+
|  Filter 3         |  post: 添加安全响应头 (X-Content-Type-Options 等)
+------------------+
    |
    v
+------------------+
|  Filter 2         |  post: (无操作)
+------------------+
    |
    v
+------------------+
|  Filter 1         |  post: 计算请求耗时并写入日志
+------------------+
    |
    v
响应返回给客户端

从这个流程图中可以清晰看出两个关键事实。第一,过滤器的执行范围明显大于拦截器,从请求进入容器到响应离开容器,过滤器全程在场;而拦截器只在 DispatcherServlet 和 Controller 之间的那一段路程中活动。第二,执行顺序是先过滤后拦截,返回时是先拦截后过滤,形成完全对称的"夹心"结构。


三、回调接口的逐一剖析

3.1 Filter 接口的三个方法

public interface Filter {
    void init(FilterConfig filterConfig) throws ServletException;
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
    void destroy();
}

init() 在过滤器被实例化后由容器调用,且只调用一次。你可以在这里读取 web.xml 或注解中配置的初始化参数。比如一个 IP 黑名单过滤器可以在 init() 中加载黑名单文件并缓存到内存。需要注意的是,默认情况下 Servlet 容器在启动时不会立即初始化过滤器,而是在第一个匹配的请求到来时才初始化。如果过滤器初始化比较耗时,建议在配置中将 loadOnStartup 设为 true,避免第一个请求的响应延迟。

doFilter() 是过滤器的核心方法,每个匹配的请求都会触发一次。方法签名中传入的是 ServletRequestServletResponse,而非 HttpServletRequestHttpServletResponse,这是为了保持协议无关性。在实际使用中,我们通常会做一次强制类型转换,因为 HTTP 场景占据了绝大多数。FilterChain 参数是责任链的"链环",调用 chain.doFilter() 相当于说"我处理完了,下一个"。如果在调用 chain.doFilter() 之前就返回了(比如发现 IP 在黑名单中直接写回 403),那么这个请求就永远不会到达后续的过滤器和目标 Servlet。

destroy() 在容器卸载过滤器之前调用,通常用于释放 init() 中获取的资源,比如关闭数据库连接池、清理缓存等。大多数轻量级过滤器无需重写这个方法。

3.2 HandlerInterceptor 接口的三个方法

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception;
    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception;
    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            Exception ex) throws Exception;
}

preHandle() 在 Controller 方法执行之前调用。这个方法的返回值决定了请求能否继续走下去:返回 true 表示放行,返回 false 表示拦截。当你返回 false 时,后续的所有拦截器和 Controller 都不会被执行,请求在这个拦截器这里就终结了。这是 Web 开发中最常用的鉴权入口:校验 Token、检查权限、验证参数签名等逻辑都适合放在这里。preHandle() 的第三个参数 Object handler 在大多数情况下是一个 HandlerMethod 对象,里面封装了目标 Controller 的 Method 反射对象,你可以通过 handler.getClass().isAssignableFrom(HandlerMethod.class) 来判断,然后读取方法上的注解。

postHandle() 在 Controller 方法执行之后、视图渲染之前调用。这个方法的特殊之处在于它的第四个参数 ModelAndView,你可以在这里对模型数据做追加或修改。不过现在前后端分离的架构越来越普遍,大多数 Controller 直接返回 JSON 而非视图,此时 ModelAndView 通常为 null。另外,有一个重要的细节必须注意:如果 Controller 执行过程中抛出了异常,postHandle() 将不会被执行,因为此时 DispatcherServlet 会跳过正常的视图渲染流程,直接进入异常处理分支。

afterCompletion() 在整个请求处理完成之后调用,这里的"完成"包括正常返回和异常抛出两种情况。也就是说,无论 Controller 是成功执行、还是中途抛异常被全局异常处理器捕获,afterCompletion() 都一定会被执行。这个特性使它成为资源清理的理想场所,比如清除 ThreadLocal 中的用户上下文、释放数据库连接等。你可以通过第四个参数 Exception ex 来判断请求是否是因异常而结束的:如果 exnull,说明请求正常完成;否则可以通过 ex 的类型来做不同的善后处理。


四、使用场景的深度剖析

4.1 为什么这些场景适合用过滤器

过滤器的最大优势在于它的覆盖范围最广——从请求进入 Servlet 容器的第一刻起它就能介入,到响应离开容器的最后一刻它还在场。这种"从头管到尾"的特性使它在以下场景中不可替代。

字符编码设置是最经典也最容易理解的过滤器场景。HTTP 请求的编码问题必须在解析请求参数之前解决,否则一旦读取了 request.getParameter(),后续再设编码就无效了。过滤器在请求到达任何 Servlet 之前就能调用 request.setCharacterEncoding("UTF-8"),完美卡住了时间窗口。Spring 框架自己也深知这一点,所以内置了 CharacterEncodingFilter 来解决这个问题。

请求/响应体的包装也是过滤器的专属领域。HttpServletRequest 中的输入流(getInputStream()getReader())只能读取一次,如果你需要在过滤器中读取请求体做验签或日志,那么后续的 Controller 就无法再次读取请求体了。此时你需要用 HttpServletRequestWrapper 包装原始请求,把请求体缓存到一个 byte[] 中,然后在 getInputStream() 中返回一个基于这个缓存的新流。同理,如果你想在过滤器中修改响应体内容,比如对 JSON 响应做加密或压缩,就需要用 HttpServletResponseWrapper 来拦截输出流,这在过滤器中是常规操作,而在拦截器中是很难做到的。

跨域处理 (CORS) 虽然也可以在后端 Controller 上用 @CrossOrigin 注解来逐接口声明,但在实际项目中跨域通常是全局需求。过滤器可以在每个响应上统一添加 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等头信息,而且能处理浏览器在正式请求前发送的 OPTIONS 预检请求。过滤器在容器层面拦截预检请求后,直接返回 200 状态码和必要的 CORS 头,避免了预检请求穿透到业务逻辑层的开销。

XSS 和 SQL 注入防御作为全局安全措施,必须覆盖所有请求入口,包括登录接口、文件上传接口、甚至是未匹配到任何 Controller 的异常请求。如果只在拦截器里做安全过滤,攻击者完全可以发送一个不经过 DispatcherServlet 的请求(比如请求一个不存在的静态资源路径)来探测你的防御机制。过滤器在容器层对每一个进入的请求做参数清洗,才能实现真正意义上的纵深防御。

静态资源访问控制是一个容易被忽略但实际很常见的需求。假设你的系统中有一些 PDF 文件或报表文件只允许登录用户下载,这些文件通常以静态资源的形式存放在 /static/reports/ 下。如果只在前端做权限校验,用户可以直接通过浏览器地址栏访问文件 URL 绕开校验。在过滤器中验证请求中的 Token 或 Cookie,然后决定是否允许访问这些文件,是从根本上堵住这个安全漏洞的方法。

URL 重写和重定向在过滤器层面处理也非常自然。比如你要把旧版 API 的 /v1/users 统一重定向到 /v2/users,在过滤器中直接检查 URL 前缀,命中后调用 response.sendRedirect() 即可。如果将这个逻辑放在拦截器里,那么只有那些原本就匹配到 Controller 的请求才会被处理,如果一个接口在新版中已经完全不存在了,拦截器根本感知不到这个请求。

限流降级是保护后端服务不被突发流量击垮的重要手段。过滤器位于整个请求链的最前端,在这里做请求计数和限流判断效率最高——恶意请求在进入 DispatcherServlet 和业务逻辑之前就被拦截了,不会浪费任何服务器的 CPU 和内存资源。常用的做法是在过滤器中结合 Redis 的滑动窗口或令牌桶算法,统计每个用户或每个 IP 的请求频率,超出阈值直接返回 429 (Too Many Requests)。

4.2 为什么这些场景适合用拦截器

拦截器的优势在于它能精确感知和控制"即将被哪个 Controller 的哪个方法处理"这件事。这种粒度级的信息使它天然适合做与业务逻辑紧密相关的处理。

用户认证鉴权放在拦截器中是最自然的做法。拦截器的 preHandle() 能拿到请求头中的 Token,调用认证服务验证 Token 的有效性,如果 Token 过期或无效直接返回 false 即可中断整个请求。这比过滤器更灵活的地方在于,拦截器可以通过 excludePathPatterns("/api/login", "/api/register") 精确排除不需要鉴权的接口,而不是像过滤器那样只能通过 URL 模式匹配来做粗粒度的放行。更重要的是,拦截器能结合方法注解实现更灵活的鉴权策略,比如在某个特定接口上标注 @RequireRole("ADMIN"),然后通过反射读取这个注解来判断当前用户是否有管理员权限。

权限控制比认证更进一步。认证回答的是"你是谁",权限回答的是"你能做什么"。拦截器可以在 preHandle() 中通过 handler 参数获取到目标方法的完整签名,然后根据用户角色和接口所需权限做匹配判断。这种 RBAC 或 ABAC 的权限校验逻辑与业务模型强关联,属于典型的业务层处理,放在拦截器中符合架构的分层原则。

操作日志记录需要记录丰富的信息:谁在什么时候做了什么操作、用的是什么接口、传了什么参数、得到什么结果。过滤器中能拿到的信息仅限于 URL 和基础请求头,无法分辨这个请求到底是在"新增用户"还是在"修改密码"。而拦截器通过 handler 可以获取到 Controller 方法上的 @ApiOperation 注解或自定义的 @Log 注解,从中提取操作名称和描述。配合 afterCompletion() 在请求结束后统一写入日志,可以做到对业务代码完全无侵入。

接口性能监控如果放在过滤器里,你只能统计每个 URL 的响应时间,比如 /api/users/{id} 这种带路径参数的 URL 会被统计为独立的 URL。而拦截器因为知道对应的 Controller 方法,可以将不同 ID 的用户查询统一归类到 UserController.getUserById() 这个方法上进行统计,数据更有意义也更便于对比分析。

参数预处理在日常开发中有非常多的实践。比如前端传过来的日期参数格式不统一,有的接口传 2024-01-01,有的传时间戳 1704067200000。如果在每个 Controller 里都写一遍格式转换,代码会非常冗长且容易出错。在拦截器中统一识别参数类型,对 @RequestParam@RequestBody 中的日期字段做统一的格式规范化,可以让 Controller 中的业务逻辑保持纯粹。

数据脱敏是针对接口返回数据的隐私保护措施。拦截器的 postHandle()afterCompletion() 在 Controller 执行之后被调用,可以从返回的响应体中提取手机号、身份证号、银行卡号等敏感字段,按照脱敏规则(比如手机号中间四位替换为 ****)进行处理后再写回响应。这种后置处理在拦截器中实现,可以做到全局自动生效,不需要在每个接口返回前手动调用脱敏工具类。

4.3 场景选择决策树

是否需要拦截静态资源请求 (如 .png、.css、.js)?
    |
    ├── 是 -> 使用 Filter
    |        (拦截器无法感知不经过 DispatcherServlet 的请求)
    |
    └── 否 -> 是否需要获取 Spring 容器中的其他 Bean?
    |             |
    |             ├── 是 -> 使用 Interceptor
    |             |        (Filter 无法自动注入 Bean, 需额外做代理或手动从
    |             |         ApplicationContext 获取, 代码不够优雅)
    |             |
    |             └── 否 -> 是否需要修改请求体或响应体的原始字节流?
    |                           |
    |                           ├── 是 -> 使用 Filter
    |                           |        (只有 Filter 能用 HttpServletRequestWrapper
    |                           |         和 HttpServletResponseWrapper 包装流)
    |                           |
    |                           └── 否 -> 是否需要根据 Controller 方法做差异化处理?
    |                                         |
    |                                         ├── 是 -> 使用 Interceptor
    |                                         |        (Filter 中拿不到 handler 信息,
    |                                         |         无法区分具体是哪个方法)
    |                                         |
    |                                         └── 否 -> 二者均可, 优先使用 Interceptor
    |                                                  (在 Spring 项目中 Interceptor
    |                                                   的集成体验更一致, 调试更方便)

五、在 Spring Boot 中的注册与排序

5.1 过滤器注册

在 Spring Boot 项目中,注册过滤器有两种主流方式。第一种是通过 FilterRegistrationBean 以编程方式注册,这是推荐做法,因为可以精确控制过滤器的顺序和 URL 匹配模式。第二种是直接在过滤器类上加上 Servlet 3.0 提供的 @WebFilter 注解,然后在启动类上添加 @ServletComponentScan 来扫描。两种方式本质上都是向 Servlet 容器注册过滤器,但在 Spring Boot 环境下,使用 FilterRegistrationBean 配合 @Order 注解能更方便地与其他 Filter 协调顺序。

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<EncodingFilter> encodingFilter() {
        FilterRegistrationBean<EncodingFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new EncodingFilter());
        // 匹配所有请求路径
        registration.addUrlPatterns("/*");
        // 数字越小优先级越高, 越先执行
        registration.setOrder(1);
        // 设置初始化参数
        registration.addInitParameter("encoding", "UTF-8");
        registration.addInitParameter("forceEncoding", "true");
        return registration;
    }

    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CorsFilter());
        registration.addUrlPatterns("/api/*");
        registration.setOrder(2);
        return registration;
    }
}

关于过滤器顺序:setOrder() 的值越小,过滤器在链中的位置越靠前。假设编码过滤器的 order 为 1,跨域过滤器的 order 为 2,那么编码过滤器会先执行 pre 逻辑,后执行 post 逻辑。这个顺序在调试时非常重要,比如跨域过滤器必须在编码过滤器之后执行,否则预检请求 OPTIONS 调用的 setCharacterEncoding() 会因提前返回而失效。

5.2 拦截器注册

拦截器的注册相对简单,只需要实现 WebMvcConfigurer 接口并重写 addInterceptors() 方法。InterceptorRegistry 提供了链式 API 来精确配置拦截器的路径匹配规则。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Autowired
    private LogInterceptor logInterceptor;

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器: 拦截所有 /api/** 路径, 但排除登录和注册接口
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**", "/api/login", "/api/register")
                .order(1);

        // 操作日志拦截器: 拦截所有 /api/** 路径, 不排除任何接口
        registry.addInterceptor(logInterceptor)
                .addPathPatterns("/api/**")
                .order(2);

        // 限流拦截器: 只针对高消耗接口
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/report/**", "/api/export/**")
                .order(3);
    }
}

拦截器的 order() 控制的是 preHandle() 的执行顺序,值越小越先执行。而 postHandle()afterCompletion() 的执行顺序与 preHandle() 完全相反。这一点和 Filter 的栈式调用逻辑是一致的。


六、生命周期对比

过滤器的生命周期由 Servlet 容器管理,经历三个阶段:构造与 init() 初始化、doFilter() 处理每个请求、destroy() 销毁。在整个容器运行期间,每个过滤器类只有一个实例(单例),所有请求共享同一个过滤器实例,因此过滤器必须是线程安全的——不要在过滤器中持有线程不安全的成员变量。

拦截器的生命周期受 Spring IoC 容器管理。拦截器类通过 @Component 标注后成为 Spring Bean,默认也是单例模式。不过 Spring 也允许将拦截器声明为其他作用域(如 @Scope("prototype")),使其在每个请求中创建一个新实例,不过这在拦截器场景中极少使用,因为单例模式配合无状态的代码设计已经能满足绝大多数需求。

两者的初始化时机也有微妙区别。Filter 的 init() 在 Servlet 容器装载该 Filter 时调用,这个时机可能发生在 Spring 容器初始化之前或之后,取决于容器的实现。而 Interceptor 因为是 Spring Bean,其初始化一定在 Spring 容器启动过程中完成。这意味着如果你的 Filter 中需要访问数据库或调用远程服务,你不能直接在 init() 中通过 @Autowired 注入——此时 Spring 容器可能还没准备好。需要在 doFilter() 中通过 WebApplicationContextUtils 手动获取 Spring 上下文,或者改用 DelegatingFilterProxy 让 Spring 来代理你的 Filter。


七、常见误区与最佳实践

误区一:所有鉴权都应该放在拦截器中

拦截器确实适合做鉴权,但前提是你的静态资源鉴权需求已经在别处解决了(比如通过 Nginx)。如果你希望在 Tomcat 层面就对静态资源做访问控制,过滤器是更好的选择。正确的做法是根据资源类型分工:用过滤器做容器级别的安全策略(IP 白名单、基础认证),用拦截器做业务级别的精细化权限控制。

误区二:过滤器和拦截器不能同时使用同一个路径

两者不仅可以共存,而且在实际项目中几乎必然会同时使用同一个路径。比如 /api/users/** 路径可能同时被编码过滤器、跨域过滤器和登录拦截器、日志拦截器处理。关键在于理解它们的执行顺序:Filter 先于 Interceptor 执行,相同过滤器或拦截器之间的顺序由 order 控制。

误区三:在拦截器中直接操作响应流

很多开发者在拦截器中尝试通过 response.getWriter()response.getOutputStream() 写入内容,然后在 Controller 中又写入了一次,导致 IllegalStateException(getWriter() has already been called for this response)。如果你需要在拦截器中间接返回 JSON(比如打印错误信息),必须确保后续的 preHandle() 或 Controller 不会再尝试写入响应。通常的做法是在拦截器中设置一个请求属性标记,然后在后续的拦截器或 Controller 中检查这个标记并跳过写入。

误区四:在过滤器中做复杂的业务判断

过滤器设计上应该保持轻量和高效,因为每个请求都会经过过滤器链。如果在过滤器中执行了耗时的数据库查询、复杂的正则匹配或远程 HTTP 调用,所有请求(包括对静态资源的请求)都会被拖慢。这类操作应该放到拦截器中,因为拦截器只作用于经过 DispatcherServlet 的请求,并且可以精准地只应用于需要这些逻辑的特定 URL 模式。

最佳实践

  1. 过滤器只做通用基础处理:编码、安全头、请求包装、CORS、参数清洗,这些对所有请求无差别适用的逻辑放在过滤器中
  2. 拦截器做业务相关处理:认证、授权、操作日志、参数预处理、返回脱敏,这些需要感知具体业务上下文的逻辑放在拦截器中
  3. 异常处理在两者中要一致:无论是过滤器还是拦截器抛出的异常,都应该能被全局异常处理器捕获并统一格式化返回。为此,过滤器中的异常建议用 try-catch 包裹后转发给 Spring 的异常处理机制,而不是直接在过滤器中写响应
  4. 不要在过滤器和拦截器间重复实现相同逻辑:比如在过滤器中已经做了 Token 解析并存入了 ThreadLocal,拦截器中就应该直接从 ThreadLocal 取值,而不是再解析一遍

八、总结对比表

对比维度 过滤器 (Filter) 拦截器 (Interceptor)
所属层次 Servlet 容器层,属于 Java EE 基础设施 Spring MVC 框架层,属于 Spring 生态
实现基础 责任链模式,基于函数回调调用 chain.doFilter() 反射 + 动态代理,基于 AOP 思想
依赖关系 只依赖 Servlet 容器,不依赖 Spring 强依赖 Spring IoC 容器和 DispatcherServlet
作用范围 所有匹配 URL 的请求,包括静态资源和未映射的路径 仅经过 DispatcherServlet 的请求,即命中 Controller 的请求
执行粒度 粗粒度,只能通过 URL 模式控制 细粒度,可精确到 Controller 类或方法,支持排除特定路径
能否感知目标 Handler 不能,不知道请求将由哪个方法处理 可以,通过 preHandle()handler 参数获取
能否注入 Spring Bean 原生不支持,需通过 DelegatingFilterProxy 原生支持,拦截器本身就是 Spring Bean
能否修改请求/响应体 可以,通过 HttpServletRequestWrapperHttpServletResponseWrapper 不能直接修改字节流,只能修改参数或返回值
回调方法 init() / doFilter() / destroy() preHandle() / postHandle() / afterCompletion()
异常时的行为 需自行 try-catch,否则异常会向上抛给容器 postHandle() 不执行,afterCompletion() 一定执行
执行次数 一次请求一次 doFilter preHandle + postHandle + afterCompletion 各一次
线程安全要求 单例,必须线程安全 默认单例,必须线程安全
适用层次 基础设施层,与协议和容器相关 业务逻辑层,与 Controller 和业务相关

一句话原则:和容器、协议、基础设施相关的通用处理用过滤器,和业务逻辑、权限、数据上下文相关的处理用拦截器。