1. 为什么需要Filter:从Servlet的痛点说起
第一次接触Java Web开发时,我像大多数新手一样直接从Servlet开始写起。很快发现一个头疼的问题:每个Servlet里都重复写着字符编码设置、权限校验、日志记录的代码。比如用户权限检查,理论上每个需要登录的接口都要先执行验证逻辑,但总不能在每个Servlet的doGet()方法里都copy一遍相同的代码吧?
2000年左右的Java EE规范制定者们也遇到了同样的问题。他们给出的解决方案就是Filter——一种可以"过滤"请求和响应的组件。想象一下安检流程:所有乘客(请求)必须经过安检仪(Filter)才能进入候机区(Servlet),而随身行李(响应)离开时还要再过一次安检。这种设计完美解决了横切关注点(Cross-cutting Concerns)的问题。
提示:横切关注点指那些会影响到多个模块的功能需求,比如日志、安全、事务等。它们不适合放在业务逻辑里,但又必须被统一处理。
2. Filter核心机制解剖
2.1 生命周期三阶段
Filter的生命周期比Servlet简单得多,只有三个关键节点:
-
初始化:Web容器启动时调用init(),通常在这里读取配置参数
java复制public void init(FilterConfig config) throws ServletException { String param = config.getInitParameter("threshold"); this.threshold = Integer.parseInt(param); } -
执行过滤:每次请求都会触发doFilter(),这是主战场
java复制public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 预处理逻辑 HttpServletRequest httpReq = (HttpServletRequest) req; System.out.println("拦截到请求:" + httpReq.getRequestURI()); chain.doFilter(req, res); // 放行到下一个过滤器或Servlet // 后处理逻辑 System.out.println("响应状态码:" + ((HttpServletResponse)res).getStatus()); } -
销毁:容器关闭时调用destroy()释放资源
2.2 过滤链(FilterChain)的运作原理
当我在Tomcat源码中看到ApplicationFilterChain类时,终于明白了过滤链的奥秘。它内部维护了一个Filter数组和当前执行位置的索引,调用chain.doFilter()时:
- 索引未到末尾:执行下一个Filter的doFilter()
- 索引到达末尾:执行目标Servlet的service()
- 方法返回时:逆向执行各Filter的后处理代码
这种设计实现了经典的责任链模式,就像多米诺骨牌一样逐个触发。
3. 五种实战过滤器实现
3.1 字符编码过滤器(必备基础款)
这是每个JavaWeb项目都应该配置的基础Filter:
java复制public class EncodingFilter implements Filter {
private String encoding;
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
req.setCharacterEncoding(encoding);
res.setCharacterEncoding(encoding);
chain.doFilter(req, res);
}
public void init(FilterConfig config) {
this.encoding = config.getInitParameter("encoding") != null ?
config.getInitParameter("encoding") : "UTF-8";
}
}
在web.xml中配置:
xml复制<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>com.example.EncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3.2 权限验证过滤器(Session方案)
一个典型的登录校验实现:
java复制public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpSession session = httpReq.getSession(false);
if (session == null || session.getAttribute("user") == null) {
((HttpServletResponse)res).sendRedirect("/login");
return;
}
chain.doFilter(req, res);
}
}
3.3 接口耗时统计过滤器(性能监控)
记录每个API的执行时间:
java复制public class TimingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(req, res);
long cost = System.currentTimeMillis() - start;
String uri = ((HttpServletRequest)req).getRequestURI();
System.out.printf("[%s] 耗时:%dms%n", uri, cost);
}
}
3.4 XSS防御过滤器(安全必备)
处理请求参数的HTML转义:
java复制public class XssFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XssRequestWrapper((HttpServletRequest)req), res);
}
}
class XssRequestWrapper extends HttpServletRequestWrapper {
public XssRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
return escapeHtml(super.getParameter(name));
}
private String escapeHtml(String input) {
if (input == null) return null;
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
}
}
3.5 响应压缩过滤器(性能优化)
对大数据量响应进行Gzip压缩:
java复制public class GzipFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpServletResponse httpRes = (HttpServletResponse) res;
if (acceptsGzip(httpReq)) {
httpRes.setHeader("Content-Encoding", "gzip");
chain.doFilter(httpReq, new GzipResponseWrapper(httpRes));
} else {
chain.doFilter(req, res);
}
}
private boolean acceptsGzip(HttpServletRequest req) {
String acceptEncoding = req.getHeader("Accept-Encoding");
return acceptEncoding != null && acceptEncoding.contains("gzip");
}
}
4. 注解配置 vs web.xml配置
4.1 注解方式(Servlet 3.0+)
现代项目更推荐使用注解:
java复制@WebFilter(
filterName = "myFilter",
urlPatterns = "/*",
initParams = {
@WebInitParam(name = "param1", value = "value1")
}
)
public class MyFilter implements Filter {
// 实现代码
}
优点:
- 代码与配置一体,维护方便
- 避免web.xml过于臃肿
- 支持动态注册(通过ServletContext.addFilter())
4.2 传统web.xml配置
仍然适用于:
- 需要动态控制Filter顺序的场景
- 老版本Servlet容器(低于3.0)
- 需要集中管理所有配置的情况
顺序控制示例:
xml复制<filter-mapping>
<filter-name>filter1</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<filter-mapping>
<filter-name>filter2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
filter1会先于filter2执行,因为它在配置文件中先声明。
5. 高频面试题深度剖析
5.1 Filter与Interceptor区别
这个问题在面试中出现频率超高,我整理了一个对比表格:
| 特性 | Filter | Interceptor |
|---|---|---|
| 所属规范 | JavaEE标准 | Spring框架特有 |
| 执行位置 | Servlet容器层面 | Spring MVC流程中 |
| 依赖关系 | 不依赖Spring | 需要Spring上下文 |
| 获取Bean | 需要通过WebApplicationContext | 直接@Autowired注入 |
| 异常处理 | 只能处理ServletException | 可以处理所有异常 |
| 适用场景 | 字符编码、安全过滤等基础处理 | 业务相关的预处理/后处理 |
5.2 过滤器的执行顺序问题
实际项目中遇到过因Filter顺序导致的Bug,总结出三条黄金法则:
- web.xml声明顺序:先定义的先执行(请求时),响应时逆序
- 注解方式顺序:默认按类名排序,可用@Order控制
- 混合配置顺序:web.xml中的Filter永远先于注解方式执行
踩坑记录:曾经因为编码过滤器在权限过滤器之后执行,导致权限检查时中文参数乱码,排查了半天才发现顺序问题。
5.3 如何实现Filter的懒加载
默认情况下Filter随容器启动立即初始化,如果初始化耗时较长会影响启动速度。解决方案:
java复制@WebFilter(
urlPatterns = "/*",
initParams = {...},
asyncSupported = true // 关键配置
)
public class LazyFilter implements Filter {
// 实现代码
}
配合Servlet 3.0的异步特性,可以实现按需初始化。
6. 性能优化实战技巧
6.1 减少不必要的过滤
通过合理配置url-pattern提高效率:
xml复制<!-- 不好:所有请求都经过过滤器 -->
<url-pattern>/*</url-pattern>
<!-- 更好:只过滤需要处理的路径 -->
<url-pattern>/api/*</url-pattern>
<url-pattern>/admin/*</url-pattern>
6.2 线程安全注意事项
Filter默认是单例多线程环境,实例变量需要特别注意:
java复制public class UnsafeFilter implements Filter {
private int count; // 危险!多线程共享
public void doFilter(...) {
count++; // 非原子操作
}
}
// 正确做法:使用Atomic变量或局部变量
public class SafeFilter implements Filter {
private final AtomicInteger count = new AtomicInteger(0);
public void doFilter(...) {
count.incrementAndGet();
}
}
6.3 异步处理支持
Servlet 3.0+环境下可以启用异步处理:
java复制@WebFilter(asyncSupported = true)
public class AsyncFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
AsyncContext asyncCtx = req.startAsync();
asyncCtx.start(() -> {
try {
chain.doFilter(asyncCtx.getRequest(), asyncCtx.getResponse());
} catch (Exception e) {
// 异常处理
} finally {
asyncCtx.complete();
}
});
}
}
7. 常见问题排查指南
7.1 Filter不生效的五大原因
- url-pattern配置错误:检查是否匹配目标URL
- Filter顺序问题:前面的Filter没有调用chain.doFilter()
- 缺少Servlet依赖:比如忘记添加@WebServlet注解
- 异常被吞没:Filter中抛出异常但未正确处理
- 容器版本问题:比如Tomcat版本过低不支持某些特性
7.2 内存泄漏排查
Filter中常见的内存泄漏场景:
java复制public class LeakFilter implements Filter {
private static List<Object> cache = new ArrayList<>(); // 危险!
public void doFilter(...) {
cache.add(...); // 元素只增不减
}
}
解决方案:
- 使用WeakReference
- 定期清理缓存
- 避免静态集合长期持有请求相关对象
7.3 跨域处理最佳实践
现代前后端分离项目中常用的CORS过滤器:
java复制public class CorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest)req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
}
