1. 概述

在大多数 Web 应用中,我们常需要对请求进行统一处理,比如日志记录、参数校验或身份认证。这类任务通常会作用于一组 HTTP 接口(endpoint),而不是单个接口。

好消息是,Spring Web 框架为此提供了成熟的 Filter 机制,可以方便地实现横切逻辑。

本文将重点讲解:✅ 如何让某个 Filter 只作用于特定 URL,或 ❌ 明确排除某些 URL 不执行过滤逻辑。

这在实际开发中非常常见,比如健康检查接口 /health 通常不需要鉴权或限流,如果不做排除,反而会造成不必要的开销甚至故障,属于典型的“踩坑点”。


2. 针对特定 URL 的 Filter

假设我们的应用需要记录请求路径和内容类型(Content-Type),可以通过自定义一个日志 Filter 来实现。

2.1. 日志 Filter 实现

我们创建一个 LogFilter 类,继承 Spring 提供的 OncePerRequestFilter,并重写 doFilterInternal 方法:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String path = request.getRequestURI();
    String contentType = request.getContentType();
    logger.info("Request URL path : {}, Request content type: {}", path, contentType);
    filterChain.doFilter(request, response);
}

⚠️ 使用 OncePerRequestFilter 而不是原生 Filter,是为了确保在整个请求生命周期中只被执行一次,避免在异步或转发时重复执行。

2.2. 白名单方式:只对指定 URL 生效(Rule-in)

如果我们希望日志功能仅对 /health/faq/* 这些接口生效,可以通过 FilterRegistrationBean 显式指定匹配的 URL 模式:

@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
    FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new LogFilter());
    registrationBean.addUrlPatterns("/health", "/faq/*");
    return registrationBean;
}

这种方式简单粗暴,适用于明确知道哪些接口需要被拦截的场景。

2.3. 黑名单思路:排除某些 URL(Rule-out)

如果原本是全局拦截,现在想排除某些接口,有两种做法:

  • 新增接口时,确保其 URL 不匹配已注册的 Filter 模式
  • 对已有接口,调整 Filter 的 URL 匹配规则,将其排除

但注意:如果 Filter 使用了 /** 通配符匹配所有路径,上面的方法就不够用了,必须在代码层面做判断。


3. 全局匹配下的 Filter 处理

当 Filter 被配置为拦截所有请求(如使用 * 通配符)时,想排除某些 URL 就不能依赖注册时的模式匹配了,必须在 Filter 内部实现排除逻辑。

3.1. 自定义 Filter:基于请求头校验

举个例子:我们的服务目前仅支持美国用户,通过请求头 X-Country-Code 判断来源地。如果不是 "US",直接拒绝请求。

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

    String countryCode = request.getHeader("X-Country-Code");
    if (!"US".equals(countryCode)) {
        response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid Locale");
        return;
    }

    filterChain.doFilter(request, response);
}

这个 Filter 需要作用于绝大多数接口,因此我们希望它默认拦截所有请求。

3.2. 注册为全局 Filter

使用 FilterRegistrationBean 并通过 * 通配符注册,使其匹配所有 URL:

@Bean
public FilterRegistrationBean<HeaderValidatorFilter> headerValidatorFilter() {
    FilterRegistrationBean<HeaderValidatorFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new HeaderValidatorFilter());
    registrationBean.addUrlPatterns("*");
    return registrationBean;
}

此时,所有请求都会进入该 Filter。接下来的问题是:如何排除像 /health 这样的公共接口?


4. 如何正确排除 URL

4.1. 错误做法:在 doFilterInternal 中硬编码判断

最直观的方式是在 doFilterInternal 里加个 if 判断:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String path = request.getRequestURI();
    if ("/health".equals(path)) {
        filterChain.doFilter(request, response);
        return;
    }

    String countryCode = request.getHeader("X-Country-Code");
    if (!"US".equals(countryCode)) {
        response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid Locale");
        return;
    }

    filterChain.doFilter(request, response);
}

⚠️ 问题很明显
Filter 和具体接口路径耦合了!一旦 /health 改成 /ping,而忘了改这里的判断,健康检查就会被拦截,导致运维误判服务异常。

这不是优雅的解法,属于“临时补丁”,不推荐在生产环境使用。

4.2. 正确做法:重写 shouldNotFilter 方法

Spring 的 OncePerRequestFilter 提供了一个专门用于控制是否执行过滤逻辑的方法:shouldNotFilter(HttpServletRequest)

我们可以重写它,把“排除逻辑”单独抽出来:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    String path = request.getRequestURI();
    return "/health".equals(path);
}

这样,原来的 doFilterInternal 就可以保持纯净,只关注核心业务逻辑:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String countryCode = request.getHeader("X-Country-Code");
    if (!"US".equals(countryCode)) {
        response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid Locale");
        return;
    }
    filterChain.doFilter(request, response);
}

优点总结

  • 符合单一职责原则(SRP)
  • 排除规则集中管理,易于维护
  • 修改 URL 排除列表不会影响主逻辑
  • 更利于单元测试(可单独测试 shouldNotFilter

5. 总结

本文通过两个典型场景(日志记录、请求头校验),讲解了在 Spring Web 应用中如何灵活控制 Filter 的作用范围。

关键结论如下:

场景 推荐方案
Filter 仅对少数接口生效 使用 FilterRegistrationBean.addUrlPatterns(...) 白名单注册
需要排除个别接口,且 Filter 已全局匹配 重写 shouldNotFilter() 方法
简单判断、临时调试 可在 doFilterInternal 中加判断(但别上线)

📌 特别提醒:当 Filter 使用 * 通配符时,不要在 doFilterInternal 中做排除判断,否则会导致逻辑混乱和维护困难。

完整示例代码已托管至 GitHub:https://github.com/baeldung/spring-web-tutorials(模块:spring-web-url)


原始标题:Excluding URLs for a Filter in a Spring Web Application | Baeldung