6.1 Filter 链与 Servlet 的执行 本节摘要:终点站的执行机制核心是控制权接力——过滤器链按配置顺序逐环调用,chain 的 doFilter 是交接动作,前置代码在下行时执行、后置代码在回程时执行;Servlet 以实例为单位走"实例化、初始化、服务、空闲、销毁"五阶段,单实例多线程是它的并发模型。本节用调用栈图、完整代码与一次顺序实验把机制讲透。 5.1 认识了过滤器这道闸的位置,本节拆开闸盒看齿轮:链怎么排序、控制权怎么传递、Servlet 怎么活完一生。这里的机制知识是第 7 章性能排障的底层依据——线程栈里的每一帧都能对上本节的调用链。
本节摘要:终点站的执行机制核心是控制权接力——过滤器链按配置顺序逐环调用,chain 的 doFilter 是交接动作,前置代码在下行时执行、后置代码在回程时执行;Servlet 以实例为单位走"实例化、初始化、服务、空闲、销毁"五阶段,单实例多线程是它的并发模型。本节用调用栈图、完整代码与一次顺序实验把机制讲透。
5.1 认识了过滤器这道闸的位置,本节拆开闸盒看齿轮:链怎么排序、控制权怎么传递、Servlet 怎么活完一生。这里的机制知识是第 7 章性能排障的底层依据——线程栈里的每一帧都能对上本节的调用链。
用户在订单页按下查询按钮,请求带着表单参数出发,过城门(第 3 章)、过迷宫(第 4 章)、过关卡(第 5 章),最后停在一段这样的代码前面:编码过滤器、登录过滤器、订单查询 Servlet。三者的执行不是"依次跑完",而是嵌套——这是本节最重要的一个认知。
调用栈画出来是一副弹簧手串的形状。
编码过滤器 doFilter 前置段 ← 下行途中执行 登录过滤器 doFilter 前置段 ← 下行途中执行 Servlet service 方法 ← 链底 业务处理 登录过滤器 doFilter 后置段 ← 回程途中执行 编码过滤器 doFilter 后置段 ← 回程途中执行
两行 filter-mapping 的先后顺序决定谁在外谁在内:web.xml 里先声明的在外环。想验证不必背规则,写两个过滤器各打两行日志。
// 过滤器甲:前置后置各打一行 import jakarta.servlet.*; import java.io.IOException; public class FilterA implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { System.out.println("A 前置 找到登录过滤器之前的活"); chain.doFilter(req, resp); // 交接 控制权流向下一环 System.out.println("A 后置 响应已经生成"); } }
<!-- web.xml:声明顺序即嵌套顺序 A 在外 B 在内 --> <filter-mapping><filter-name>filterA</filter-name><url-pattern>/*</url-pattern></filter-mapping> <filter-mapping><filter-name>filterB</filter-name><url-pattern>/*</url-pattern></filter-mapping>
一次请求的日志输出是四行:A 前置、B 前置、B 后置、A 后置。若日志顺序成了 A 前置、B 前置、A 后置、B 后置,说明某个过滤器忘了调用 chain 的 doFilter——链断了,后面的环与 Servlet 都不会执行,响应多半是空白。忘了交接是过滤器开发的头号新手病,症状就是"页面空白但没报错"。
顺带三个实践要点。其一,纯前置逻辑(如设置字符集)写在 chain 调用之前,纯后置逻辑(如记录耗时、压缩响应)写在之后,位置写反了拿不到想要的状态。其二,过滤器链对静态资源同样生效——默认 Servlet 也是终点住户,url-pattern 写成全路径时静态请求也会过闸,性能敏感的站点要留意这层开销。其三,异步请求(返回后业务还在异步跑)会把链的回程时机推迟到异步完成,jstack 里看到的栈与此处描述会不同,属正常现象。
Servlet 的生命周期五阶段:加载并实例化、初始化、服务、空闲待命、销毁。每阶段对应一个回调,容器负责按时调用。
// 完整生命周期演示 初始化参数的读取也在初始化阶段 import jakarta.servlet.*; import jakarta.servlet.http.*; import java.io.IOException; public class OrderQueryServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException { // 每个实例只执行一次 适合读配置 建缓存 String pageSize = config.getInitParameter("pageSize"); System.out.println("初始化完成 每页条数 " + pageSize); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 服务阶段 每个请求一次 并发时多个线程同时进来 resp.setContentType("text/plain;charset=UTF-8"); resp.getWriter().println("订单查询结果"); } @Override public void destroy() { // 销毁前执行一次 释放资源 System.out.println("即将销毁 释放缓存"); } }
并发模型是笔试高频也是实践底线:默认一个 urlPattern 对应一个 Servlet 实例,所有请求线程共享它。doGet 里的成员变量就是共享变量,两个请求同时写一个字段,数据错乱不报错、只在业务对账时发作。三类写法天然安全:方法内局部变量、无状态工具调用、并发容器。要写可变共享状态就得加锁或用并发容器——但更好的答案是重构掉状态。
实例数量可以改变吗?可以,实现单线程模型的接口已废弃,现代做法是不动实例数、动线程池(3.2 的 maxThreads 才是并发闸门)。初始化时机也可控:4.3 见过的 load-on-startup 决定是"启动就初始化"还是"首请求时初始化",前者把初始化耗时挪到启动窗口,后者让第一个用户买单——生产环境对首屏有要求的 Servlet 都该配启动加载。
💡 关键直觉:把过滤器链与 Servlet 服务阶段连起来看,一个请求的业务耗时由三段组成:链上前置、业务本身、链上后置。第 7 章做慢请求排查时,先用本节的日志切分三段,再决定往哪段下钻——盲猜是排障大敌。
机制清楚了,下一节看一批"自动生成"的 Servlet——JSP 文件如何被 Jasper 编译出来,首访慢的账单如何算清。