2.6 JSP:一个装配工的没落
从页面脚本混写走到请求、模型、模板和转义响应分层,理解 JSP、标签库与模板引擎的责任边界。
学习目标
- 能沿 HTTP 请求、控制器、模型、模板、标签库和 HTML 响应追踪一次服务器端页面渲染
- 能解释 JSP、标签库和模板引擎如何减少脚本混写,同时保留转义、缓存和异常边界
- 能用正常、边界和单一故障样本定位模板缺值、上下文转义错误、渲染失败和缓存污染
2.6 JSP:一个装配工的没落
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.6 JSP:一个装配工的没落。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
服务器端动态页面的难题不是“能不能拼出 HTML”,而是如何让请求数据、业务查询、页面结构和安全输出各自有边界。JSP 曾让 Java 代码和标记快速靠近,标签库把常见控制流包装成标签,模板引擎进一步把视图变成可替换的渲染层;但任何工具都不能自动知道一个值将进入 HTML 文本、属性、URL、JavaScript 还是 CSS 上下文。
三个会让服务端页面失真的陷阱
七个目录节点到渲染证据
黑暗岁月
↡页面文件同时承担请求处理、业务查询、控制流和 HTML 拼接,短期交付快但长期难以测试与复用的阶段。不是对 JSP 的道德评价,而是一个可观察的耦合形态。诊断时看页面是否直接拿连接、修改状态、读取会话和拼接未转义值;只要跨层隐式发生,改动就会同时影响业务和视图。
服务器端动态页面
↡服务器接收 HTTP 请求,准备模型并在服务端生成 HTML 或其他表示,再以 HTTP 响应发送给客户端的页面路径。应明确请求方法、参数、身份、模型版本、响应状态和缓存策略。模板负责把已批准的数据变成表示,不能替代授权、事务或输入校验。
标签库
↡把循环、条件、格式化、链接和安全输出等常见视图操作封装为可组合标签,降低页面脚本密度。是视图层工具,不是万能安全层。标签需要定义输入类型、空值行为、转义策略和异常传播;一个标签自动转义 HTML 文本,不代表它能安全生成 JavaScript 或 URL。
模板引擎
↡读取模板结构和模型数据,按规则生成目标表示的渲染组件,通常把控制器与页面格式分开。让视图可替换、可测试和可缓存。它的输入应是明确模型,模板语法和用户数据必须分开;渲染失败要返回可识别的错误响应,不能把半截 HTML 当成成功页面。
草根搅局
↡低门槛标签、模板和开源组件让更多开发者能扩展页面能力,也使默认配置和组合风险更广泛传播。带来速度和生态,但扩展标签、模板片段和插件都要经过版本、权限、转义和依赖审查。可组合不等于可任意执行;把第三方片段当成代码依赖管理。
2.6 JSP:一个装配工的没落
标题中的“没落”指的是页面装配工作逐渐被控制器、标签库和模板引擎分担,而不是服务器端渲染失去价值。2.6 JSP:一个装配工的没落的工程结论是:表示层可以演进,但请求、模型、渲染、转义和缓存边界必须可追踪。
最小服务器端渲染合同
record PageModel(String title, List<String> items, String requestId) {}
PageModel loadModel(HttpServletRequest request) {
String requestId = request.getHeader("X-Request-Id");
String query = request.getParameter("q");
return new PageModel(
queryService.findTitle(query),
queryService.findItems(query),
requestId == null ? "generated" : requestId
);
}
String render(PageModel model) {
return template.render(model); // 每个插槽按自身上下文转义
}合同把请求输入、业务读取、模型和渲染分开。模板只接收模型,不直接执行写入;每个动态插槽要记录 HTML 文本、属性、URL 或脚本上下文;响应状态、缓存头和错误页也属于合同。
四步复核一条页面渲染链
1. 接收请求并固定身份
记录方法、路径、参数、身份、请求 ID、语言和缓存条件。正常样本使用可信查询;边界样本为空值、超长值、未授权身份和重复请求,先确认控制器不会把原始输入当模板。
Lab
JSP 渲染边界实验
只改变一个模型、插槽或缓存条件,观察响应内容与安全判定如何变化。
控制器提供最小模型,模板生成完整 HTML
request-id → authorized model → escaped text/URL → 200 complete response
判定
accept:响应完整且缓存键可解释
当前样本:正常渲染;保存请求 ID、模型版本、插槽上下文、缓存键和响应头。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 模型完整、模板版本匹配、上下文明确 | 生成完整响应并正确转义 | 请求 ID、模型摘要、响应头、HTML |
| 边界 | 空值、长值、缓存身份或模板缺值 | 明确空态/拒绝/失效,不串用户数据 | 上下文、缓存键、状态码、响应片段 |
| 单一故障 | 一个业务查询、标签或模板依赖失败 | 错误页完整、无堆栈泄露、可重放 | 首个失败、异常映射、响应、恢复 |
故障诊断:先定位数据进入了哪一层
- 请求层:检查参数、身份、CSRF/授权和请求 ID;原始用户输入不应直接成为模板或脚本。
- 模型层:检查业务查询、字段权限、空态、模型版本和缓存键;模型错误不能靠模板条件掩盖。
- 渲染层:检查标签/模板版本、插槽上下文、转义和异常;HTML 文本安全不代表 URL 或脚本安全。
- 响应层:检查状态码、Content-Type、字符集、安全头、缓存和完整性;失败响应不能泄露堆栈或半截敏感内容。
如果响应内容正确但用户之间串数据,先查缓存键;如果页面出现脚本执行,先查插槽上下文和转义;如果模板偶尔返回半页,查异常传播和响应缓冲。诊断要沿请求 ID 从输入到响应闭环。
最小页面证据包与反例
证据包包含请求方法/路径摘要、身份与租户、参数脱敏值、控制器分支、模型字段摘要、模板/标签版本、插槽上下文、转义动作、缓存键、响应状态/头、错误 ID 和重放结果。不要记录完整敏感内容,但要保留区分用户和版本所需的身份摘要。
反例一是 HTML 文本插槽转义后,把同一个值未经 URL 编码放入链接;反例二是模板缓存只按 URL 键,导致用户 A 的模型片段进入用户 B 的响应;反例三是标签失败后仍返回 200 和半页 HTML。固定单一变量重放并记录最早偏差。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 黑暗岁月
请求处理、业务查询、控制流和 HTML 拼接混在页面文件中的高耦合阶段。
- 服务器端动态页面
服务端依据 HTTP 请求和模型生成 HTML 等表示并返回响应的页面路径。
- 标签库
把循环、条件、格式化和输出操作封装成可组合视图标签的组件集合。
- 模板引擎
读取模板结构和模型数据并生成目标表示的渲染组件。
- 草根搅局
低门槛标签、模板和开源扩展带来的生态速度与组合风险。
练习
练习
问题 1: 页面显示的 HTML 文本已转义,但点击链接时仍能改变目标地址。为什么一个 escape 函数不够?
问题 2: 用户 A 访问后,用户 B 偶尔看到 A 的页面片段。你先查哪三项?
问题 3: 模板标签抛错,但服务仍返回 200 和半页 HTML。怎样改进?
本页小结
2.6 JSP:一个装配工的没落的关键不是把 JSP 简单换成另一个模板引擎,而是把请求、模型、标签、模板、上下文转义和响应缓存分层。完成标准是能从请求 ID 追到页面输出,在边界与故障样本中阻止脚本注入、跨用户缓存和半截响应。