过滤器与拦截器

Spring Boot 中有三种拦截 HTTP 请求的机制,它们处于请求处理的不同层级:

客户端请求
    │
    ▼
┌─────────────────────────────────────┐
│   Servlet Filter(过滤器)           │  ← Servlet 规范,最外层
│  ┌───────────────────────────────┐  │
│  │  DispatcherServlet            │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │  HandlerInterceptor     │  │  │  ← Spring MVC 层
│  │  │  ┌───────────────────┐  │  │  │
│  │  │  │  Controller / AOP │  │  │  │  ← 方法级别
│  │  │  └───────────────────┘  │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

三者对比

对比项Filter(过滤器)HandlerInterceptor(拦截器)AOP
规范来源Servlet 规范Spring MVCSpring AOP
作用范围所有请求(含静态资源)仅经过 DispatcherServlet 的请求Spring Bean 方法
能获取的信息HttpServletRequest/ResponseHttpServletRequest/Response + HandlerMethod方法参数、返回值、注解
能否访问 Spring Bean需通过 WebApplicationContextUtils直接注入直接注入
执行时机最早Controller 前后方法执行前后
典型用途字符编码、CORS、安全认证登录校验、日志、权限日志、缓存、事务

更细粒度的方法级增强参见 AOP

Filter(过滤器)

实现方式

实现 OncePerRequestFilter 保证每次请求只执行一次(推荐):

@Component
public class RequestLogFilter extends OncePerRequestFilter {
 
    private static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class);
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        long start = System.currentTimeMillis();
        String uri = request.getRequestURI();
        String method = request.getMethod();
 
        try {
            chain.doFilter(request, response);  // 继续执行后续过滤器和 Servlet
        } finally {
            long cost = System.currentTimeMillis() - start;
            log.info("{} {} - {}ms - status:{}", method, uri, cost, response.getStatus());
        }
    }
 
    // 排除不需要过滤的路径
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getRequestURI().startsWith("/actuator");
    }
}

注册与排序

通过 FilterRegistrationBean 精确控制过滤器的 URL 范围和执行顺序:

@Configuration
public class FilterConfig {
 
    @Bean
    public FilterRegistrationBean<RequestLogFilter> requestLogFilter() {
        FilterRegistrationBean<RequestLogFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new RequestLogFilter());
        bean.addUrlPatterns("/api/*");       // 只过滤 /api/** 请求
        bean.setOrder(1);                    // 数字越小,优先级越高
        bean.setName("requestLogFilter");
        return bean;
    }
 
    @Bean
    public FilterRegistrationBean<AuthFilter> authFilter() {
        FilterRegistrationBean<AuthFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new AuthFilter());
        bean.addUrlPatterns("/api/*");
        bean.setOrder(2);                    // 在日志过滤器之后执行
        return bean;
    }
}

若 Filter 已通过 @Component 注册,@Bean FilterRegistrationBean@Component 只能选其一,否则过滤器会执行两次。

读取请求体(Body)

HttpServletRequest 的 InputStream 只能读一次,重复读取需包装:

public class CachingRequestBodyFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        ContentCachingRequestWrapper wrappedRequest =
            new ContentCachingRequestWrapper(request);
        chain.doFilter(wrappedRequest, response);
 
        // 此时才能安全读取 body(chain 执行后 body 已被缓存)
        byte[] body = wrappedRequest.getContentAsByteArray();
        log.debug("Request body: {}", new String(body, StandardCharsets.UTF_8));
    }
}

HandlerInterceptor(拦截器)

三个回调方法

@Component
public class AuthInterceptor implements HandlerInterceptor {
 
    @Autowired
    private TokenService tokenService;
 
    /**
     * Controller 执行前调用。
     * 返回 false 则中断请求,后续拦截器和 Controller 不再执行。
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 非 Controller 方法(如静态资源)直接放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
 
        HandlerMethod method = (HandlerMethod) handler;
 
        // 检查是否有 @SkipAuth 注解
        if (method.hasMethodAnnotation(SkipAuth.class)) {
            return true;
        }
 
        String token = request.getHeader("Authorization");
        if (!tokenService.validate(token)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("{\"message\":\"未授权\"}");
            return false;
        }
        return true;
    }
 
    /**
     * Controller 执行后、视图渲染前调用。
     * 发生异常时不会调用此方法。
     */
    @Override
    public void postHandle(HttpServletRequest request,
                          HttpServletResponse response,
                          Object handler,
                          ModelAndView modelAndView) {
        // 可在此修改 ModelAndView,常用于添加公共数据
    }
 
    /**
     * 请求完成后(视图渲染后)调用,无论是否发生异常。
     * 适合清理 ThreadLocal 等资源。
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                               HttpServletResponse response,
                               Object handler,
                               Exception ex) {
        UserContext.clear();  // 清理线程变量,防止内存泄漏
    }
}

注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    @Autowired
    private AuthInterceptor authInterceptor;
 
    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns(
                "/api/auth/login",
                "/api/auth/register",
                "/api/public/**"
            )
            .order(1);
 
        registry.addInterceptor(rateLimitInterceptor)
            .addPathPatterns("/api/**")
            .order(2);
    }
}

多拦截器执行顺序

假设有拦截器 A(order=1)和 B(order=2):

请求进入:
    A.preHandle  → B.preHandle → Controller
                                    ↓
    A.afterCompletion ← B.afterCompletion ← A.postHandle ← B.postHandle

B.preHandle 返回 false

A.preHandle → B.preHandle(false) → A.afterCompletion
(B 和 Controller 的后续不执行,已执行 preHandle 的拦截器 afterCompletion 仍会执行)

常见应用场景

登录状态校验(拦截器)

@Override
public boolean preHandle(HttpServletRequest request, ...) {
    String token = request.getHeader("Authorization");
    Claims claims = jwtUtil.parse(token);
    if (claims == null) {
        response.setStatus(401);
        return false;
    }
    // 将用户信息存入 ThreadLocal,Controller 中直接获取
    UserContext.set(new CurrentUser(claims.getSubject()));
    return true;
}

Token 相关实现参见 OAuth2与JWT,Session 管理参见 Session管理

CORS 跨域(过滤器)

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type");
 
        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        chain.doFilter(request, response);
    }
}

Spring MVC 内置的跨域配置参见 跨域处理

接口耗时统计(拦截器)

public class TimingInterceptor implements HandlerInterceptor {
 
    private static final String START_TIME = "startTime";
 
    @Override
    public boolean preHandle(HttpServletRequest request, ...) {
        request.setAttribute(START_TIME, System.currentTimeMillis());
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, ...) {
        long cost = System.currentTimeMillis() - (Long) request.getAttribute(START_TIME);
        if (cost > 1000) {
            log.warn("慢请求: {} {} - {}ms",
                request.getMethod(), request.getRequestURI(), cost);
        }
    }
}

异常处理

过滤器中的异常无法被 @ControllerAdvice 捕获(因为在 DispatcherServlet 之外),需自行处理:

@Override
protected void doFilterInternal(...) throws ServletException, IOException {
    try {
        chain.doFilter(request, response);
    } catch (TokenExpiredException e) {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write("{\"code\":401,\"message\":\"Token已过期\"}");
    }
}

拦截器中的异常会被 DispatcherServlet 转发给 @ControllerAdvice,参见 全局异常处理

如何选择

  • 过滤器:处理与 HTTP 协议相关的通用逻辑(字符编码、CORS、请求体缓存)、需要在 Spring MVC 之外执行的逻辑(如安全框架 Spring Security)
  • 拦截器:需要访问 Controller 元信息(注解、方法名)的逻辑,如权限校验、审计日志
  • AOP:需要访问方法参数和返回值的业务逻辑,如缓存(见 缓存)、事务(见 事务管理