news 2026/9/10 8:39:38

JavaWeb过滤器Filter实战:统一处理编码、登录校验与日志记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb过滤器Filter实战:统一处理编码、登录校验与日志记录

如果你一路跟着 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。

我把这个流程拆成几个关键节点,帮助你记忆:

  1. 客户端发起请求,Tomcat 接收到后,根据请求 URL 匹配过滤器映射。
  2. 命中第一个过滤器,执行doFilterchain.doFilter()之前的代码。
  3. chain.doFilter()的作用是把请求放行给下一个过滤器,如果这是最后一个过滤器,就放行给 Servlet。
  4. Servlet 处理完成后,响应会沿着原来的链路“逆着”返回,每个过滤器中chain.doFilter()之后的代码在此阶段执行。
  5. 响应最终回到客户端。

这里最容易被忽视的知识点是: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是每次请求都会执行的,也是过滤器真正干活的入口。需要注意ServletRequestServletResponse是父接口,如果要操作 Http 相关的 API,比如获取 Session、设置状态码,必须强转为HttpServletRequestHttpServletResponse

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 前面。项目里如果你依赖注解排序,建议给类名加上明确前缀,比如Filter01AuthFilter02Encoding,这样逻辑一眼就能看出来。

我把这块知识总结成一句话:过滤器链是“请求自顶向下,响应自底向上”,配置顺序直接决定执行顺序。

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 三个过滤器的组合顺序与效果验证

三个过滤器都写完后,组合顺序就变得至关重要。如果我用的是注解方式,执行顺序按照类名字典序排序,因为AuthFilterEncodingFilterLogFilter三个类名按字母排序是 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 配置了。

验证效果时,我一般按三个场景测:

  1. 未登录直接访问http://localhost:8080/travel/list.jsp,会被重定向到 login.jsp。
  2. 登录成功后访问同样的地址,能正常看到列表页,控制台输出两条日志,因为登录请求和列表请求都经过了日志过滤器。
  3. 在表单里输入中文并提交,后台 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 学习里最值得记住的瞬间之一。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:37:22

context-mode:智能体上下文协商协议与SQLite BM25落地实践

1. “context-mode”不是功能开关&#xff0c;而是智能体系统里的上下文协商协议 第一次在 GitHub 的某个 MCP 协议实现仓库里看到 context-mode 这个字段时&#xff0c;我下意识以为是某种调试开关——比如 --context-modeverbose 或 context_mode: true 。结果跑通 de…

作者头像 李华
网站建设 2026/9/10 8:35:02

我的数据伦理案例研究

我的数据伦理案例研究 【免费下载链接】Data-Science-For-Beginners 10 Weeks, 20 Lessons, Data Science for All! 项目地址: https://gitcode.com/GitHub_Trending/da/Data-Science-For-Beginners 1. 所选伦理挑战 &#xff08;从 10 类挑战中明确选择一项&#xff0…

作者头像 李华
网站建设 2026/9/10 8:34:52

context-mode:大模型对话中的上下文编排与工程实践

1. 为什么需要 context-mode&#xff1a;从一次线上事故说起先讲一个我实际经历过的场景。之前给一家企业做智能客服系统&#xff0c;业务方提了个需求&#xff1a;用户咨询时&#xff0c;如果能知道“他刚才在浏览哪个页面”“当前是售前还是售后阶段”“是否已经确认过订单信…

作者头像 李华
网站建设 2026/9/10 8:32:37

RNOH横竖屏切换实战:从尺寸监听到状态恢复的完整适配方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:32:06

2026企业级AI Agent竞争版图:四类玩家的工程较量与落地路线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:31:43

绝缘子自爆检测实战:从滑动窗口切图到YOLOv8训练

简介&#xff1a;这是一份面向电力巡检与计算机视觉研究者的绝缘子自爆点目标检测数据集&#xff0c;由无人机或巡检机器人在塔内作业时拍摄&#xff0c;聚焦玻璃绝缘子串上自爆缺陷的定位与识别&#xff0c;既可用于独立检测任务&#xff0c;也可衔接语义分割流程。全部数据共…

作者头像 李华