6.2 Jasper:JSP 的编译之旅 本节摘要:JSP 不是被"解释执行"的,它要经 Jasper 走完一条四级流水线——解析为语法树、翻译成 Servlet 源码、编译为字节码、加载实例化——然后才成为 6.1 里的普通 Servlet。本节拆解流水线每一级的产物与存放位置(work 目录寻宝)、首访慢的量化账单与预编译解法、开发模式与生产模式的切换。学完你对"JSP 到底是什么"会有确定性的答案。 终点站里有一批住户不是人手写出来的——JSP 页面是它们的"受精卵形态"。6.1 讲了 Servlet 的执行机制,本节讲这些 Servlet 是怎么被 Jasper"生"出来的。这条流水线也解释了三个日常谜题:首访慢、work 目录膨胀、改一行 JSP 为何只重新编译一个文件。
本节摘要:JSP 不是被"解释执行"的,它要经 Jasper 走完一条四级流水线——解析为语法树、翻译成 Servlet 源码、编译为字节码、加载实例化——然后才成为 6.1 里的普通 Servlet。本节拆解流水线每一级的产物与存放位置(work 目录寻宝)、首访慢的量化账单与预编译解法、开发模式与生产模式的切换。学完你对"JSP 到底是什么"会有确定性的答案。
终点站里有一批住户不是人手写出来的——JSP 页面是它们的"受精卵形态"。6.1 讲了 Servlet 的执行机制,本节讲这些 Servlet 是怎么被 Jasper"生"出来的。这条流水线也解释了三个日常谜题:首访慢、work 目录膨胀、改一行 JSP 为何只重新编译一个文件。

流水线的触发点值得单独点明:第一次请求某个页面时才走全流程,不是应用部署时。此后按文件时间戳做增量——只有被修改的页面重新编译。这意味着同一页面第一次访问与第二次访问走的是完全不同的代码路径,压测报告里"冷启动首屏慢"的元凶常常就是它。
写一个带循环的订单列表页,语言层面只用到最小集合。
<%@ page contentType="text/html;charset=UTF-8" %> <%@ page import="java.util.List" %> <% // 脚本块里的 Java 会被原样搬进生成的 Servlet List<String> orders = List.of("订单一 已发货", "订单二 待支付", "订单三 已签收"); %> <html><body> <h2>我的订单</h2> <ul> <% for (String o : orders) { %> <li><%= o %></li> <% } %> </ul> </body></html>
部署后首次访问,然后去 work 目录寻宝。
# work 目录按应用分层 找翻译产物 find work -name '*order*list*jsp*.java' -o -name '*order*list*jsp*.class' | head -n 4 head -n 12 "$(find work -name '*order*list*jsp*.java' | head -n 1)"
work/Catalina/localhost/order/org/apache/jsp/order_005flist_jsp.java work/Catalina/localhost/order/org/apache/jsp/order_005flist_jsp.class // 生成的源码开头 类名由页面路径转写而来 public final class order_005flist_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent {
三样东西坐实了"JSP 即 Servlet":源码文件证明翻译真的发生,类文件证明编译完成,类继承的基类正是 Servlet 家族的一员(它间接继承 HttpServlet,service 方法由父类回调到页面生成的执行方法)。类名里的数字编码是路径中非字母数字字符的转义——看懂这个命名,work 目录里翻产物就不再靠运气。
把首访耗时拆开算:解析快、翻译快,编译是绝对大头——启动编译器进程或加载编译器组件都是百毫秒级动作,页面多、依赖标签库多时更甚。三个解法按工程化程度排序。
其一,访问预热:发布脚本在切流量前把每个页面请求一遍,简单直接,页面多时脚本丑陋。其二,容器自带的预编译任务:构建阶段把全部页面提前编译好,产物直接进 WAR 包,发布后没有首访账单,流水线前三级的成本全部转移到构建机。其三,干脆少用 JSP——前后端分离架构下页面由前端框架渲染,接口层是纯 Servlet 或更高层的框架,Jasper 只在遗留系统里继续服役。三种方案对应三种架构阶段,没有高下只有合适。
另一个相关开关是开发模式标志:开发模式 true 时容器允许按需检测页面更新,生产模式 false 时页面改了也不重编(除非重新部署),省掉每次请求的时间戳检查开销。遗留系统上线前记得确认这个开关处于生产位——它在应用的 web.xml 或 JSP 配置里,属于"配错也不报错、只在压测曲线里现形"的隐性参数。
⚠️ 常见坑:页面里写了编译不过的 Java 代码,浏览器看到的错误页像运行时异常,日志里的真实栈帧指向生成的源码文件与行号——去 work 目录打开对应源码文件对行号,排错效率远高于盯着 JSP 猜。记住:JSP 的报错要按"它是个 Servlet"来读。
页面顶部的指令标签控制 Jasper 的翻译行为,四个最常用。
| 指令属性 | 作用 | 常用值与场景 |
|---|---|---|
| contentType | 响应类型与编码 | 文本页面统一带字符集,防乱码 |
| import | 引入 Java 类型 | 等价源码里的导入语句,可多条 |
| errorPage | 本页异常转投的页面 | 友好错误页兜底,配合全局错误页使用 |
| session | 是否参与会话 | 纯展示页设为不需要,省会话开销 |
<%@ page contentType="text/html;charset=UTF-8" session="false" %>
这一行同时示范了两件事:编码在页面级声明覆盖全局约定;不需要会话的页面显式关掉,能显著降低高流量站点的会话内存占用——每个访客少建一个会话对象,乘以流量就是可观的一笔。
终点站还剩最后一批特殊旅客——WebSocket 的连接不走一问一答,Spring Boot 干脆把整座管道装进了应用。下一节收尾终点站。