记得首次用 Filter 解决 “全局登录校验” 问题 —— 那时才明白,Filter 就像商场的 “安保关卡”:所有进入商场的顾客(请求)和离开商场的商品(响应),都要经过安保检查(Filter 处理),既可以拦截异常人员,也能给商品贴标签,灵活管控整个流程。

在 Servlet 体系中,Filter 的核心作用是 “拦截并处理请求 / 响应”,弥补 Servlet 仅能处理请求的局限。它基于责任链模式工作,就像工厂的 “流水线工位”:多个 Filter 按顺序组成 Filter Chain,请求依次经过每个 Filter 处理,再传递给 Servlet;响应则反向经过 Filter 处理后,返回给客户端。

而请求 / 响应包装器(如 HttpServletRequestWrapper、HttpServletResponseWrapper),就像 “快递包装加工机”:能对原始请求 / 响应进行 “包装改造”—— 比如给请求加额外参数,给响应内容压缩,扩展原始对象的能力。早年做 OA 系统时,我曾用包装器实现 “响应内容加密”,无需修改原有 Servlet 代码,就像给快递包裹外层加了一层安全膜,灵活又高效。

一、从 Filter Chain 到包装器实战

1. Filter Chain 实现(电商登录 + 日志 + 权限校验)

三个 Filter 组成责任链,依次完成日志记录、登录校验、权限控制:

// 1. 日志Filter:记录请求基本信息(第一个执行)

public class LogFilter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

// 记录请求URL、方法、客户端IP

System.out.println("请求URL:" + request.getRequestURI() +

",方法:" + request.getMethod() +

",IP:" + request.getRemoteAddr());

long start = System.currentTimeMillis();

// 放行请求到下一个Filter

chain.doFilter(req, resp);

// 记录响应耗时

System.out.println("请求耗时:" + (System.currentTimeMillis() - start) + "ms");

}

}

// 2. 登录校验Filter:拦截未登录请求(第二个执行)

public class LoginFilter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

HttpServletResponse response = (HttpServletResponse) resp;

// 排除登录页、静态资源等不需要登录的路径

String url = request.getRequestURI();

if (url.contains("/login") || url.contains("/static/")) {

chain.doFilter(req, resp);

return;

}

// 检查Session中的登录状态

HttpSession session = request.getSession(false);

if (session == null || session.getAttribute("userId") == null) {

// 未登录:重定向到登录页

response.sendRedirect("/login");

return;

}

// 已登录:放行到下一个Filter

chain.doFilter(req, resp);

}

}

// 3. 权限Filter:校验用户操作权限(第三个执行)

public class PermissionFilter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

HttpServletResponse response = (HttpServletResponse) resp;

// 获取当前用户ID和请求路径

Long userId = (Long) request.getSession().getAttribute("userId");

String url = request.getRequestURI();

// 校验权限:如只有管理员能访问订单管理

if (url.contains("/admin/order") && !isAdmin(userId)) {

response.setStatus(403);

response.getWriter().write("无权限访问");

return;

}

// 有权限:放行到Servlet

chain.doFilter(req, resp);

}

// 模拟权限查询:判断用户是否为管理员

private boolean isAdmin(Long userId) {

return userId != null && userId == 1L; // 假设ID=1的是管理员

}

}
// 4. 配置Filter(web.xml方式)

<filter>

<filter-name>logFilter</filter-name>

<filter-class>com.shop.filter.LogFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>logFilter</filter-name>

<url-pattern>/*</url-pattern> <!-- 所有请求都拦截 -->

</filter-mapping>

<filter>

<filter-name>loginFilter</filter-name>

<filter-class>com.shop.filter.LoginFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>loginFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<filter>

<filter-name>permissionFilter</filter-name>

<filter-class>com.shop.filter.PermissionFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>permissionFilter</filter-name>

<url-pattern>/admin/*</url-pattern> <!-- 仅拦截管理员路径 -->

</filter-mapping>
2. 请求包装器

给原始请求 “包装” 一层,添加全局参数,无需修改 Servlet 代码:


// 自定义请求包装器:添加全局用户类型参数

public class CustomRequestWrapper extends HttpServletRequestWrapper {

private final String userType; // 额外参数:用户类型(普通用户/管理员)

public CustomRequestWrapper(HttpServletRequest request, String userType) {

super(request);

this.userType = userType;

}

// 重写getParameter:优先返回包装器中的参数

@Override

public String getParameter(String name) {

if ("userType".equals(name)) {

return userType;

}

return super.getParameter(name);

}

// 重写getParameterMap:合并原始参数和包装器参数

@Override

public Map<String, String[]> getParameterMap() {

Map<String, String[]> originalMap = super.getParameterMap();

Map<String, String[]> newMap = new HashMap<>(originalMap);

newMap.put("userType", new String[]{userType});

return newMap;

}

}

// 在Filter中使用包装器

public class RequestWrapperFilter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

// 从Session获取用户ID,判断用户类型

Long userId = (Long) request.getSession().getAttribute("userId");

String userType = userId == 1L ? "admin" : "user";

// 用包装器替换原始请求

CustomRequestWrapper wrapper = new CustomRequestWrapper(request, userType);

// 传递包装器到后续流程

chain.doFilter(wrapper, resp);

}

}

// Servlet中直接获取包装后的参数

public class OrderServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

// 直接获取包装器添加的userType参数,无需关心来源

String userType = req.getParameter("userType");

resp.getWriter().write("当前用户类型:" + userType);

}

}
3. 响应包装器

对响应内容进行 Gzip 压缩,减少传输体积:


// 自定义响应包装器:捕获响应内容用于压缩

public class GzipResponseWrapper extends HttpServletResponseWrapper {

private final ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();

private final PrintWriter printWriter = new PrintWriter(byteArrayOut);

public GzipResponseWrapper(HttpServletResponse response) {

super(response);

// 设置响应头:告知客户端内容已压缩

response.setHeader("Content-Encoding", "gzip");

}

// 重写getWriter:将响应内容写入字节流

@Override

public PrintWriter getWriter() throws IOException {

return printWriter;

}

// 重写getOutputStream:同理,支持字节流输出

@Override

public ServletOutputStream getOutputStream() throws IOException {

return new ServletOutputStream() {

@Override

public void write(int b) throws IOException {

byteArrayOut.write(b);

}

@Override

public boolean isReady() {

return true;

}

@Override

public void setWriteListener(WriteListener listener) {}

};

}

// 压缩响应内容并写入原始响应

public void flush() throws IOException {

printWriter.flush();

// 用Gzip压缩字节流

try (GZIPOutputStream gzipOut = new GZIPOutputStream(getResponse().getOutputStream())) {

byteArrayOut.writeTo(gzipOut);

}

}

}

// 压缩Filter

public class GzipFilter implements Filter {

@Override

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

throws IOException, ServletException {

HttpServletResponse response = (HttpServletResponse) resp;

// 排除图片、JS等已压缩的资源

String contentType = response.getContentType();

if (contentType != null && (contentType.contains("image/") || contentType.contains("application/javascript"))) {

chain.doFilter(req, resp);

return;

}

// 用压缩包装器替换原始响应

GzipResponseWrapper wrapper = new GzipResponseWrapper(response);

chain.doFilter(req, wrapper);

// 压缩并输出响应内容

wrapper.flush();

}

}

二、实战场景系统的 Filter 与包装器应用

当年给某国企做企业内网系统系统时,曾用 Filter 和包装器解决三个核心问题:

全局日志审计

需求:记录所有用户操作(如 “张三删除了公文 XXX”)。我们开发了AuditLogFilter,在 Filter 中获取用户信息和请求参数,调用日志服务保存到数据库。无需在每个 Servlet 中写日志代码,就像 “给所有工位装了监控”,自动记录操作。

敏感数据脱敏

需求:响应中的手机号、身份证号需脱敏(如 138****5678)。我们用SensitiveDataWrapper包装响应,在getWriter()方法中拦截输出内容,通过正则替换敏感信息。比如公文详情 Servlet 返回的 JSON 中,手机号自动被脱敏,无需修改 Servlet 逻辑。

跨域请求处理

需求:OA 系统需支持前端页面(部署在fe.oa.com)跨域请求。我们开发了CorsFilter,在 Filter 中设置响应头:

response.setHeader("Access-Control-Allow-Origin", "http://fe.oa.com");

response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");

response.setHeader("Access-Control-Allow-Credentials", "true");

解决了跨域请求被浏览器拦截的问题,就像 “给不同园区的大门开了通行许可”。

三、对比分析

组件

核心作用

像什么场景

电商 / OA 适配性

痛点

Filter

拦截请求 / 响应,全局处理

商场安保关卡,统一检查

登录校验、日志、跨域、压缩

执行顺序配置复杂,调试难度高

Servlet

处理具体业务逻辑

商场店铺,提供特定服务

订单处理、公文管理等业务

无法全局拦截,重复代码多

Listener

监听容器 / 对象状态变化

商场警报器,触发特定动作

Session 创建销毁、上下文初始化

仅能监听状态,无法拦截请求

包装器(Wrapper)

扩展请求 / 响应能力

快递包装加工机,改造内容

敏感数据脱敏、参数添加、压缩

需重写多个方法,易遗漏

2020 年我们做过一次性能测试:用 Filter 处理 10 万次请求,日志 Filter 耗时约 2ms / 次,压缩 Filter 耗时约 5ms / 次,对整体响应影响极小。这也说明:Filter 在全局处理场景下,性能损耗可忽略,且能大幅减少重复代码 —— 比如登录校验用 Filter,比在每个 Servlet 中写校验逻辑,代码量减少 80%。

四、常见陷阱

执行顺序配置错误

有个同事在 web.xml 中先配置permissionFilter,再配置loginFilter,导致未登录用户先被校验权限,出现 “空指针异常”。这就像 “先检查用户是否有会员卡,再确认用户是否已进门”,逻辑顺序完全颠倒。

原理:web.xml 中 Filter 的执行顺序由filter-mapping的配置顺序决定,先配置的先执行;注解方式(@WebFilter)则按类名 ASCII 排序(如 LogFilter 先于 LoginFilter)。

解法:按 “日志→登录→权限” 的逻辑顺序配置filter-mapping,或用 Spring Boot 的@Order注解指定顺序(数值越小越先执行)。

包装器未传递完整

某同事在 Filter 中用CustomRequestWrapper包装请求后,后续 Filter 或 Servlet 仍用(HttpServletRequest) req强转,导致获取不到包装后的参数。这就像 “给快递换了包装,但后续环节仍按原包装处理”,改造效果失效。

解法:Filter 链中必须传递包装后的对象,后续所有环节需通过ServletRequest接收,而非强转原始类型(或强转包装器类型)。

响应流重复关闭

有个压缩 Filter 在doFilter中手动关闭了response.getOutputStream(),导致后续 Servlet 无法写入响应,报 “流已关闭” 错误。这就像 “快递包装机把包装封死了,后续无法再装东西”。

解法:包装器的流由容器统一管理,Filter 中无需手动关闭;若需处理流(如压缩),在flush()等方法中操作,避免提前关闭。

URL 匹配陷阱

某同事配置 Filter 的url-pattern为/admin,想拦截/admin/order等路径,结果发现无法拦截。这就像 “给‘admin’门牌的房间装了锁,却管不了‘admin/order’门牌的房间”。

原理:/admin仅匹配精确路径/admin,/admin/*才匹配/admin下的所有子路径。

解法:根据需求选择匹配模式:/*(所有路径)、/admin/*(子路径)、*.do(后缀匹配)。

五、Filter 与包装器的 “避坑指南”

Filter 设计原则

  • 单一职责:一个 Filter 只做一件事(如日志 Filter 只记录日志,登录 Filter 只做登录校验),避免 “大而全” 导致维护困难;
  • 路径精准匹配:避免用/*拦截所有请求,按业务场景缩小范围(如权限 Filter 仅拦截/admin/*),减少性能损耗;
  • 排除无需拦截的路径:在 Filter 中排除登录页、静态资源、接口文档等,避免不必要的处理(如if (url.contains("/login")) { chain.doFilter(); return; })。

执行顺序控制策略

  • web.xml 配置:按 “全局处理→安全校验→业务过滤” 的顺序配置filter-mapping(如日志→跨域→登录→权限);
  • Spring Boot 注解:用@Order指定顺序,建议定义常量(如@Order(FilterOrder.LOG)),避免硬编码数值;
  • 优先级划分:全局基础功能(日志、跨域)优先级最高(Order=10),安全校验(登录、权限)次之(Order=20),业务过滤(压缩、脱敏)最低(Order=30)。

包装器使用技巧

  • 按需重写方法:仅重写需要扩展的方法(如添加参数只需重写getParameter()),避免重写所有方法导致冗余;
  • 资源释放:在包装器中使用try-with-resources管理流资源(如GZIPOutputStream),避免内存泄漏;
  • 调试便捷:在包装器中添加日志,记录扩展内容(如脱敏前后的对比),方便排查问题。

性能优化

  • 避免重复处理:如压缩 Filter 仅处理文本类型(HTML、JSON、XML),通过response.getContentType()判断,跳过图片(image/jpeg)、JS(application/javascript)、CSS(text/css)等已压缩或无需压缩的资源,减少无效计算;
  • 缓存常用数据:如权限 Filter 在init()方法中加载全局权限配置(如 “哪些路径需要管理员权限”),或用本地缓存(如 Caffeine)存储高频访问的用户权限列表,避免每次请求都查询数据库,将权限校验耗时从 20ms 降至 2ms;
  • 异步处理非关键逻辑:如日志 Filter 中,日志记录属于非关键操作(不影响请求响应),可改用异步线程池处理(如Executors.newFixedThreadPool(5)),避免日志写入数据库阻塞主线程,提升请求吞吐量;
  • 减少包装器嵌套:包装器每次嵌套都会增加方法调用层级,若需同时扩展请求和响应能力,尽量合并为一个包装器(如同时实现参数添加与敏感数据脱敏),而非多层嵌套,避免性能损耗。

六、云原生与微服务时代的 Filter 演进

随着云原生与微服务架构的普及,Filter 与包装模式并未被淘汰,反而通过 “适配新场景” 焕发新价值:

微服务网关中的 Filter 延伸

现在的 API 网关(如 Spring Cloud Gateway、Zuul),其核心 “过滤器” 设计完全继承了 Servlet Filter 的责任链思想 —— 只是将拦截范围从 “单个应用” 扩展到 “整个微服务集群”。例如:

  • 网关的 “认证 Filter” 统一校验所有微服务的请求令牌(如 JWT),替代每个服务单独开发登录校验;
  • “限流 Filter” 在网关层对请求进行流量控制(如每秒 1000 次),避免下游微服务被压垮。

这就像 “从商场单个入口的安保,升级为整个商业园区的总门禁”,管控范围更大,复用性更强。

容器化环境下的 Filter 适配

在 Docker 容器部署场景中,Filter 的 “无侵入性” 优势更明显:若需新增 “跨域处理” 或 “日志审计” 功能,只需在容器启动时挂载 Filter 配置(如 Spring Boot 的@Component注解自动扫描),无需修改业务代码或重启整个集群。例如:

  • 给所有电商微服务统一添加 “链路追踪 Filter”,在请求头中注入 Trace ID,无需每个服务单独配置,实现全链路日志串联;
  • 容器化部署时,通过环境变量(如LOG_FILTER_ENABLE=true)动态开关 Filter,灵活适配开发、测试、生产环境。

Serverless 场景中的轻量化应用

在 Serverless 函数(如 AWS Lambda、阿里云 FC)中,虽无传统 Servlet 容器,但 Filter 的 “拦截 - 处理” 逻辑仍适用:

  • 将 Filter 逻辑封装为 “函数装饰器”,如在订单支付函数前添加 “参数校验装饰器”,拦截非法请求(如支付金额为负);
  • 包装器思想用于函数输入输出改造,如将前端传递的 JSON 参数包装为 Java 对象,或将函数返回的结果包装为统一格式(如{code:200, data:{}, msg:"success"}),减少重复代码。

最后小结

回顾 Filter 与包装模式的应用历程,从早期 Servlet 的 “单应用全局拦截”,到现在微服务网关的 “集群级管控”,其核心设计哲学从未改变 ——“开闭原则” 与 “单一职责” 的落地实践

  • 对 “扩展” 开放:新增功能(如日志、压缩、权限)时,无需修改原有 Servlet 或微服务代码,只需新增 Filter 或包装器,符合 “开闭原则”;
  • 对 “修改” 关闭:原有业务逻辑(如订单处理、公文管理)无需因全局功能变化而调整,避免引入新 bug;
  • 单一职责落地:每个 Filter 只聚焦一件事,每个包装器只扩展一种能力,让代码可维护、可复用 —— 就像工厂的流水线,每个工位只负责一道工序,最终协同完成复杂产品。

现在虽有 Spring Interceptor、微服务网关过滤器等 “新工具”,但它们的底层逻辑都源自 Servlet Filter 与包装模式。理解这两种技术,不仅能解决当前开发中的问题,更能掌握 “如何设计灵活、可扩展的系统” 的核心思路 —— 毕竟,好的技术设计,永远经得起时间与场景的考验。未完待续..........

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐