Servlet过滤器详解
过滤器概述
Servlet过滤器(Filter)是Java Web开发中的一个重要组件,它位于客户端请求与目标资源之间,可以对请求和响应进行拦截、修改和处理。过滤器可以在不修改原有代码的情况下,动态地添加功能,实现横切关注点的处理。
过滤器的作用
- 请求拦截与处理:在请求到达目标资源之前进行预处理
- 响应拦截与处理:在响应返回给客户端之前进行后处理
- 资源访问控制:实现权限验证、登录检查等功能
- 数据转换:修改请求参数、响应内容等
- 日志记录:记录请求信息、响应时间等
- 压缩响应:对响应内容进行压缩处理
- 编码转换:统一处理字符编码
过滤器接口与生命周期
Filter接口
所有过滤器都必须实现javax.servlet.Filter接口,该接口定义了三个核心方法:
java
public interface Filter {
// 初始化方法
void init(FilterConfig filterConfig) throws ServletException;
// 过滤器核心方法,处理请求和响应
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
// 销毁方法
void destroy();
}生命周期方法详解
init方法
作用:过滤器初始化时调用,用于加载配置参数、初始化资源等。
- 在Web容器启动时创建过滤器实例并调用init方法
- 整个生命周期内只调用一次
- 通过
FilterConfig对象可以获取初始化参数和ServletContext
java
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 获取初始化参数
String encoding = filterConfig.getInitParameter("encoding");
// 获取ServletContext
ServletContext context = filterConfig.getServletContext();
// 初始化其他资源
}doFilter方法
作用:过滤器的核心方法,处理请求和响应。
- 每次请求匹配的URL时都会调用
- 可以访问请求和响应对象,修改它们的属性和状态
- 通过
FilterChain继续传递请求到下一个过滤器或目标资源 - 可以在调用
chain.doFilter()之前和之后执行处理逻辑
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 请求预处理
System.out.println("过滤器:请求处理前");
// 继续传递请求
chain.doFilter(request, response);
// 响应后处理
System.out.println("过滤器:响应处理后");
}destroy方法
作用:过滤器销毁时调用,用于释放资源。
- 在Web容器关闭时调用
- 整个生命周期内只调用一次
- 用于关闭数据库连接、释放文件资源等
java
@Override
public void destroy() {
// 释放资源
System.out.println("过滤器被销毁");
}过滤器的配置
过滤器可以通过两种方式进行配置:XML配置和注解配置。
XML配置方式
在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>
<!-- 可以指定特定的Servlet -->
<!-- <servlet-name>MyServlet</servlet-name> -->
</filter-mapping>注解配置方式
在Java EE 6及以上版本中,可以使用@WebFilter注解配置过滤器:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(
filterName = "encodingFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8")
}
)
public class EncodingFilter implements Filter {
private String encoding;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
encoding = filterConfig.getInitParameter("encoding");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(encoding);
response.setCharacterEncoding(encoding);
response.setContentType("text/html;charset=" + encoding);
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 释放资源
}
}过滤器链
当多个过滤器同时匹配一个URL时,它们会形成一个过滤器链。请求会按照配置的顺序依次通过每个过滤器,然后到达目标资源,响应则按照相反的顺序返回。
过滤器链执行顺序
- XML配置:按照
<filter-mapping>在web.xml中出现的顺序 - 注解配置:没有明确定义的顺序,但大多数容器按照类名的字母顺序
- 混合配置:XML配置的过滤器通常在注解配置的过滤器之前执行
过滤器链示例
java
// 过滤器1
@WebFilter(filterName = "Filter1", urlPatterns = "/test")
public class Filter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Filter1: 请求处理前");
chain.doFilter(request, response);
System.out.println("Filter1: 响应处理后");
}
// 其他方法略
}
// 过滤器2
@WebFilter(filterName = "Filter2", urlPatterns = "/test")
public class Filter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Filter2: 请求处理前");
chain.doFilter(request, response);
System.out.println("Filter2: 响应处理后");
}
// 其他方法略
}执行结果可能如下:
Filter1: 请求处理前
Filter2: 请求处理前
[目标资源处理]
Filter2: 响应处理后
Filter1: 响应处理后过滤器的应用场景
1. 字符编码过滤器
解决中文乱码问题:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(filterName = "EncodingFilter", urlPatterns = "/*")
public class EncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String initEncoding = filterConfig.getInitParameter("encoding");
if (initEncoding != null) {
encoding = initEncoding;
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 设置请求编码
request.setCharacterEncoding(encoding);
// 设置响应编码
response.setCharacterEncoding(encoding);
response.setContentType("text/html;charset=" + encoding);
// 继续处理请求
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 释放资源
}
}2. 登录验证过滤器
检查用户是否已登录,未登录则重定向到登录页面:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebFilter(filterName = "LoginFilter", urlPatterns = "/admin/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpSession session = httpRequest.getSession();
// 获取用户信息
Object user = session.getAttribute("user");
// 判断用户是否已登录
if (user == null) {
// 未登录,重定向到登录页面
String loginPath = httpRequest.getContextPath() + "/login.jsp";
httpResponse.sendRedirect(loginPath);
} else {
// 已登录,继续处理请求
chain.doFilter(request, response);
}
}
// 其他方法略
}3. 日志过滤器
记录请求信息和执行时间:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
@WebFilter(filterName = "LogFilter", urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 获取请求信息
String uri = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
String clientIP = httpRequest.getRemoteAddr();
// 记录请求开始时间
long startTime = System.currentTimeMillis();
System.out.println("[" + new Date() + "] Request: " + method + " " + uri + " from " + clientIP);
try {
// 处理请求
chain.doFilter(request, response);
} finally {
// 计算请求处理时间
long endTime = System.currentTimeMillis();
long processTime = endTime - startTime;
System.out.println("[" + new Date() + "] Response: " + uri + " processed in " + processTime + "ms");
}
}
// 其他方法略
}4. 压缩过滤器
对响应内容进行压缩,减少网络传输量:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
@WebFilter(filterName = "CompressionFilter", urlPatterns = {"*.html", "*.jsp", "*.js", "*.css"})
public class CompressionFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查浏览器是否支持压缩
String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
// 创建包装后的响应对象
CompressionResponseWrapper wrappedResponse = new CompressionResponseWrapper(httpResponse);
// 设置响应头,告知浏览器内容已压缩
wrappedResponse.setHeader("Content-Encoding", "gzip");
try {
// 处理请求
chain.doFilter(request, wrappedResponse);
} finally {
// 确保压缩流被正确关闭
wrappedResponse.finish();
}
} else {
// 浏览器不支持压缩,直接处理
chain.doFilter(request, response);
}
}
// 其他方法略
// 压缩响应包装器内部类
private static class CompressionResponseWrapper extends HttpServletResponseWrapper {
private GZIPServletOutputStream gzipStream;
private ServletOutputStream outputStream;
private PrintWriter writer;
public CompressionResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called");
}
if (outputStream == null) {
outputStream = getResponse().getOutputStream();
gzipStream = new GZIPServletOutputStream(outputStream);
}
return gzipStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called");
}
if (writer == null) {
gzipStream = new GZIPServletOutputStream(getResponse().getOutputStream());
writer = new PrintWriter(new java.io.OutputStreamWriter(gzipStream, getResponse().getCharacterEncoding()));
}
return writer;
}
@Override
public void setContentLength(int length) {
// 压缩后长度不确定,忽略设置
}
public void finish() throws IOException {
if (writer != null) {
writer.close();
} else if (gzipStream != null) {
gzipStream.close();
}
}
}
// GZIP输出流内部类
private static class GZIPServletOutputStream extends ServletOutputStream {
private GZIPOutputStream gzipStream;
public GZIPServletOutputStream(ServletOutputStream output) throws IOException {
this.gzipStream = new GZIPOutputStream(output);
}
@Override
public void write(int b) throws IOException {
gzipStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
gzipStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
gzipStream.write(b, off, len);
}
@Override
public void flush() throws IOException {
gzipStream.flush();
}
@Override
public void close() throws IOException {
gzipStream.close();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
// 可选实现
}
}
}过滤器的高级特性
1. 请求包装器和响应包装器
当需要修改请求或响应的行为时,可以使用ServletRequestWrapper和ServletResponseWrapper:
java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
// 请求包装器
class ParameterRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params;
public ParameterRequestWrapper(HttpServletRequest request) {
super(request);
// 复制原始参数
this.params = new HashMap<>(request.getParameterMap());
}
// 添加或修改参数
public void addParameter(String name, String value) {
params.put(name, new String[]{value});
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(params);
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
// 在过滤器中使用请求包装器
@WebFilter(filterName = "ParameterFilter", urlPatterns = "/process")
public class ParameterFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
// 创建请求包装器
ParameterRequestWrapper wrappedRequest = new ParameterRequestWrapper((HttpServletRequest) request);
// 添加新参数
wrappedRequest.addParameter("processedBy", "filter");
// 使用包装后的请求
chain.doFilter(wrappedRequest, response);
} else {
chain.doFilter(request, response);
}
}
// 其他方法略
}2. 异步过滤器
在Servlet 3.0及以上版本中,过滤器可以支持异步处理:
java
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(filterName = "AsyncFilter", urlPatterns = "/async", asyncSupported = true)
public class AsyncFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("AsyncFilter: 开始处理,线程ID = " + Thread.currentThread().getId());
// 开启异步模式
HttpServletRequest httpRequest = (HttpServletRequest) request;
AsyncContext asyncContext = httpRequest.startAsync();
// 添加异步监听器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("AsyncFilter: 异步处理完成,线程ID = " + Thread.currentThread().getId());
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("AsyncFilter: 异步处理超时,线程ID = " + Thread.currentThread().getId());
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("AsyncFilter: 异步处理错误,线程ID = " + Thread.currentThread().getId());
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("AsyncFilter: 开始异步处理,线程ID = " + Thread.currentThread().getId());
}
});
// 设置超时时间
asyncContext.setTimeout(10000);
// 继续处理请求
chain.doFilter(asyncContext.getRequest(), asyncContext.getResponse());
System.out.println("AsyncFilter: doFilter方法结束,线程ID = " + Thread.currentThread().getId());
}
// 其他方法略
}过滤器的注意事项
- 性能影响:过滤器会在每个匹配的请求上执行,过于复杂的过滤器可能会影响性能
- 异常处理:应该妥善处理过滤器中的异常,避免影响整个请求处理流程
- 资源释放:在destroy方法中释放占用的资源,如数据库连接、文件句柄等
- 过滤器顺序:多个过滤器的执行顺序很重要,特别是在有依赖关系的情况下
- 线程安全:过滤器实例是单例的,多个请求会共享同一个过滤器实例,需要注意线程安全问题
- 避免无限循环:在过滤器中转发或重定向时,要避免形成无限循环
过滤器与拦截器的区别
| 特性 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 技术实现 | Servlet规范 | 通常基于AOP实现(如Spring) |
| 适用范围 | 仅Web应用 | 可以在非Web环境中使用 |
| 生命周期 | 由Web容器管理 | 由Spring容器管理 |
| 拦截范围 | 请求和响应 | 方法调用和生命周期事件 |
| 访问权限 | 只能访问HttpServletRequest/Response | 可以访问方法参数、返回值等 |
| 配置方式 | XML或@WebFilter注解 | 通常使用Spring配置或注解 |
最佳实践
- 单一职责原则:每个过滤器只负责一种功能,避免创建过于复杂的过滤器
- 使用过滤器链:对于多个相关但不同的功能,使用多个过滤器组成过滤器链
- 适当的URL模式:为过滤器指定精确的URL模式,避免不必要的过滤
- 参数化配置:使用初始化参数使过滤器更加灵活可配置
- 资源管理:正确管理资源的获取和释放,使用try-with-resources等机制
- 异常记录:记录过滤器中的异常信息,便于调试和问题排查
- 性能监控:对关键过滤器进行性能监控,及时发现性能瓶颈
总结
Servlet过滤器是Java Web开发中的重要组件,它提供了一种声明式的机制来拦截和处理HTTP请求和响应。通过合理使用过滤器,可以实现权限控制、日志记录、数据转换等横切关注点的处理,使代码更加模块化和可维护。在实际开发中,应该根据具体需求设计和实现过滤器,遵循最佳实践,确保系统的性能和可靠性。