EL表达式与JSTL标签库:从JSP Scriptlet到优雅视图层的实战改造指南

📅 发布时间:2026/10/16 18:41:23
EL表达式与JSTL标签库:从JSP Scriptlet到优雅视图层的实战改造指南
学习 Java Web 到了第 36 天说实话我有点拖延因为这一篇要写的东西太基础了。基础到很多人都默认会用但真到接手项目的时候又会看到满屏的% %把 JSP 页面搅得一团糟。EL 表达式和 JSTL 标签库就是专门用来解决这个问题的组合拳一个负责把后端数据安全优雅地取出来一个负责把页面里的循环、判断、格式化全部换成标签写法。这篇笔记不是入门教科书而是我把一套老旧后台页面拆干净之后的总结适合已经能写 ServletJSP 但还没彻底摆脱 Scriptlet 的人也适合想搞清楚 EL 和 JSTL 到底怎么配合的开发者。整个技术栈虽然不算新但只要还在维护传统 Java Web 项目这套技能就一天都不会过时。1. 为什么说 EL 和 JSTL 是 JSP 从能跑到好用的分水岭1.1 我见过最糟糕的 JSP 页面长什么样先说我手头正在维护的一个模拟项目 X某后台管理系统。页面数量不算多但有一个员工列表页打开第一眼差点把我劝退。顶部塞了一排% page importjava.util.List %中间到处是%和%循环用 Java 的for判断用 Java 的if连表格序号都是从循环变量里手动加 1 算出来的。大概长这样% page importjava.util.List % % page importcom.example.pojo.Employee % % ListEmployee empList (ListEmployee) request.getAttribute(empList); if (empList ! null !empList.isEmpty()) { for (int i 0; i empList.size(); i) { Employee emp empList.get(i); % tr td% i 1 %/td td% emp.getName() %/td td% emp.getDept() %/td td% emp.getHireDate() %/td /tr % } } else { % trtd colspan4暂无数据/td/tr % } %这段代码能跑但问题非常大。页面里出现了request.getAttribute、强制类型转换、空判断、循环控制变量、甚至还有 Java 的 import。也就是说一个本该只负责展示的 JSP把 Servlet 该做的数据获取、类型转换全都干了一遍。后面只要业务稍微变一点比如加一个在职/离职状态判断、按部门过滤、薪资格式化页面里的 Java 代码就会越来越多直到没人敢动。1.2 EL 和 JSTL 到底各自解决了哪一类问题很多人分不清 EL 和 JSTL 的边界其实划分特别清楚。ELExpression Language解决的是数据怎么读的问题。以前你在页面里取值要写% request.getAttribute(empList) %然后再强转再遍历。有了 EL 之后直接写${emp.name}就能取到对象的 getName() 结果写${requestScope.empList}就能拿到 request 里的集合。最关键的是EL 对 null 值非常友好不会动不动就抛空指针取不到就输出空字符串。JSTLJSP Standard Tag Library解决的是页面逻辑怎么写的问题。以前你要在页面里写% if %、% for %现在换成c:if、c:forEach这么一套标准标签。JSTL 还附带格式化功能日期、数字、文本处理都有对应的标签和函数。把这两样组合起来效果就是Java 逻辑全部留在 ServletJSP 页面只剩 HTML 加少量标签。这才是 JSP 该有的姿态。我前面提到的那杯满屏 Scriptlet 页面用 ELJSTL 重写之后代码量能砍掉三分之一而且任何人接手都能在五分钟内看明白这个页面的结构。这个对比比任何理论都直观。2. EL 表达式看懂这套取值规则页面就干净了一半2.1 从 ${user.name} 说起属性访问和域查找顺序先看一个最简单的例子。Servlet 里写了request.setAttribute(user, user);转发到 JSP 之后页面上想输出这个用户的姓名最自然的写法是${user.name}这句代码背后藏着一个很多初学者不知道的规则EL 并不是直接从 request 里找 user 的它会按照 pageScope、requestScope、sessionScope、applicationScope 四个范围依次查找。也就是说如果 pageContext、request、session、application 四个范围里有同名的属性EL 会优先取最靠近当前页面的那个取不到才往更大的范围找。这个机制平时不会给你找麻烦但一旦你在 session 里存了一个跟 request 属性同名的变量就可能出现我明明传了 A页面显示的却是 B的诡异问题。所以我现在写 EL 有一个习惯能带范围前缀就带范围前缀。${requestScope.user.name}直接锁死从 request 里取值语义明确也不会被 session 里的同名变量干扰。范围前缀有四个内置对象对应的范围pageScope当前 JSP 页面的 pageContext 属性requestScope当前请求的 request 属性sessionScope当前会话的 session 属性applicationScope全局的 application 属性除了这四个取值作用域EL 还提供了一些常用的隐含对象比如param拿请求参数header拿请求头cookie拿 CookieinitParam拿 web.xml 里配置的初始化参数还有pageContext可以拿到 request、response、session 等对象。比如项目根路径就是这么写的${pageContext.request.contextPath}这行代码在模板页里出现频率非常高。2.2 运算符、空值处理和 EL 函数EL 里的运算符不多但足够日常用。算术类的加减乘除、取模都有比较运算符可以写、!、、也可以写eq、ne、gt、lt这种单词形式。逻辑运算用、||、!或者and、or、not。还有一个特别常用的empty运算符专门判断 null、空字符串、空集合。${empty empList ? 暂无数据 : 有员工数据}这句用了一个三目运算如果empList是 null 或者空集合就显示暂无数据否则显示有员工数据。注意empty对数字 0 无效如果你判断的是count 0那得用${count 0}不能用${empty count}这是很多新手会踩的坑。关于字符串比较EL 里用就可以因为 EL 内部会对字符串调用 equals 方法不必写equals。但是要注意字符串字面量要用单引号或双引号包起来比如${emp.status ACTIVE}。EL 还有一个很实用的函数库叫 Functions配合 JSTL 一起用。比较常用的有fn:length获取集合或字符串长度fn:contains判断字符串是否包含子串fn:substring截取字符串。注意拿集合大小别用${list.size()}这个写法在多数 EL 版本里不会正常工作正确姿势是${fn:length(empList)}使用前要在页面顶部导入函数库% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %2.3 EL 失效与 isELIgnored最常见的表达式原样输出问题有时候你高高兴兴写完${user.name}刷新页面浏览器上显示的就是一行字${user.name}EL 根本没被解析。这通常是两个原因造成的。第一个原因是 page 指令里把 isELIgnored 设成了 true% page isELIgnoredtrue %这种情况下 JSP 容器不会解析页面里的 EL 表达式原样输出。解决方法是删掉这行或者改成isELIgnoredfalse。第二个原因是 web.xml 的版本问题。如果项目的 web.xml 声明的是 Servlet 2.3 或更早版本哪怕你什么都没设置EL 默认也是不启用的。这种项目现在不多但存量系统里偶尔还能碰上。解决方式是升级 web.xml 的头版本到 2.5 或以上或者单独在 JSP 页面加isELIgnoredfalse。我遇到过最隐蔽的情况是某个页面自己没设置但它 include 的公共头部文件里设置了isELIgnoredtrue导致整个页面 EL 全部失效。排查时强烈建议先看一眼页面上有没有 include 公共文件再看公共文件里写了什么。3. JSTL 核心标签库实操c:if、c:forEach、c:choose 的使用逻辑3.1 标签库导入和 Maven 依赖先跑通这一套JSTL 不是 JSP 自带的需要额外引入依赖。用 Maven 管理项目的话最省事的是直接引javax.servlet:jstl:1.2这一个包它把 API 和实现都合在一起了。dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency如果是非 Maven 的普通 Web 项目需要把jstl.jar和standard.jar都放到WEB-INF/lib目录下。注意Tomcat 自带了 EL 解析能力但不带 JSTL 实现所以 JSTL 的 jar 必须自己提供这是新手经常困惑的点。然后在 JSP 页面顶部导入标签库最常用的两个% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %prefix是你在页面里使用的前缀uri是这个标签库的唯一标识。这个 uri 看起来像网址但不需要真的能访问只是用来定位 TLD 描述文件的字符串。习惯上 core 用c格式化用fmt千万别自创一套不然别人接手会懵。3.2 c:forEach 遍历集合varStatus、索引和 Map 遍历c:forEach是使用频率最高的 JSTL 标签没有之一。基本用法c:forEach items${empList} varemp tr td${emp.name}/td td${emp.dept}/td /tr /c:forEachitems指定要遍历的集合var指定每个元素的变量名。如果items是 null循环体不会执行也不会报错。但真正让c:forEach强大的是varStatus属性。它给我们提供了一组循环状态信息index表示当前索引从 0 开始count表示当前循环次数从 1 开始first和last表示是否是第一个/最后一个元素。我经常用varStatus做两件事表格序号和奇偶行样式。c:forEach items${empList} varemp varStatusst tr class${st.index % 2 0 ? even : odd} td${st.count}/td td${emp.name}/td /tr /c:forEach注意st.count从 1 开始正好适合用来做表格的序号列不用自己再i 1了。奇偶行的判断则是用st.index % 2因为 index 从 0 开始0 对应偶数行。遍历 Map 的时候var拿到的是一个 Map.Entry通过entry.key和entry.value分别取键和值c:forEach items${deptMap} varentry option value${entry.key}${entry.value}/option /c:forEach如果只是想循环固定次数不遍历集合也可以只用begin和endc:forEach begin1 end10 vari span${i}/span /c:forEach这个在生成下拉选项、分页按钮时特别好用比在页面里写 Java for 循环干净得多。3.3 c:if 与 c:choose流程控制的取舍c:if是最简单的判断标签c:if test${empty empList} p暂无员工数据/p /c:if很多初学者用完之后问c:if 有没有 else答案是没有。如果需要 if-else 或者多分支就用c:choose组合c:choose c:when test${emp.status ACTIVE} span classtag-on在职/span /c:when c:when test${emp.status LEAVE} span classtag-leave离职/span /c:when c:otherwise span classtag-unknown未知状态/span /c:otherwise /c:choosec:choose相当于 Java 里的 switchc:when相当于 casec:otherwise相当于 default。这里有个细节c:choose标签体里最好只放c:when和c:otherwise不要在它们之间直接夹带 HTML 文本或普通标签否则可能出现预期外的解析问题。c:if还有一个不太起眼但很实用的写法可以把判断结果保存到变量里后面反复使用c:if test${not empty empList} varhasEmp scoperequest /c:if c:if test${hasEmp} !-- 后面还能用 -- /c:if这种用法在页面多次引用同一个判断条件时能省不少事也会让可读性更好。3.4 fmt 格式化标签日期和数字显示fmt 标签库是另一个刚需。后端返回的 Date 类型直接输出会是一长串默认格式完全没法看用fmt:formatDate格式化一下fmt:formatDate value${emp.hireDate} patternyyyy-MM-dd /数字格式化用fmt:formatNumber比如薪资显示成千分位fmt:formatNumber value${emp.salary} pattern#,##0.00 /pattern的规则和 Java 的SimpleDateFormat、DecimalFormat一致写过 Java 的人基本都能看懂。fmt 标签库还包含国际化相关的fmt:setLocale、fmt:message不过在普通后台系统里用得不多暂时了解就行。4. 实战改造把一台满是 Scriptlet 的员工列表页拆干净4.1 改造前Scriptlet 版本页面为了直观对比我拿模拟项目 X 里面一个真实的页面来说明。背景是一个员工列表页Servlet 查出了ListEmployee放到 request 里转发给 JSP。每个员工有姓名、部门、入职日期、薪资、状态。改造前的 JSP 核心部分是这样% page importjava.util.List % % page importcom.example.pojo.Employee % % ListEmployee empList (ListEmployee) request.getAttribute(empList); % table tr th序号/thth姓名/thth部门/th th入职日期/thth薪资/thth状态/th /tr % if (empList ! null !empList.isEmpty()) { for (int i 0; i empList.size(); i) { Employee emp empList.get(i); % tr td% i 1 %/td td% emp.getName() %/td td% emp.getDept() %/td td% emp.getHireDate() %/td td% emp.getSalary() %/td td% ACTIVE.equals(emp.getStatus()) ? 在职 : 离职 %/td /tr % } } else { % trtd colspan6暂无数据/td/tr % } % /table这段代码的问题一眼就能看出来。页面上同时出现了 Java 的条件判断、循环、字符串比较、三元表达式。如果哪天要在姓名后面加一个链接又要写a href...% emp.getId() %...整个页面会越来越像一个混杂了 HTML 的 Java 类。4.2 改造后EL JSTL 版本页面同样一个页面完全用 EL 和 JSTL 重写之后是这样% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table tr th序号/thth姓名/thth部门/th th入职日期/thth薪资/thth状态/th /tr c:choose c:when test${not empty empList} c:forEach items${empList} varemp varStatusst tr class${st.index % 2 0 ? even : odd} td${st.count}/td td${emp.name}/td td${emp.dept}/td tdfmt:formatDate value${emp.hireDate} patternyyyy-MM-dd //td tdfmt:formatNumber value${emp.salary} pattern#,##0.00 //td td c:choose c:when test${emp.status ACTIVE}在职/c:when c:otherwise离职/c:otherwise /c:choose /td /tr /c:forEach /c:when c:otherwise trtd colspan6暂无数据/td/tr /c:otherwise /c:choose /table改造完之后页面里没有任何一行 Java 代码也没有一个%。整个文件读起来就是一个带了一点动态内容的 HTML 模板。哪个卖相好一眼便知。4.3 前后对比表与改造经验维度Scriptlet 版本EL JSTL 版本页面可读性标签和 Java 代码混在一起难读页面结构清晰像 HTML 模板维护成本加一列要同时改 HTML 和 Java只改 HTML 和标签类型安全需要手动强转出错风险高EL 自动处理 null 和类型转换职责划分JSP 做了 Servlet 的事JSP 只负责展示调试难度编译错误隐藏在页面里定位慢标签错误和 Java 错误分离这次改造给我最大的教训是JSP 里的 Java 代码越少越好但不是说页面里绝对不能出现复杂逻辑。我的原则是页面只做展示分支不做业务计算。比如状态是 ACTIVE 显示在职这种属于展示分支可以留在页面里但根据某些条件算出综合评级这种属于业务计算必须在 Servlet 或者 Service 层提前算好放到 model 里页面只负责取结果。另外分享一个细节日期和薪资这种格式化也不要全指望页面上的 fmt 标签。如果这个值要在多个页面复用我更倾向于在 Java 层直接转成字符串再往页面上放。页面上的 fmt 标签适合临时、一次性格式化经常被多个页面使用的数据提前在 Java 层处理会更统一。5. 我在学习过程中踩过的 JSTL 配置坑5.1 标签库 URI 报错与 jar 包冲突最常见的问题没有之一就是页面一启动直接报The absolute uri: [http://java.sun.com/jsp/jstl/core] cannot be resolved这个报错的意思是 JSP 容器去解析 taglib 指令里的 uri但是找不到对应的 TLD 描述文件。排查思路是这样先问自己项目里到底有没有引入 JSTL 的 jar。Maven 项目确认jstl依赖存在并且依赖的 scope 不是provided。有些同学为了蹭 Tomcat 自带的包故意把 scope 写成 provided但 Tomcat 恰恰不自带 JSTL最终还是启动报错。普通 Web 项目还要确认jstl.jar和standard.jar是否真的被打进了WEB-INF/lib。有些人把 jar 放在 IDEA 的 Library 里运行环境用的是 Tomcat 的部署目录两者不一致也会报错。看构建产物的 lib 目录最靠谱。另一个坑是同时引入了jstl和standard的多个版本比如项目原本用standard-1.1后来又加了一个jstl-1.2两个版本里都有相同 uri 的 TLD 描述文件容器可能加载其中一个但标签类在另一个 jar 里于是出现运行时标签找不到。这种问题排查起来特别费时间我的建议是直接统一成javax.servlet:jstl:1.2不要混用。5.2 IDEA 不提示标签代码的问题在 IDEA 里写 JSP有时候c:forEach完全不提示一片红色波浪线。这不是代码写错了而是 IDEA 没有把 JSTL 的 jar 加入到当前模块的依赖里。Maven 项目在 pom.xml 加了jstl依赖之后IDEA 一般会自动同步如果没有就手动Reload All Maven Projects。非 Maven 项目需要在Project Structure里的Modules标签页找到当前模块的Dependencies把jstl.jar和standard.jar加进去。加完之后红色波浪线和代码提示通常会立刻恢复。还有一个很隐蔽的坑IDEA 默认把 JSP 文件的Language level设置得比较低导致varStatus这种高级一点的属性不提示。这个在Settings - Editor - File Types里检查 JSP 的文件类型是否被识别成普通的 HTML 就行一般项目结构正常的话不会出现。5.3 一个容易被忽略的 URL 拼接坑ELJSTL 改造页面之后我顺带把链接也换成了c:url主要是为了自动处理项目上下文路径c:url value/emp/detail vardetailUrl c:param nameid value${emp.id} / /c:url a href${detailUrl}详情/a这个写法比手动${pageContext.request.contextPath}/emp/detail?id${emp.id}安全得多。c:url会自动在路径前加上项目上下文路径还会对参数做 URL 编码。如果以后需要做 session 追踪它连;jsessionid都会自动处理。传统项目里手动拼 URL 的页面特别多最容易出现的问题有两个。一是漏了上下文路径导致请求直接 404二是参数里有中文或特殊字符没有 URL 编码传到后端乱码。用c:url统一处理之后这两种问题基本就绝迹了。还有一个和页面编码相关的坑也值得提一下。JSP 页面出现中文乱码时先检查页面开头的contentType和pageEncoding是否都设置为 UTF-8% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %同时Tomcat 的连接器最好加上URIEncodingUTF-8这样 GET 请求的参数也不会乱码。这个配置不在 JSP 文件里而是在 Tomcat 的配置文件里排查顺序一般是页面编码 - 请求编码 - 响应编码按这个顺序查基本都能解决。最后分享一个我自己坚持下来的习惯。接手一个老旧 Web 项目时我第一件事不是看代码结构而是统计所有 JSP 页面里出现了多少个%。凡是 Scriptlet 数量超过十个的页面都会优先把 Java 逻辑往 Servlet 层挪然后用 EL 和 JSTL 重写视图层。这个习惯坚持下来之后页面质量的提升非常明显。Day36 这两板斧——EL 负责取数JSTL 负责腾挪——看着简单但真的把 JSP 从一个能跑的页面变成了好用的页面。学习这东西不怕基础怕的是基础没打牢就直接上框架。等哪天遇见一个只用 ServletJSP 的老项目你会发现今天这篇学的所有细节全都能派上用场。