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)