1. 为什么今天还要回头啃 JSTL刚入行那会儿我接手的第一个老项目就是一套典型的 JSP Servlet 架构页面里到处是% %脚本片段一个列表页能塞进去两百多行 Java 代码改一个字段要在 HTML 和 Java 之间来回跳。后来带我的老哥甩给我一句“你去把 JSTL 学一下”我才第一次系统接触这套标签库。十几年过去前后端分离成了主流JSTL 看起来像是上个时代的产物但只要你还在维护存量系统、还在做 JSP 页面改造、还在给一些内网管理后台做迭代它就绕不开。JSTL 全称是 JSP Standard Tag Library也就是 JSP 标准标签库。它做的事情说白了就一件把原本写在 JSP 页面里的 Java 逻辑用标签的形式表达出来让页面回归“展示”这件事本身。它主要解决三类问题——条件与循环控制、数据格式化与国际化、对集合和字符串的常用操作。适合谁来学如果你正在维护 JSP 老项目、需要读懂别人写的标签代码、或者要给一套遗留后台做小步改造那这篇内容就是给你准备的。哪怕你是纯前端出身理解 JSTL 也能帮你更快看懂服务端渲染那套思路。我先把结论摆在这JSTL 不难难的是很多人学的时候只记住了标签怎么写却没搞明白“为什么这么设计”“什么时候该用它、什么时候不该用”。下面我按自己实际踩过的路把这块内容拆开讲透。2. JSTL 的整体设计与选型思路2.1 它到底想解决什么问题在没有 JSTL 的年代JSP 页面里写循环是这么个画风% ListUser users (ListUser) request.getAttribute(users); for (int i 0; i users.size(); i) { User u users.get(i); % trtd% u.getName() %/td/tr % } %这段代码能跑但问题一大堆。第一页面里混入 Java 逻辑前端同学看不懂后端同学改起来也容易漏掉括号。第二% %这种表达式不做转义用户输入里带个尖括号就可能出问题。第三逻辑和展示耦合想换个展示方式得动 Java 代码。JSTL 的设计目标就是把这层逻辑“标签化”。同样一个循环用 JSTL 写出来是这样c:forEach varu items${users} trtdc:out value${u.name}//td/tr /c:forEach标签是自闭合的、成对出现的结构一眼能看清。c:out默认会做 HTML 转义安全性也顺带提升了。这就是它存在的核心理由让页面只负责展示逻辑用声明式标签表达。2.2 五大标签库的分工JSTL 并不是一个单一标签库它按功能拆成了五块每块有自己的命名空间前缀。这个划分很多人一开始会搞混我整理成表格方便对照前缀名称主要用途典型标签c核心库条件、循环、变量、输出、URLc:if、c:forEach、c:setfmt格式化库日期、数字格式化国际化fmt:formatDate、fmt:formatNumberfn函数库字符串、集合的常用函数fn:length、fn:containssql数据库库页面内直接执行 SQLsql:query、sql:updatexmlXML 库解析和操作 XMLxml:parse、xml:out这里有个经验之谈sql 和 xml 这两个库在实际项目里几乎不用。sql 库把数据库操作写到页面上等于把分层架构彻底打碎维护起来是灾难xml 库在 JSON 当道的今天也基本退场。真正高频的是 c、fmt、fn 这三个学习精力优先砸在这上面。2.3 为什么选标签而不是别的方案有人会问既然要解耦为什么不直接上模板引擎比如 FreeMarker、Thymeleaf这就要看场景了。JSTL 的最大优势是它是 JSP 规范的一部分不需要额外引入依赖只要你的容器支持 JSP它就能用。对于存量项目改造引入新模板引擎意味着要重写整个视图层成本极高而 JSTL 可以一个页面一个页面地渐进替换风险可控。另一个考量是学习曲线。JSTL 的标签数量有限核心常用的也就十来个一个下午就能上手。模板引擎虽然表达力更强但要重新学一套语法和配置。所以在“老项目小步快跑”这个场景下JSTL 是性价比最高的选择。注意新项目如果从零开始我个人的建议是优先考虑前后端分离或现代模板引擎JSTL 更适合作为存量系统的维护和改造工具而不是新项目的首选。3. 核心标签的实操要点与避坑3.1 环境准备两个 jar 包别搞错JSTL 的使用需要引入实现包。这里有个经典坑API 包和实现包是分开的。早期很多人只引了一个jstl.jar结果运行时报ClassNotFoundException。正确的做法是引入 API 和实现两个包或者直接用一个整合包。如果你用 Maven 管理依赖坐标大致是这样dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency引入之后在 JSP 页面顶部声明要用的标签库% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %uri这一串看着长但它是固定的直接抄就行。prefix可以自己改但强烈建议保持 c、fmt、fn 这套约定俗成的写法否则别人接手你的代码会一脸懵。3.2 c:forEach用得最多也最容易出错c:forEach是核心库里的绝对主力几乎每个列表页都离不开它。它的基本属性有这么几个items要遍历的集合或数组var每次迭代的当前元素varStatus迭代状态对象能拿到索引、序号、是否首尾begin/end/step控制遍历范围和步长一个典型用法c:forEach varitem items${orderList} varStatusst tr class${st.index % 2 0 ? even : odd} td${st.count}/td tdc:out value${item.orderNo}//td tdfmt:formatDate value${item.createTime} patternyyyy-MM-dd HH:mm//td /tr /c:forEach这里st.index从 0 开始st.count从 1 开始做斑马纹和序号时特别有用。st.first和st.last是布尔值判断首尾行很方便。踩过的坑items为 null 时c:forEach不会报错而是直接不渲染。这个特性有时候是好事但如果你期望它至少渲染个表头就得配合c:if判断。另外items传的是 EL 表达式${}千万别写成% %两者混用会出问题。3.3 c:if 与 c:choose条件判断的两种姿势c:if只能处理单分支没有else。想要多分支得用c:choosec:whenc:otherwisec:choose c:when test${user.level 1}普通用户/c:when c:when test${user.level 2}高级用户/c:when c:otherwise未知等级/c:otherwise /c:choose很多人第一次用c:if会习惯性想写else结果发现没有然后硬生生写两个c:if用相反条件。这样能跑但可读性差条件一多就容易出错。多分支场景一律用c:choose这是更规范的做法。test属性里写的是 EL 表达式返回布尔值。注意字符串比较要用eq或别用是赋值。这个坑我在代码评审里见过不止一次。3.4 c:set 与 c:out变量与安全输出c:set用来在指定作用域里设值c:set vartotal value${price * count} scoperequest/scope可以是 page、request、session、application默认是 page。设完之后用${total}就能取到。c:out的重点在于转义。它有个escapeXml属性默认是 true会把、、这些字符转成实体。这意味着用户输入里的恶意脚本会被当成普通文本显示而不是被执行。所以凡是输出用户可控内容的地方都应该用c:out而不是直接${}。提示${}直接输出不会转义c:out默认转义。安全敏感的场景优先用c:out。3.5 fmt 库日期和数字的格式化日期格式化是 fmt 库的高频用法fmt:formatDate value${order.createTime} patternyyyy年MM月dd日/ fmt:formatNumber value${order.amount} pattern#,##0.00/pattern的写法和 Java 的 SimpleDateFormat 一致。数字格式化里#,##0.00表示千分位加两位小数金额展示特别常用。国际化方面fmt:setLocale和fmt:message配合资源文件使用能实现多语言切换。不过说实话在只做单一语言的后台系统里这块用得不多了解即可。3.6 fn 库字符串处理的利器fn 库提供了一堆静态函数调用方式是${fn:函数名(参数)}。常用的有函数作用示例fn:length取长度${fn:length(list)}fn:contains是否包含${fn:contains(str, abc)}fn:substring截取${fn:substring(str, 0, 5)}fn:trim去空格${fn:trim(str)}fn:split分割${fn:split(str, ,)}fn:toUpperCase转大写${fn:toUpperCase(str)}一个实用场景是列表展示时截断过长的文本c:out value${fn:length(article.title) 20 ? fn:substring(article.title, 0, 20) : article.title}/这样超过 20 个字符的标题会被截断避免撑破表格布局。4. 完整实操流程与关键环节4.1 从零搭一个可运行的 JSTL 页面光看标签说明容易忘我带你走一遍完整流程。假设你有一个 Servlet 往 request 里塞了一个用户列表要在 JSP 里渲染出来。第一步Servlet 端准备数据ListUser users userService.findAll(); request.setAttribute(users, users); request.getRequestDispatcher(/list.jsp).forward(request, response);第二步JSP 页面声明标签库并渲染% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html body table thead trth序号/thth姓名/thth注册时间/th/tr /thead tbody c:forEach varu items${users} varStatusst tr td${st.count}/td tdc:out value${u.name}//td tdfmt:formatDate value${u.regTime} patternyyyy-MM-dd//td /tr /c:forEach /tbody /table /body /html第三步部署运行访问对应 URL 就能看到渲染结果。整个过程不需要在页面里写一行 Java 代码逻辑全在 Servlet展示全在 JSP职责清晰。4.2 参数计算与选择过程这里有个容易被忽略的细节作用域的选择。c:set的scope默认是 page意味着只在当前页面有效。如果你在 include 的片段里设了值想在主页面用就得设成 request。我见过有人设了 page 作用域然后在另一个 include 文件里取不到值排查半天。再一个是集合为空时的处理。c:forEach遇到空集合不渲染但页面可能显示一个空表格体验不好。规范做法是配合c:ifc:if test${empty users} trtd colspan3暂无数据/td/tr /c:ifempty这个关键字能同时判断 null 和空集合比users null || users.size() 0简洁得多。4.3 实操现场记录一次列表页改造我之前改造过一个订单列表页原页面用脚本片段写了 180 多行 Java 代码包含分页计算、状态判断、日期格式化。改造思路是分三步走第一步把数据准备逻辑全部挪到 Servlet页面只留展示。第二步用c:forEach替换 for 循环用c:choose替换 if-else 链。第三步用fmt:formatDate替换 SimpleDateFormat 调用。改造后页面从 180 行降到 60 行而且前端同学能直接看懂结构了。中间遇到一个坑原来的状态判断用了枚举的ordinal()值改成 EL 表达式后不能直接调方法得在 Servlet 里把状态转成字符串再传过来。这个转换看着多余但符合“页面不做逻辑”的原则反而更清晰。5. 常见问题与排查技巧实录5.1 标签不生效的几种典型情况现象可能原因排查方向标签原样输出标签库未声明或 uri 写错检查 taglib 指令报 ClassNotFoundException缺 jstl 实现包检查依赖是否完整EL 表达式不解析web.xml 版本过低或 isELIgnored检查 servlet 版本配置中文乱码页面编码未设置检查 pageEncoding 和 contentType标签原样输出是最常见的八成是uri抄错了或者前缀对不上。EL 不解析这个问题在老项目里也常见有些老版本 web.xml 默认关闭了 EL需要在 page 指令里加isELIgnoredfalse。5.2 EL 表达式取不到值的排查思路取不到值先按这个顺序查作用域对不对、属性名拼写对不对、getter 方法存不存在。EL 表达式${user.name}本质是调用user.getName()如果 User 类没有这个 getter就会报属性找不到。这个错误信息有时候不明显容易误以为是作用域问题。还有一个隐蔽的坑属性名大小写。EL 对属性名是大小写敏感的${user.Name}和${user.name}是两回事。我见过有人数据库字段是user_name映射到实体是userName页面里写成${user.username}怎么都取不到。5.3 性能相关的注意事项JSTL 标签在页面渲染时会被编译成 Java 代码执行性能上基本不用担心。但有两个点要注意一是避免在循环里做复杂运算比如在c:forEach里反复调用 fn 函数处理大字符串会拖慢渲染二是合理使用 c:set 缓存计算结果如果一个值在页面里用多次先 set 再引用比每次重新计算划算。提示JSTL 的性能瓶颈通常不在标签本身而在页面里嵌套了过多的数据库查询或远程调用。视图层就该干视图层的事。5.4 独家避坑清单别在页面里写业务逻辑JSTL 能写条件判断但不代表该把业务规则塞进来。状态码转文字这种事能在后端转就在后端转。c:out 和 ${} 别混用安全敏感的地方统一用 c:out避免遗漏转义。fn:substring 注意越界截取前先判断长度否则字符串短于截取位置会抛异常。fmt 的 pattern 别写错mm是分钟MM是月份写反了日期就错了这个坑新手必踩。标签嵌套注意闭合JSTL 标签必须成对闭合嵌套层级多了容易漏建议写完立刻检查缩进。6. 从 JSTL 看视图层设计的取舍学 JSTL 的过程其实也是理解“视图层该承担什么职责”的过程。它用标签把逻辑从页面里抽出来但抽得并不彻底——条件判断、循环这些控制流还是留在了页面。这是它的局限也是它的定位它不追求彻底消灭页面逻辑只追求让页面逻辑变得可读、可控、可维护。站在今天回看JSTL 的很多设计思路被后来的模板引擎继承和发展了。Thymeleaf 的自然模板、Vue 的指令系统本质上都在解决同一个问题怎么让展示层既灵活又不失控。理解了 JSTL 的取舍再看这些新工具会更容易抓住它们的设计意图。如果你手上正好有 JSP 老项目我的建议是别急着推翻重写先用 JSTL 把页面里的脚本片段一点点替换掉把逻辑往 Servlet 或 Service 层挪。这个过程本身就是一次代码梳理改完之后你会发现哪怕不换技术栈可维护性也能上一个台阶。至于新项目JSTL 可以作为了解服务端渲染的入门砖但选型时还是要结合团队和场景综合判断别为了“统一”而硬上。
阅读完成 · 觉得有帮助?