Spring 里的过滤器(Filter)和拦截器(Interceptor)到底啥区别?

在做 Spring Boot 开发时,经常会听到两句话:"用 Filter 做统一处理"、"用 Interceptor 拦截请求"。很多同学会混淆:它们是不是都是 Spring 的?它们在请求链路的哪个位置?适合干嘛?Spring Boot 里怎么写?

这篇文章用简单的方式讲清楚 Filter 和 Interceptor 的定位,并给出两个能直接复制运行的例子,最后用一个总结把选择建议说透。

1. 先下结论:它们归属不同

Filter(过滤器)不是 Spring 发明的,它属于 Servlet 规范,由 Tomcat/Jetty 等容器负责调用。它的位置更靠外,在请求进入 Spring MVC 之前就会执行。

Interceptor(拦截器)通常指 Spring MVC 的 HandlerInterceptor,是 Spring MVC 的能力。它的位置更靠内,请求进入 DispatcherServlet 之后、Controller 方法执行前后才会触发。

2. 用"洋葱模型"理解位置关系

请求从外往内走:

Tomcat → Filter(Servlet 规范)→ DispatcherServlet(Spring MVC 入口)→ Interceptor(Spring MVC)→ Controller

响应返回时,再按相反方向回去。

3. 什么时候用 Filter?什么时候用 Interceptor?

Filter 更适合做"全站通用、跟业务无关"的事情,例如编码处理、全局 CORS、XSS 过滤、RequestWrapper(读 body/改 header)、统一访问日志、限流、以及安全链路相关处理。很多人不知道的是,Spring Security 本质上就是一条很长的 FilterChain。

Interceptor 更适合做"跟 Controller/业务强相关"的事情,例如登录态/权限校验(尤其需要拿到 Controller 方法或注解时)、接口埋点统计每个 Controller 的耗时、统一注入上下文(userId、traceId)并在 afterCompletion 清理 ThreadLocal/MDC、多租户 tenant 上下文等。

4. 最关键区别:触发时机 & 能拿到的信息

维度 Filter Interceptor
归属 Servlet 规范(容器调用) Spring MVC(DispatcherServlet 调用)
执行时机 更早(进入 Spring MVC 之前) 更晚(进入 Spring MVC 之后,Controller 前后)
是否能拿到 Controller 方法 不知道具体 handler 能拿到 handler(方法/注解)
典型用途 全局通用处理、安全过滤、包装 request 业务校验、埋点、上下文管理、权限

5. 例子 1:Filter 记录全站耗时(完整可用)

目标是打印每个请求的 URI、线程名、耗时,并且能直观看到它在 Controller 前后执行。推荐继承 OncePerRequestFilter,避免一次请求多次执行的问题。

scala 复制代码
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class AccessLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        long start = System.currentTimeMillis();
        String uri = request.getRequestURI();
        String thread = Thread.currentThread().getName();

        try {
            System.out.println("[Filter] before chain, uri=" + uri + ", thread=" + thread);
            filterChain.doFilter(request, response); // 放行,进入 DispatcherServlet / Controller
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println("[Filter] after chain , uri=" + uri + ", cost=" + cost + "ms");
        }
    }
}

理解要点是:filterChain.doFilter 前是"进 Spring MVC 之前",doFilter 后是"Controller 和后续处理都结束后"。

6. 例子 2:Interceptor 统一注入 userId + traceId,并正确清理(完整可用)

这个例子更贴近真实线上:从请求头/参数拿 userId,生成 traceId,放到 ThreadLocal,Controller 里随时能取,同时在 afterCompletion 清理,避免线程池复用导致串号和"线程级常驻"。

先定义一个上下文对象和 ThreadLocal 容器。

RequestContext.java:

arduino 复制代码
public class RequestContext {
    private final String userId;
    private final String traceId;

    public RequestContext(String userId, String traceId) {
        this.userId = userId;
        this.traceId = traceId;
    }

    public String getUserId() { return userId; }
    public String getTraceId() { return traceId; }
}

RequestContextHolder.java:

csharp 复制代码
public class RequestContextHolder {
    private static final ThreadLocal<RequestContext> CTX = new ThreadLocal<>();

    public static void set(RequestContext ctx) { CTX.set(ctx); }
    public static RequestContext get() { return CTX.get(); }
    public static void remove() { CTX.remove(); }
}

然后实现 Interceptor。

typescript 复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.UUID;

public class ContextInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String userId = request.getHeader("X-UserId");
        if (userId == null || userId.isBlank()) {
            userId = request.getParameter("userId");
        }
        if (userId == null || userId.isBlank()) {
            userId = "anonymous";
        }

        String traceId = UUID.randomUUID().toString().replace("-", "");
        RequestContextHolder.set(new RequestContext(userId, traceId));

        System.out.println("[Interceptor] preHandle, userId=" + userId + ", traceId=" + traceId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        RequestContextHolder.remove();
        System.out.println("[Interceptor] afterCompletion, cleaned");
    }
}

接着在 Spring Boot 中注册 Interceptor。

typescript 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ContextInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/health");
    }
}

最后写一个 Controller 验证上下文是否能拿到。

kotlin 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/demo")
    public String demo() {
        RequestContext ctx = RequestContextHolder.get();
        return "ok, userId=" + ctx.getUserId() + ", traceId=" + ctx.getTraceId();
    }
}

访问 /demo?userId=mm,你会看到输出顺序大致是:

Filter\] before chain \[Interceptor\] preHandle Controller 执行 \[Interceptor\] afterCompletion \[Filter\] after chain 这也能直观看到 Filter 在外层包着整个 Spring MVC,而 Interceptor 在 Spring MVC 内部围绕 Controller。 ### 总结 Filter 属于 Servlet 规范,位置更靠外,更适合做全站通用处理(日志、编码、CORS、安全过滤、请求包装等),它只关心 request/response,不知道具体会走哪个 Controller。 Interceptor 属于 Spring MVC,位置更靠内,更适合做与 Controller/业务相关的拦截(权限、埋点、上下文注入与清理、多租户等),它能拿到 handler(方法/注解),并且可以在 afterCompletion 做统一清理。 写 Filter 记住围绕 chain.doFilter 包一层;写 Interceptor 记住 preHandle 做拦截、afterCompletion 做清理(尤其 ThreadLocal/MDC)。

相关推荐
源码获取_wx:Fegn08952 小时前
基于springboot + vue物业管理系统
java·开发语言·vue.js·spring boot·后端·spring·课程设计
無量2 小时前
MySQL事务与锁机制深度剖析
后端·mysql
無量2 小时前
MySQL索引设计与优化实战
后端·mysql
木木一直在哭泣2 小时前
CAS 一篇讲清:原理、Java 用法,以及线上可用的订单状态机幂等方案
后端
王中阳Go2 小时前
我辅导400+学员拿Go Offer后发现:突破年薪50W,常离不开这10个实战技巧
后端·面试·go
Tortoise2 小时前
OpenTortoise:开箱即用的Java调用LLM中间件,一站式解决配置、调用、成本监控和智能记忆
后端
摸鱼仙人~3 小时前
Flask-SocketIO 连接超时问题排查与解决(WSL / 虚拟机场景)
后端·python·flask
Lisonseekpan3 小时前
@Autowired 与 @Resource区别解析
java·开发语言·后端
chenyuhao20244 小时前
Linux系统编程:线程概念与控制
linux·服务器·开发语言·c++·后端