本节摘要:脚本元素是在页面里直接写 Java 代码的三种语法——脚本片段落入服务方法体、表达式变成输出语句、声明升格为类成员。三者共用百分号括号,翻译去向却分属类的三个区域,这决定了它们各自的生命周期与线程安全属性。本章是语法清点的第一站:读懂三件套,就拿到了给老页面定风险等级的标尺。
上一章你已经在链路图上见识过服务方法多线程并发的分量,本节把镜头对准代码本身:那些代码是用什么语法写进页面、又落在类的哪个位置的。看一个把三件套凑齐的页面片段:
<%! // 声明区:翻译后是类的成员,全站共享 private int totalViews = 0; private String badge(int n) { return n > 100 ? "热门" : "常客"; } %> <html> <body> <% // 脚本片段:翻译后是服务方法体里的局部代码 totalViews++; String user = request.getRemoteUser(); %> <p>访问累计:<%= totalViews %> 次</p> <p>访客等级:<%= badge(totalViews) %></p> </body> </html>
三种括号只有开头的半个符号不同——<%、<%=、<%!——但翻译后分道扬镳。声明区代码进了类成员区,脚本片段进了服务方法体,表达式则变成一条输出语句。把去向画出来,三者的身份差异一目了然。

这张图值得打印出来贴在屏幕边上。以后读任何一个老页面,视线扫到 <%! 就自动亮红灯:这行代码在单例里。扫到 <% 则想一层:这里的变量是局部安全的,但它访问了谁——如果访问了成员区或 application,风险又绕回去了。
三件套之外还有一对容易忽视的符号:注释。HTML 注释以双横线括号包裹,内容会原样输出到浏览器——用户查看源代码就能看到。JSP 隐藏注释以百分号双横线开头,翻译时被整段丢弃,永远不会离开服务器。
<!-- 数据库连接串见内部文档第 3 页:这是一次真实的信息泄露 --> <%-- 下个迭代替换为缓存方案,本次先保留直查 --%>
上面第一行就是老站里最典型的安全隐患样本:当年的开发者把部署细节、调试信息、甚至半截代码写进 HTML 注释,全部随页面发给了全世界。接手青梧书肆后,值得安排一次全站注释审计——凡涉及内部结构的注释,一律改写为隐藏注释。这也是第 7 章安全补课的前置项之一。
背景:图书详情页偶尔显示乱码日期,比如"2026-08-2"后面跟一串错位字符,概率约千分之几,无法稳定复现。该页面是基岩层老代码,历史比站内大多数文件都老。
操作:按本节的方法读页面。头部声明区里躺着这样一段:
<%! private java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("yyyy-MM-dd"); %> <%= fmt.format(new java.util.Date()) %>
红灯亮起:格式化工具是成员变量,单例共享,而它不是线程安全的——多个请求线程同时调用同一个格式化实例,内部日历字段互相踩踏,输出自然偶尔错乱。
结果:把实例化语句搬进脚本片段,让它成为服务方法的局部变量,随请求创建。错乱消失。
解读:这个案例是"位置决定性质"的完美注脚。同样的类,放在局部每请求一份,安全;放在成员区全站共享,就是并发隐患。老代码里所有"偶发、低频、不可复现"的错乱,都值得先用本节的位置分析法过一遍——成员区里的任何可变对象,默认都该按线程不安全对待。
变式:如果格式化开销确实大、不想每请求新建,可以改用线程安全的日期格式化方案,或用容器提供的对象池按线程隔离。判断依据是调用频率与对象创建成本——但永远不要用"共享一个实例"来省这点开销,账算不过来。
💡 关键直觉:给老页面做体检时,先找
<%!。声明区是整页风险密度最高的十行——它天然共享、天然跨请求、天然逃离局部变量的一切保护。
本站的知识有个特点:五分钟就能亲手验证。在第 1 章搭好的环境里建一个试验页面,三种括号各写一段带"身份标记"的代码:脚本片段里定义一个局部变量并输出时间戳;声明区里定义一个成员计数器做累加;表达式输出计数器的当前值。然后开两个不同浏览器窗口交替刷新——你会看到成员计数器跨越窗口持续增长(它在单例里),而局部变量的时间戳每个请求各自独立(它在方法体里)。再往声明区放一个格式化对象并并发刷新,格式偶发错乱的现象也大概率能复现。
这个实验的意义不止于验证:它把"位置决定性质"从一句结论变成一次亲眼所见。日后你给团队讲为什么声明区危险,不用背条文,直接把试验页面丢给对方跑一遍。维护老站的教学成本,很多都省在这种五分钟实验上。
既然声明区是雷区,改造时它的代码都去了哪?三种典型归宿,按优先级排列。第一,搬进服务层:声明区里但凡带业务语义的(格式化规则、状态计算、数据加工),归宿都是第 4 章的服务层——视图不再持有逻辑,这占大多数。第二,转成标签文件:纯展示辅助(把状态码画成徽章、按等级输出样式名)留在视图层但换成标签文件,第 6 章车间的产品正好接住它们。第三,就地转正:极少数确实属于"页面自身状态"的(比如渲染进度指示),改写进脚本片段并加注释说明为何保留——这是例外通道,需要评审签字。青梧书肆清点时的统计是:声明区代码七成进了服务层、两成进了标签文件、一成经评审就地保留。给声明区做改造时先把三种归宿的口径定下来,动手就不会犹豫;怕的恰恰是边改边纠结,改到最后变成半吊子混搭。
脚本元素管的是"写逻辑",但一个页面除了逻辑还需要配置——编码、错误页、公共片段、标签库。下一站清点三大指令,看那些以 @ 开头的行如何决定页面的编译行为。