资讯详情

资讯详情

JSTL 标签库实战:从脚本片段到标签封装的核心技巧

1. 从脚本片段到标签封装JSTL 到底解决了什么问题很多人第一次接触 JSTL是在维护一个老项目的时候。打开 JSP 文件满屏的% for(...) { %、% if(...) { %和% request.getAttribute(xxx) %逻辑和页面混在一起改一个字段名要在好几个文件里翻来覆去地找。这种写法在早期确实能跑但一旦页面结构复杂起来维护成本会急剧上升。JSTL 的出现本质上就是为了把这种“Java 代码嵌在 HTML 里”的混乱局面收拾干净。JSTL 全称是 JavaServer Pages Standard Tag Library翻译过来就是 JSP 标准标签库。它做的事情说起来很简单用一套类似 HTML 的标签替代 JSP 页面里那些脚本片段。比如原本要写% if(user ! null) { %用 JSTL 就变成c:if test${not empty user}。看起来只是换了个写法但背后的意义完全不同——页面里不再有 Java 语法前端人员也能看懂逻辑判断和循环被封装成标签职责边界清晰了。我刚开始学的时候有个疑问既然有 EL 表达式了为什么还要 JSTL后来才明白EL 负责的是“取值”比如${user.name}把数据从作用域里拿出来而 JSTL 负责的是“控制”比如循环、条件判断、格式化输出。两者配合使用才能把 JSP 页面写得干净。只靠 EL你没法做循环也没法做条件分支页面照样得靠脚本片段撑着。JSTL 的核心价值可以归纳成三点。第一是可读性标签的语义比 Java 代码更直观c:forEach一眼就知道是循环不用去数花括号。第二是可维护性页面逻辑和 Java 代码分离改样式的人不用碰逻辑改逻辑的人不用管 HTML。第三是标准化JSTL 是规范的一部分不同容器比如 Tomcat、Jetty都支持不像自定义标签那样绑死在某个项目里。适合学 JSTL 的人其实很明确如果你在维护传统的 JSP 项目或者需要快速搭建一个基于 JSP 的后台管理页面JSTL 是绕不开的。即使现在前后端分离是主流理解 JSTL 也能帮你读懂大量遗留系统这在接手老项目时特别有用。接下来我会从环境搭建开始一步步把 JSTL 的核心标签、常见坑点和实战技巧讲清楚。2. 环境准备中最容易被忽略的两个细节2.1 依赖引入不只是放一个 jar 包那么简单JSTL 的使用需要引入对应的 jar 包。在早期版本中通常需要两个 jarjstl.jar和standard.jar。这两个包分别提供了 API 接口和具体实现。很多人第一次配置时只放了一个结果页面报ClassNotFoundException或者标签完全不被解析排查半天才发现是包不全。如果你用的是 Maven 项目依赖配置大概是这样dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency注意版本号。JSTL 1.2 是 Java EE 6 之后比较通用的版本支持 EL 2.2 的一些特性。如果你用的是更老的容器可能需要 1.1 版本这时候依赖坐标会不一样通常还要额外引入standard标签库的实现包。我建议先确认你的 Servlet 容器版本再决定用哪个版本的 JSTL不要盲目复制网上的配置。还有一个细节jar 包必须放在WEB-INF/lib目录下而不是项目的其他位置。有些人习惯把 jar 放在项目根目录的lib文件夹里然后在 IDE 里配置构建路径本地跑起来没问题但部署到服务器上就找不到类了。原因是 Web 应用运行时只认WEB-INF/lib和容器的公共类路径IDE 的构建路径配置不会被打包进去。这个坑我踩过不止一次后来养成习惯凡是 Web 项目的依赖一律放WEB-INF/lib。2.2 标签库声明uri 写错一个字符都白搭引入 jar 包之后还需要在 JSP 页面顶部声明标签库。标准写法是% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %这行代码里有两个关键点。prefixc是前缀你可以改成别的比如jstl但通常大家都用c因为 core 库的标签用得最多短前缀写起来方便。uri是标签库的唯一标识必须和 jar 包里META-INF目录下 tld 文件中定义的 uri 完全一致。我见过有人把http://java.sun.com/jsp/jstl/core写成http://java.sun.com/jstl/core少了一个/jsp结果标签全部失效页面直接把c:forEach当普通文本输出。不同标签库对应的 uri 也不一样。核心库是core格式化库是fmt函数库是functions数据库操作库是sqlXML 处理库是xml。实际开发中c和fmt用得最多functions偶尔用来做字符串处理sql和xml基本不建议在页面里用原因后面会讲。提示如果你在页面里写了标签但没声明 uri容器会直接忽略标签把它当成普通 HTML 标签输出到浏览器。这时候你会在页面上看到c:if这样的文字而不是执行结果。遇到这种情况第一件事就是检查 taglib 声明。3. 核心标签库的实战用法拆解3.1 c:forEach循环里的 varStatus 是个宝藏c:forEach是 JSTL 里使用频率最高的标签没有之一。基本用法很直观c:forEach items${userList} varuser tr td${user.name}/td td${user.email}/td /tr /c:forEachitems指定要遍历的集合var是每次迭代的变量名。这比脚本片段里的for(User user : userList)清爽多了。但很多人不知道的是c:forEach还有一个varStatus属性它提供了一个LoopTagStatus对象里面藏着不少有用的信息。varStatus常用的属性有四个index表示当前索引从 0 开始count表示当前是第几次迭代从 1 开始first和last是布尔值判断是不是第一条或最后一条。这些在渲染表格时特别有用。比如给奇数行和偶数行加不同的背景色c:forEach items${userList} varuser varStatusstatus tr class${status.index % 2 0 ? even : odd} td${status.count}/td td${user.name}/td /tr /c:forEach还有一个场景是分页导航。你可以在循环里用status.last判断是否到了最后一条然后在最后一条后面追加“加载更多”的按钮。这种写法比在 Java 代码里拼 HTML 要灵活得多。另外c:forEach还可以指定begin、end和step用来做固定次数的循环。比如你想渲染 1 到 10 的数字c:forEach begin1 end10 vari span${i}/span /c:forEach这种用法在生成序号、分页页码时很常见。注意end是包含的所以begin1 end10会输出 1 到 10 共十个数。3.2 c:if 与 c:choose条件判断的两种姿势c:if是最简单的条件标签只有一个test属性值为布尔表达式c:if test${not empty user} p欢迎${user.name}/p /c:if这里用了not empty来判断 user 是否为空。empty是 EL 里的关键字能同时判断 null、空字符串和空集合比写user ! null user.name ! 简洁得多。我建议在 JSTL 里判断空值时优先用empty少用 null因为empty的语义更准确能覆盖更多边界情况。c:if有个局限它没有else分支。如果你需要 if-else 逻辑就得用c:choosec:choose c:when test${user.role admin} p管理员面板/p /c:when c:when test${user.role editor} p编辑面板/p /c:when c:otherwise p普通用户面板/p /c:otherwise /c:choosec:choose相当于 switch 语句c:when是 casec:otherwise是 default。注意c:choose里只能直接放c:when和c:otherwise不能放其他标签或文本否则会报错。这个限制在写复杂页面时容易忽略比如你想在 choose 外面包一层 div得把 div 放在 choose 的外面而不是里面。还有一个细节c:when的test属性是按顺序判断的一旦某个条件为真后面的 when 就不会再执行了。所以如果你有多个条件可能同时为真要注意排列顺序把最具体的条件放在前面。3.3 c:set 与 c:remove作用域操作的轻量方案c:set用来在指定作用域里设置变量。基本用法c:set vartotal value${price * quantity} scoperequest /scope可以是page、request、session、application默认是page。这个标签在页面里做临时计算时很有用比如你从数据库查出一批数据想在页面里算个总和就可以用c:set存起来后面直接引用。但要注意c:set设置的是作用域属性不是局部变量。它的生命周期取决于 scope 的设置。如果你设成session那这个值会一直保留到会话结束可能造成内存泄漏。我见过有人在循环里用c:set往 session 里塞数据结果每次请求都往 session 里加东西最后内存爆掉。所以除非确实需要跨请求共享否则尽量用默认的page作用域。c:remove用来删除作用域里的变量c:remove vartotal scoperequest /这个标签用得不多但在某些场景下有用。比如你在页面开头设置了一个临时变量用完之后想清理掉避免影响后续的页面片段。不过大多数情况下page 作用域的变量在页面结束后会自动失效不需要手动清理。3.4 fmt 格式化库日期和数字的显示问题fmt:formatDate和fmt:formatNumber是格式化库里的两个核心标签。先说日期fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm:ss /pattern属性和 Java 的SimpleDateFormat是一样的规则。这里有个坑如果value是java.util.Date类型直接格式化没问题但如果是从数据库取出来的java.sql.Timestamp在某些容器里可能会有时区问题。我遇到过页面显示的日期比实际时间少了 8 小时的情况排查后发现是数据库连接串里没设时区导致 JDBC 驱动把时间当成了 UTC。解决办法是在连接串里加上serverTimezoneAsia/Shanghai或者在 Java 代码里统一转成Date再传给页面。数字格式化也很实用fmt:formatNumber value${product.price} pattern#,##0.00 /#,##0.00表示千分位分隔保留两位小数。如果你要做货币显示还可以用typecurrency它会根据 locale 自动加上货币符号。不过在实际项目里我一般建议在 Java 层就把数字格式化好页面只负责展示。原因是 JSTL 的格式化依赖 locale 设置如果服务器和用户的 locale 不一致显示结果可能不符合预期。4. 自定义标签与 functions 库的进阶玩法4.1 functions 库字符串处理的快捷方式functions库提供了一批静态方法用来在 EL 表达式里做字符串操作。使用前需要声明% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %常用的函数有fn:length()获取长度fn:contains()判断包含fn:substring()截取子串fn:trim()去空格fn:toUpperCase()转大写等。比如你想在页面上显示用户名的首字母${fn:toUpperCase(fn:substring(user.name, 0, 1))}这比在 Java 代码里写一堆substring和toUpperCase要直观。但要注意fn:length()对集合和字符串都有效对 null 会返回 0不会抛异常。这个特性在判断空集合时很有用比如c:if test${fn:length(userList) 0}。不过 functions 库的函数数量有限复杂的字符串处理还是建议放在 Java 层。页面里做太多逻辑维护起来照样头疼。我的原则是简单的展示逻辑用 JSTL复杂的业务逻辑回 Java。4.2 自定义标签什么时候值得自己写JSTL 虽然覆盖了大部分常见场景但总有它做不到的事情。比如你想在页面上渲染一个权限按钮逻辑是“如果用户有权限就显示按钮否则显示禁用状态”这个逻辑在多个页面里重复出现。这时候可以考虑写一个自定义标签。自定义标签的开发流程大致是先写一个继承SimpleTagSupport的 Java 类实现doTag()方法然后写一个 tld 文件描述标签的属性和行为最后在 JSP 里声明并使用。听起来步骤不少但实际写起来并不复杂。关键是要判断值不值得——如果这个逻辑只在两三个地方用直接复制 JSTL 代码更省事如果到处都在用封装成标签才能体现价值。我个人的经验是自定义标签适合封装“与页面结构强相关、但逻辑相对独立”的功能比如分页条、权限按钮、树形菜单。不适合封装业务逻辑因为业务逻辑应该放在 Service 层页面标签只负责展示。5. 那些年我踩过的 JSTL 坑5.1 标签不解析从现象到根因的排查链路页面直接输出c:forEach标签文本这是最典型的“标签不解析”问题。排查思路可以按这个顺序走第一步检查 taglib 声明。uri 是否写对prefix 是否和后面使用的一致。我见过有人声明了prefixc但页面里写的是jstl:forEach这种低级错误在复制粘贴时很容易出现。第二步检查 jar 包。确认jstl.jar和standard.jar如果版本需要都在WEB-INF/lib下。可以解压 jar 包看META-INF目录下有没有对应的 tld 文件。如果 tld 文件缺失标签库就无法被识别。第三步检查 web.xml 的版本。有些老项目的web.xml声明的是 Servlet 2.3 规范这个版本默认不解析 EL 和 JSTL。解决办法是把web.xml的 schema 声明升级到 2.4 或更高版本。这个坑特别隐蔽因为项目能正常启动只是标签不生效很多人会往 jar 包方向排查忽略了 web.xml 的版本问题。第四步检查容器版本。极老的容器可能不支持 JSTL 1.2需要降级到 1.1。这种情况现在比较少见但如果你的项目部署在很旧的环境里还是要留意。5.2 EL 表达式不生效isELIgnored 的陷阱EL 表达式${user.name}直接原样输出没有被解析成实际值。这个问题通常和isELIgnored属性有关。在 JSP 页面的 page 指令里可以设置isELIgnoredfalse来强制启用 EL% page isELIgnoredfalse %如果这个属性设成了true或者 web.xml 里的版本声明导致默认忽略 EL那${...}就会被当成普通文本。解决办法有两个一是在页面里显式设置isELIgnoredfalse二是升级 web.xml 的版本声明。我一般推荐后者因为统一在 web.xml 里配置不用每个页面都写一遍。还有一个可能是 EL 版本和容器不匹配。比如容器支持 EL 3.0但项目里引入了旧版本的 EL 实现 jar导致冲突。这种情况需要检查WEB-INF/lib下有没有多余的 EL 相关 jar有的话删掉让容器用自己的实现。5.3 循环里的性能问题别在页面里查数据库JSTL 的c:forEach用起来太方便以至于有人会在循环里调用数据库查询。比如遍历订单列表每个订单再去查用户信息c:forEach items${orderList} varorder c:set varuser value${userService.findById(order.userId)} / p${user.name}/p /c:forEach这种写法在数据量小的时候看不出问题一旦订单上百条页面加载就会变得极慢。原因是每次循环都发起一次数据库查询N 条数据就是 N1 次查询。正确的做法是在 Java 层一次性把关联数据查出来组装成完整的对象再传给页面。页面只负责展示不负责取数据。这个坑的本质是把业务逻辑写到了页面里。JSTL 再方便也只是展示层的工具不该承担数据访问的职责。我后来给自己定了个规矩页面里只出现${...}取值和c:...控制不出现任何 Service 或 DAO 的调用。6. 从 JSTL 到现代模板引擎的过渡思路现在新项目基本不用 JSP 了Thymeleaf、Freemarker、Velocity 这些模板引擎更受欢迎。但如果你手头有 JSP 项目要维护或者需要快速改一个老系统JSTL 还是得会。而且 JSTL 的很多设计思想在现代模板引擎里都能找到影子。比如 Thymeleaf 的th:each和 JSTL 的c:forEach思路一致都是把循环逻辑封装成标签属性。Thymeleaf 的th:if和c:if也是同样的道理。区别在于 Thymeleaf 的模板可以直接在浏览器里打开预览而 JSP 必须经过容器解析。这个差异决定了 Thymeleaf 更适合前后端协作而 JSP 更适合纯后端渲染。如果你已经熟悉 JSTL过渡到 Thymeleaf 的成本其实不高。核心概念是相通的作用域取值、条件判断、循环遍历、格式化输出。需要适应的主要是语法差异比如 Thymeleaf 用th:*属性而不是标签表达式用${...}但支持更复杂的 OGNL 语法。我个人的建议是老项目继续用 JSTL不要为了技术升级而升级稳定压倒一切新项目直接上 Thymeleaf 或前后端分离方案没必要再走 JSP 的老路。但无论用哪种技术理解“展示层只负责展示”这个原则比会用某个具体标签更重要。最后分享一个我在实际维护 JSP 项目时的小技巧把常用的 JSTL 代码片段整理成一个common.jsp用% include filecommon.jsp %引入。这样 taglib 声明只需要写一次页面里直接使用标签即可。这个做法能减少重复代码也避免了每个页面都写一遍 taglib 声明容易写错的问题。不过要注意include指令是在编译时合并的如果common.jsp里有变量定义可能会和页面里的变量冲突所以common.jsp里最好只放 taglib 声明和常量定义不要放逻辑代码。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →