Skip to content

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. 请求包装器和响应包装器

当需要修改请求或响应的行为时,可以使用ServletRequestWrapperServletResponseWrapper

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());
    }
    
    // 其他方法略
}

过滤器的注意事项

  1. 性能影响:过滤器会在每个匹配的请求上执行,过于复杂的过滤器可能会影响性能
  2. 异常处理:应该妥善处理过滤器中的异常,避免影响整个请求处理流程
  3. 资源释放:在destroy方法中释放占用的资源,如数据库连接、文件句柄等
  4. 过滤器顺序:多个过滤器的执行顺序很重要,特别是在有依赖关系的情况下
  5. 线程安全:过滤器实例是单例的,多个请求会共享同一个过滤器实例,需要注意线程安全问题
  6. 避免无限循环:在过滤器中转发或重定向时,要避免形成无限循环

过滤器与拦截器的区别

特性过滤器(Filter)拦截器(Interceptor)
技术实现Servlet规范通常基于AOP实现(如Spring)
适用范围仅Web应用可以在非Web环境中使用
生命周期由Web容器管理由Spring容器管理
拦截范围请求和响应方法调用和生命周期事件
访问权限只能访问HttpServletRequest/Response可以访问方法参数、返回值等
配置方式XML或@WebFilter注解通常使用Spring配置或注解

最佳实践

  1. 单一职责原则:每个过滤器只负责一种功能,避免创建过于复杂的过滤器
  2. 使用过滤器链:对于多个相关但不同的功能,使用多个过滤器组成过滤器链
  3. 适当的URL模式:为过滤器指定精确的URL模式,避免不必要的过滤
  4. 参数化配置:使用初始化参数使过滤器更加灵活可配置
  5. 资源管理:正确管理资源的获取和释放,使用try-with-resources等机制
  6. 异常记录:记录过滤器中的异常信息,便于调试和问题排查
  7. 性能监控:对关键过滤器进行性能监控,及时发现性能瓶颈

总结

Servlet过滤器是Java Web开发中的重要组件,它提供了一种声明式的机制来拦截和处理HTTP请求和响应。通过合理使用过滤器,可以实现权限控制、日志记录、数据转换等横切关注点的处理,使代码更加模块化和可维护。在实际开发中,应该根据具体需求设计和实现过滤器,遵循最佳实践,确保系统的性能和可靠性。