如果你一路跟着 JavaWeb 学到 Day13,大概率已经写了不少 Servlet、天天和 Request、Response 打交道,也可能在 JSP 里被各种数据展示折磨过。这个阶段你一定会冒出一种感觉:太多重复代码了。每个 Servlet 都要手动设置编码,每个需要登录的页面都得复制粘贴一段判断 Session 的代码,想给系统加个访问日志却发现不知道该从哪个入口统一塞进去。Filter 过滤器就是来解决这类“横切关注点”问题的标准方案。它可以把编码处理、登录校验、日志记录这些和具体业务无关、但又躲不开的逻辑,从业务代码里彻底抽出来,做成统一拦截、统一处理的组件。这篇 Day13 学习笔记,我按照“原理拆解 + 项目实战”的方式,把 Filter 从入门到落地完整梳理了一遍,适合已经掌握 Servlet 基础、正处于 JavaWeb 进阶阶段的同学直接抄作业。
在本篇笔记里,我不会只贴一堆配置代码,而是把过滤器为什么这么设计、请求经过过滤器时底层到底发生了什么、多个过滤器以什么顺序执行这些关键问题,全部用大白话讲清楚。最后还会带着你做一个贴近真实项目的综合案例:统一编码处理、登录状态校验、访问日志记录三个过滤器一次到位。看完之后,你不仅能应付作业和面试,更重要的是能把“抽公共逻辑”这套思路内化成自己的设计习惯。
1. 内容整体设计与思路拆解
1.1 Day13 这个节点,为什么先学过滤器
学习 JavaWeb 有个很典型的“顿悟时刻”:第一次发现所有 Servlet 里都在重复做同一件事。比如每个 doGet、doPost 方法第一行都是request.setCharacterEncoding("UTF-8");每个需要登录的页面都要先从 Session 里取用户对象,取不到就response.sendRedirect("login.jsp")。一开始你还能忍受,写着写着就烦了,因为每新增一个页面,这些代码就要再复制一遍,改一个参数还要全局搜索替换。
过滤器就是在这个痛点背景下出现的。它的设计初衷很简单:在请求到达 Servlet 之前,先把所有请求统一拦一道;在 Servlet 返回响应之后,再统一拦一道。你可以把过滤器理解成小区门口的保安亭,所有进出的车都要在这里过一下,保安可以做登记、查证件、查后备箱,做完该做的事之后才放行。对应到代码里,编码过滤器就是所有请求先过一遍“UTF-8 通道”,登录过滤器就是“查证件”,日志过滤器就是“登记进出记录”。
这个阶段学 Filter 还有一个隐藏价值:它是后面学习 SpringMVC 拦截器、AOP 思想的认知铺垫。JavaWeb 里的 Filter 把你带进“横切逻辑”这个思维方式,SpringMVC 的 Interceptor 就是把同一套思路换了个更精细的包装。所以在 Day13 这个节点认真搞懂 Filter,收益是全链路式的,不只是单纯多学一个 API。
1.2 过滤器核心原理:一次请求的“蹲守点”
从底层看,过滤器要解决的问题是:在不修改 Servlet 源码的前提下,给一组 Servlet 增加通用能力。Servlet 规范里定义了javax.servlet.Filter接口,Web 容器(比如 Tomcat)在启动时会创建 Filter 实例,并且维护一张“过滤器链”的表。当请求进来时,Tomcat 会按照顺序把请求依次交给每一个过滤器,最后一个过滤器处理完,才会调用真正的 Servlet。
我把这个流程拆成几个关键节点,帮助你记忆:
- 客户端发起请求,Tomcat 接收到后,根据请求 URL 匹配过滤器映射。
- 命中第一个过滤器,执行
doFilter中chain.doFilter()之前的代码。 chain.doFilter()的作用是把请求放行给下一个过滤器,如果这是最后一个过滤器,就放行给 Servlet。- Servlet 处理完成后,响应会沿着原来的链路“逆着”返回,每个过滤器中
chain.doFilter()之后的代码在此阶段执行。 - 响应最终回到客户端。
这里最容易被忽视的知识点是:chain.doFilter()前后的代码并不是同时执行的,而是“前半段在请求阶段执行,后半段在响应阶段执行”。所以如果你想统计一次请求的总耗时,就在 doFilter 调用前记一个开始时间,调用之后再用当前时间减去开始时间。这个特性在后面实战的日志过滤器里会直接用到。
1.3 过滤器的典型应用场景
过滤器到底能干什么?我在整理这张场景表时,是按“请求前拦截、请求后处理、请求前后都处理”三类来归类的,理解了分类,面试时被问到就能马上举出例子。
| 分类 | 场景 | 核心逻辑 |
|---|---|---|
| 请求前拦截 | 登录状态校验 | 检查 Session 或 Token,未登录直接跳转登录页 |
| 请求前拦截 | 权限控制 | 判断当前用户角色是否能访问某 URL |
| 请求前拦截 | 敏感词过滤 | 在请求参数进入业务层之前做内容替换 |
| 请求前拦截 | 统一编码设置 | 设置请求和响应的字符集,避免中文乱码 |
| 请求后处理 | 响应压缩 | 对响应内容做 Gzip 压缩再返回客户端 |
| 请求前后都处理 | 访问日志与耗时统计 | 记录请求 URL、IP、处理时间 |
| 请求前后都处理 | 跨域处理 | 在响应头写入 CORS 信息,解决前后端分离跨域 |
很多初学者一听到过滤器就只想到登录校验,实际上它是 JavaWeb 里做“统一治理”的最好入口。开发里常见的 XSS 防护、SQL 注入拦截、接口幂等校验,也都可以用过滤器做基础版本。学 Filter 时如果只看 API 不看场景,很容易变成“背代码”,所以我更建议你把这张场景表保存在笔记里,遇到实际需求时一个一个对照。
2. 核心细节解析与实操要点
2.1 Filter 接口三大方法与生命周期
过滤器接口的方法不多,就三个,但每个方法背后的生命周期问题都值得掰开讲。
public interface Filter { default void init(FilterConfig filterConfig) throws ServletException {} void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; default void destroy() {} }init方法在 Web 应用启动时执行一次,并且只执行一次。它拿到的是FilterConfig对象,里面可以读取 web.xml 中配置的初始化参数。比如你可以在配置里指定“哪些路径不拦截”,然后在这里读取成集合,后续做白名单判断。我在实际项目里经常这么干,灵活度明显比写死在代码里高。
doFilter是每次请求都会执行的,也是过滤器真正干活的入口。需要注意ServletRequest和ServletResponse是父接口,如果要操作 Http 相关的 API,比如获取 Session、设置状态码,必须强转为HttpServletRequest和HttpServletResponse。
destroy同样是应用卸载时执行一次,做资源清理。多数项目里这个方法都是空的,但你如果真的在init里开启了数据库连接池或者其他重量级资源,记得在这里关闭。
三个方法的执行时机用一句话记忆就是:启动时 init 一次,请求时 doFilter 无数次,停止时 destroy 一次。这个生命周期和 Servlet 的 init/service/destroy 高度相似,因为它们都是被 Web 容器管理的对象,创建和销毁的主动权都在容器手里。
2.2 配置方式与 URL 匹配规则
过滤器有两种注册方式:第一种是传统 web.xml 配置,第二种是 Servlet 3.0 引入的@WebFilter注解。我接触过不少新手在这两种方式之间“精神分裂”,一会儿用注解一会儿用 XML,出了问题不知道去哪里找。这里给一个明确建议:新项目用注解,老项目维护用 XML,两者不建议混用。
web.xml 里的配置长这样:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>com.example.filter.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>注解方式就简洁很多:
@WebFilter(urlPatterns = "/*", initParams = @WebInitParam(name = "encoding", value = "UTF-8")) public class EncodingFilter implements Filter { // ... }关于 URL 匹配规则,这个必须记牢,因为很多过滤器“没生效”或“拦多了”都是在这里出问题的。规则一共有四种:
- 精确匹配:
/login,只匹配这一个路径。 - 路径匹配:
/admin/*,匹配 /admin 开头的所有路径,包括 /admin、/admin/user、/admin/user/list。 - 扩展名匹配:
*.do,匹配所有以 .do 结尾的请求。 - 通配符匹配:
/*,匹配所有请求,最常用,也最容易被误用。
很多新手会把/和/*搞混。/在 Servlet 规范里是默认 Servlet 的映射,永远不会被过滤器匹配;/*是真正匹配所有路径的通配符。写过滤器时,绝大多数统一处理场景(编码、日志)都应该用/*。
2.3 FilterChain 执行顺序:链式调用的秘密
多个过滤器同时存在时,执行顺序是最容易踩坑的地方。底层实现可以理解为数据结构里的责任链模式:每个过滤器持有对下一个过滤器(或 Servlet)的引用,调用chain.doFilter()就是沿着链条往下传。
用两个过滤器举例,标注为 FilterA 和 FilterB,实际执行顺序是:
FilterA 前段代码 FilterB 前段代码 Servlet 代码 FilterB 后段代码 FilterA 后段代码这个结果看起来像“栈”,先进去的后出来。所以在写多个过滤器时,你要想清楚每个过滤器放在哪个位置:需要最先执行、最后返回时最后处理逻辑的过滤器,应该排在链条最前面;需要最靠近 Servlet 的过滤器,排在最后面。
顺序怎么控制?web.xml 方式看<filter-mapping>的书写顺序,从上到下依次执行;注解方式比较坑,不是按代码书写顺序,而是按过滤器类的名字进行字典序排序。比如AuthFilter会排在EncodingFilter前面,因为 A 在 E 前面。项目里如果你依赖注解排序,建议给类名加上明确前缀,比如Filter01Auth、Filter02Encoding,这样逻辑一眼就能看出来。
我把这块知识总结成一句话:过滤器链是“请求自顶向下,响应自底向上”,配置顺序直接决定执行顺序。
3. 实操过程与核心环节实现:登录校验 + 编码处理 + 日志记录综合案例
3.1 案例功能规划与项目结构
这一节我带你做一个贴近真实开发场景的小案例,场景设定为一个简化版的旅游后台管理系统,这也是很多 JavaWeb 课程和毕设里非常常见的业务方向。系统里有用户登录、线路管理、订单管理模块,我们只保留最基本的功能,重点是把三个过滤器串起来。
项目最终效果是这样的:
- 所有请求统一 UTF-8 编码,不再需要在每个 Servlet 里手动设置。
- 除了登录接口和静态资源,其他所有页面都要求登录后才能访问。
- 每次请求都会记录访问日志,包括访问时间、请求路径、客户端 IP 和处理耗时。
项目结构我建议这样安排:
src/main/java/com/example/filter/ EncodingFilter.java AuthFilter.java LogFilter.java src/main/java/com/example/servlet/ LoginServlet.java LogoutServlet.java TravelServlet.java src/main/webapp/ login.jsp index.jsp travel/list.jsp用 IDEA 2023 创建项目时,选择 Jakarta EE 模板里的 Web Application,记得勾选 Servlet 依赖,开发时配置一个本地的 Tomcat 9 或 10。这一步如果你还不熟,可以选File → New → Project,左侧选 Jakarta EE,右侧选 Web,然后Application Server下拉选择之前配置好的 Tomcat。项目创建完成后,在pom.xml或者 Web 结构里确保有 servlet-api 的依赖(IDEA 模板通常会自动添加)。
3.2 编码过滤器:最简单也最容易漏的过滤器
先写最简单的编码过滤器。这一步其实在 Servlet 的 API 里已经有现成的实现类CharacterEncodingFilter(在 Web 框架里可以直接使用),但为了搞清楚原理,我建议你自己手写一遍。
package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class EncodingFilter implements Filter { private String encoding = "UTF-8"; @Override public void init(FilterConfig filterConfig) throws ServletException { String param = filterConfig.getInitParameter("encoding"); if (param != null && !param.trim().isEmpty()) { encoding = param; } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; httpRequest.setCharacterEncoding(encoding); httpResponse.setCharacterEncoding(encoding); httpResponse.setContentType("text/html;charset=" + encoding); chain.doFilter(request, response); } @Override public void destroy() { // 无需清理资源 } }这里有个细节必须说清楚:setCharacterEncoding必须放在chain.doFilter()之前。因为这个方法的作用是告诉容器“请求体使用什么字符集来解码”,如果 Servlet 已经读取了参数,再设置就晚了,参数已经按照默认编码(通常是 ISO-8859-1)解析过了,中文就会变成乱码。
还有一个容易被低估的点:设置编码不只是request的事,response也必须设置。很多项目乱码查了半天,最后发现是响应输出给浏览器的编码没有指定,导致浏览器用错误的方式解码。上面代码里我设置了setContentType,这一步同时指定了页面返回的文档类型是 HTML 以及字符集,在实际页面开发里很关键。
3.3 登录校验过滤器:带白名单的权限控制
登录过滤器是整个案例里业务价值最高的一个。它的核心逻辑有三步:第一步判断请求路径是否属于白名单,属于就直接放行;第二步尝试从 Session 获取当前登录用户,存在就放行;第三步未登录用户统一重定向到登录页。
package com.example.filter; 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; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @WebFilter(urlPatterns = "/*") public class AuthFilter implements Filter { // 白名单:登录页、登录接口、注册接口以及静态资源 private static final Set<String> WHITE_LIST = new HashSet<>(Arrays.asList( "/login.jsp", "/login", "/register.jsp", "/register", "/css", "/js", "/images" )); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String uri = httpRequest.getRequestURI(); String contextPath = httpRequest.getContextPath(); // 去掉项目上下文路径,只保留应用内路径 String path = uri.substring(contextPath.length()); if (isWhite(path)) { chain.doFilter(request, response); return; } HttpSession session = httpRequest.getSession(false); Object user = session == null ? null : session.getAttribute("username"); if (user != null) { chain.doFilter(request, response); return; } httpResponse.sendRedirect(contextPath + "/login.jsp"); } private boolean isWhite(String path) { for (String prefix : WHITE_LIST) { if (path.equals(prefix) || path.startsWith(prefix + "/")) { return true; } } return false; } }写完这段代码,我每次带新手都会专门强调两个细节:
第一,getSession(false)和getSession()的区别。默认的getSession()如果发现没有 Session,会自动创建一个新的,这样你就永远不可能拦截到“未登录”状态了,因为它总能给你一个空 Session。getSession(false)表示没有就返回 null,这样才能正确判断未登录。
第二,白名单匹配不要用字符串的equals简单判断。/login能匹配上,但/login/或者页面里引用的/css/style.css就匹配不上。所以我的判断逻辑是:要么路径完全相等,要么以“白名单前缀 + /”开头,比如path.startsWith("/css" + "/")。这样/css/style.css、/js/app.js都能被正确放行。
3.4 日志过滤器:利用请求前后段时间差
日志过滤器能同时展示chain.doFilter()前后代码的执行时机,我每次讲这块都会让学员打印一下时间,亲眼看一看“前段先执行、后段后执行”的效果。
package com.example.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @WebFilter(urlPatterns = "/*") public class LogFilter implements Filter { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; long start = System.currentTimeMillis(); String uri = httpRequest.getRequestURI(); String ip = httpRequest.getRemoteAddr(); chain.doFilter(request, response); long cost = System.currentTimeMillis() - start; System.out.println("[" + LocalDateTime.now().format(FORMATTER) + "] 请求路径: " + uri + " | 客户端IP: " + ip + " | 耗时: " + cost + "ms"); } }这段代码展示了过滤器的一个核心能力:因为chain.doFilter()会把请求交给下一个组件,而这个调用是同步的,所以调用结束返回时,说明整个 Servlet 处理已经完成。因此chain.doFilter()之前取开始时间、之后取结束时间,计算出来的差值就是请求的总处理耗时。
应用启动后,你在浏览器里访问任意页面,IDEA 控制台就会输出一行日志。这种日志在生产环境里一般是不打印到控制台的,而是接入日志框架写文件或者日志系统,但思路完全一样:过滤器就是最佳的日志埋点位置。它能保证所有请求都经过,Web 容器里所有访问记录都会在这里留下痕迹。
3.5 三个过滤器的组合顺序与效果验证
三个过滤器都写完后,组合顺序就变得至关重要。如果我用的是注解方式,执行顺序按照类名字典序排序,因为AuthFilter、EncodingFilter、LogFilter三个类名按字母排序是 A、E、L,所以实际执行顺序是 Auth → Encoding → Log。
这个顺序在业务上合理吗?我的分析是这样的:编码过滤器实际上应该最靠前执行,因为后续所有组件读取参数都必须基于正确的编码;日志过滤器可以放在最外层记录时间,也可以放在中间;登录过滤器应该优先于具体业务,但不能拦截静态资源。所以理想顺序是 Log → Encoding → Auth,Log 最外层统计全部耗时,然后设置编码,最后做登录校验,都通过才交给 Servlet。
如果想要这个顺序,最简单的做法是在 web.xml 里显式配置过滤器映射顺序,而不用注解。配置方式如下:
<filter> <filter-name>logFilter</filter-name> <filter-class>com.example.filter.LogFilter</filter-class> </filter> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.example.filter.EncodingFilter</filter-class> </filter> <filter> <filter-name>authFilter</filter-name> <filter-class>com.example.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>logFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>authFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>使用 web.xml 配置时,如果类上已经加了@WebFilter注解,一定要把注解去掉,否则会重复注册,过滤器的doFilter会被执行两次,这是非常隐蔽的 bug。我在实际项目中踩过这个坑,查了半天才发现是同一套逻辑同时被注解和 XML 配置了。
验证效果时,我一般按三个场景测:
- 未登录直接访问
http://localhost:8080/travel/list.jsp,会被重定向到 login.jsp。 - 登录成功后访问同样的地址,能正常看到列表页,控制台输出两条日志,因为登录请求和列表请求都经过了日志过滤器。
- 在表单里输入中文并提交,后台 Servlet 获取到的参数不再是乱码。
4. 常见问题与排查技巧实录
4.1 过滤器不生效,先从三个方向查
过滤器写了但在页面上完全没效果,这是新手问得最多的问题。我一般按三个方向排查:第一,项目是不是没有重新发布,改完过滤器代码后 Tomcat 没有自动热部署,需要重启或者重新部署;第二,urlPatterns是不是写错了,比如把/*写成了/,导致只匹配默认 Servlet 而不匹配业务请求;第三,应用上下文路径有没有被忽略,很多人在路径匹配时拿全路径去比较,忽略了前缀的 contextPath,结果永远匹配不上。
我自己在排查时有个习惯:在doFilter的第一行加上System.out.println("进入了xxx过滤器"),然后重启项目随便访问一个页面,看控制台有没有输出。如果没输出,说明过滤器根本没被加载或者没被匹配到;如果输出了,再逐步缩小范围。这个土办法比看一堆配置要快得多。
4.2 登录过滤器导致死循环
一个非常经典的 bug 是这样的:用户未登录访问任意页面,被过滤器重定向到 login.jsp,结果 login.jsp 这个请求又被过滤器拦截,又重定向到 login.jsp,形成一个无限循环,浏览器疯狂刷新,最终报错。
我见过很多新手在这个问题上卡很久,原因就是白名单没有把登录页和登录接口放进去。所以写登录过滤器时,第一件事就是先把登录页、登录处理接口、注册页、注册接口、静态资源全部列入白名单。在刚才的实战代码里,我用了WHITE_LIST集合来管理这些路径,这个方法建议直接抄走。
另外一个容易出问题的小细节是重定向路径。sendRedirect里面需要写完整路径,包括上下文路径,也就是我在代码里写的contextPath + "/login.jsp"。如果不加 contextPath,部署到根路径时没问题,一旦加了项目名(比如部署成/travel),重定向就会 404。
4.3 过滤器顺序不对,注解与 XML 的排序规则差异
多个过滤器顺序不对,也是高频问题。我之前讲过注解方式按类名排序,这里补充一个实际使用建议:在项目里如果需要严格的控制顺序,建议统一使用 web.xml 方式配置过滤器,因为顺序写在文件里,谁在上面谁在下面,一眼就能看明白。如果用注解,类名排序太隐晦,很容易在后面加过滤器的时候无意中改变排序结果。
还有一个衍生问题:如果你看到请求被同一个过滤器处理了两次,多半是过滤器被重复注册了。检查一下是否在类上加了@WebFilter的同时,又在 web.xml 里配了一份过滤器的映射。这种低级错误非常隐蔽,因为代码本身没错,是重复注册,所以执行了两次,可能导致编码过滤器设置的字符集被后面的请求读取逻辑覆盖,登录过滤器也可能因为重复放行而出现逻辑错乱。
4.4 静态资源被拦截,页面样式全丢了
页面能打开,但 CSS 和 JS 文件全部 404,这也是登录过滤器的“副作用”。原因很简单:你的登录过滤器把/css/style.css、/js/app.js这些请求也拦截了,判定未登录后重定向到了 login.jsp。
解决方案就是在白名单里加上静态资源的前缀。但这里有个细节要注意,如果项目里静态资源路径是/static/css/style.css这种带/static前缀的,白名单就写/static;如果直接放在 webapp 根目录下,比如/css/style.css,白名单就写/css。判断逻辑要能匹配资源文件本身的路径,不是匹配前缀就万事大吉了,建议用我上面写的path.startsWith(prefix + "/")这种写法,既能匹配/css也能匹配/css/style.css,还能避免把/css-other这种路径误放。
4.5 编码过滤器没解决乱码,可能是响应类型没设置
如果你的过滤器已经写好了,POST 请求参数的中文也没问题,但页面显示还是乱码,问题往往出在response上。我之前说过,光设置setCharacterEncoding还不够,还要设置setContentType,明确告诉浏览器返回内容的文档类型和字符集。
这个乱码的排查路径我再说细一点:浏览器收到的响应如果没有charset=utf-8,它默认会用系统自带的编码去猜,中文 Windows 下经常用 GBK 解析,然后页面就出现“锟斤拷”这种经典乱码。所以过滤器里httpResponse.setContentType("text/html;charset=" + encoding);这行千万别省。如果项目里返回的是 JSON 数据,就把 content type 换成application/json;charset=utf-8。
4.6 问题排查速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 过滤器完全没执行 | 项目未热部署、url-pattern 写错、类上没有注解 | 打印日志确认,检查注解和映射 |
| 登录过滤出现死循环 | 登录页/登录接口未加入白名单 | 白名单加入 login.jsp 和登录接口 |
| 静态资源 404 | 过滤器拦截了 css/js 请求 | 白名单加入静态资源前缀 |
| 过滤器执行了两次 | 注解和 web.xml 同时注册 | 去掉一种注册方式 |
| 中文参数乱码 | 编码设置放在 doFilter 之后 | 设置编码必须在 chain.doFilter 之前 |
| 页面显示乱码 | response 内容类型未设置字符集 | 设置 setContentType 的 charset |
| 未登录也能访问页面 | getSession() 自动创建了空 Session | 改用 getSession(false) |
| 过滤器顺序不对 | 注解按类名排序不符合预期 | 改用 web.xml 显式配置顺序 |
这个速查表基本覆盖了我带项目时见过的高频问题,建议保存在你的学习笔记里。排查顺序如果不知道怎么定,就按照“先确认过滤器有没有执行 → 再看路径匹配是否正确 → 再查链顺序和重复注册 → 最后查编码和响应设置”这个路径走,90% 的问题都能在几分钟内定位到。
最后再分享一点个人体会。过滤器这套机制,刚开始学觉得就是几个接口方法,但真正用多了会发现它教会你的是一种“横切思维”:把不同业务里共通的逻辑抽出来,找一个统一的位置处理。这种思维在后面的 SpringMVC 拦截器、Spring AOP 里会反复出现,底子打好了,后面学框架会轻松很多。我在 Day13 学完 Filter 之后,回头重构了之前写的几个 Servlet,把编码、登录、日志全部拆出来,代码瞬间清爽了不少,那种“原来代码还能这么写”的感觉,可能就是 JavaWeb 学习里最值得记住的瞬间之一。