JSP开发痛点:用EL表达式与JSTL标签库告别Scriptlet脚本

发布时间:2026/10/11 16:54:41
JSP开发痛点:用EL表达式与JSTL标签库告别Scriptlet脚本
如果你写过 JSP大概率经历过这种场面页面顶部堆着一排 % page import... %HTML 中间穿插着 % for (...) { %循环结束还得记着补一个 % } %。改一个字段要在几十行标签和 Java 语法之间来回找括号。EL 表达式和 JSTL 标签库就是专门把这种痛苦从 JSP 里剥离出去的一套组合方案。今天这篇想聊的就是它俩怎么配合EL 负责取数据、算表达式JSTL 负责控制流程、搞定循环两者一起把 JSP 从混乱脚本拉回干净模板。适合两类人看一类是刚学到 Java Web 阶段的开发者正在被 scriptlet 折磨另一类是写过后端但持续被 JSP 页面维护困扰的开发者想找一套更干净的写法。1. JSP 里最让人头大的事1.1 scriptlet 把页面变成了“意大利面”JSP 本身的设计初衷是好的让页面能够动态显示数据。但很多项目写着写着就偏离了开始在页面里写大量 Java 逻辑。最常见的就是 scriptlet也就是用 % % 包裹的代码块。早年很多后台管理系统的列表页是这个画风% ListUser userList userService.findUsers(); for (User u : userList) { if (u.getStatus() 1) { % li classonline% u.getNickname() % 在线/li % } } %这种写法初看还能跑一旦需求变多问题就全冒出来了。最大的问题是排查困难。JSP 本质上是把 Java 代码和 HTML 模板混在一起交给服务器编译很多报错信息只给到一个编译行号但实际原因是前面某个 % % 没闭合或者某个 Java 判断块被 HTML 标签切成了两半。不少接手这种页面的人排查起来都很崩溃服务端日志只给出一行编译错误浏览器直接一片空白根本不知道问题出在第几层括号里。第二个问题是维护成本。scriptlet 写多了页面结构肉眼根本看不出来。你想调整 HTML 结构可能不小心动到 Java 循环你想加一个字段得先数清楚当前在第几层括号里。更别说如果页面里有多个循环嵌套负责改页面的前端同事面对这堆代码基本无从下手。时间一长这种 JSP 就变成了“能不动就不动”的禁区谁改谁踩坑。第三个问题是业务逻辑和展示逻辑完全耦合。正常的分层开发中查询、判断、计算这些事情应该提前在 Servlet 或 Service 层准备好JSP 只负责把结果展示出来。但 scriptlet 允许你在页面上直接写业务代码一旦开了这个口子后面就会越写越多最终整个页面变成一锅意大利面。1.2 EL JSTL 到底干了什么解决上面这些问题靠的就是 EL 表达式和 JSTL 标签库这对组合。两者分工非常明确EL 负责“取数据、算表达式”核心语法是 ${}JSTL 负责“流程控制、循环、格式化、URL 处理”核心是一组类似 HTML 标签的 c 标签。打个比方EL 是查数据的取件员你告诉它“把 request 里 userList 拿出来”它就去拿JSTL 是流水线上的操作员你告诉它“循环遍历执行、满足条件才输出”它就去执行。这个分工的意义在于JSP 页面里不再出现任何一个 Java 关键字。没有 import没有 for没有 if没有变量类型转换取而代之的是 ${user.name}、c:forEach、c:if 这种既像模板又像标签的东西。对比一下就很直观。同样的一个列表展示scriptlet 版本里 Java 代码占了三分之一标签结构支离破碎换成 EL JSTL 后页面看起来就是一个完整的 HTML 模板眼尖的话还能看出哪些数据是动态的哪些区域是条件控制的。当然EL 和 JSTL 也不是万能的。它俩解决的是“JSP 页面内代码混乱”的问题而不是“后端架构混乱”的问题。业务逻辑该在 Service 层还是在 Service 层数据组装该在 Servlet 还是在 Servlet只是最后一步展示和简单遍历判断可以放心交给页面模板。理解到这一层你才算是真正用对了这对组合。2. EL 表达式让 JSP 学会“自己取值”2.1 ${} 语法是怎么工作的EL 表达式的基本语法就是 ${}花括号里写你要获取的数据或者要做的运算。最简单的用法是 ${user.name}它表示在四个作用域里依次找名为 user 的对象找到之后访问它的 name 属性。这里要特别说清楚“访问属性”的底层逻辑。${user.name} 并不是直接读 user 对象里的 name 字段而是调用 user 对象的 getName() 方法。这是 JavaBean 规范决定的所以你的实体类必须有对应的 getter 方法否则 EL 表达式取到的就是空字符串而且不会报错。这一点很多新手都没注意到排查半天以为是作用域问题结果是实体类少了 getter。除了点号访问EL 还支持方括号访问两者是等价的${user.name} ${user[name]} ${user[name]}方括号写法在两种场景下更顺手一是 Map 的 key 不是合法标识符时比如 ${map[user-name]}二是 key 本身是变量时${map[keyVar]} 这种动态取值。数组和 List 的下标访问也是用方括号比如 ${userList[0].name}。容器查找顺序也是一个绕不开的知识点。${user.username} 默认会按 pageScope → requestScope → sessionScope → applicationScope 的顺序找名称为 user 的属性一旦找到就停止找不到就返回空。这个顺序背后是作用域的生命周期page 最小只管当前页面request 只管一次请求session 覆盖一个会话application 是全局共享。范围从小到大查找自然也是从小到大查不到就往外扩大。因为这个机制我见过不少“EL 取不到值”的案例十有八九是 Servlet 里把数据存到了 session而页面里没有带 sessionScope 前缀结果 request 里正好有同名的 key导致取到的是另一个值。所以页面里建议写明确的作用域前缀特别是业务数据比如 ${sessionScope.loginUser.nickname}这样一眼就能看出数据来源也能避免同名覆盖。2.2 内置对象与运算符一张表看明白EL 内置对象是它取数的“入口清单”日常开发最常用的我整理成一张表EL 对象作用典型场景pageScope当前页面范围的属性页面内部传递临时值requestScope当前请求范围的属性Servlet 转发数据到 JSPsessionScope会话范围的属性登录用户、购物车applicationScope应用范围的属性全站配置参数param获取单个请求参数表单提交值回显paramValues获取同名多值参数复选框多选header获取单个请求头客户端信息headerValues获取同名多个请求头特殊排查场景cookie读取浏览器 Cookie记住登录状态initParam读取 web.xml 初始化参数全局配置项pageContext页面上下文对象访问 request、session 等常用的几个我给你配了实际写法${param.username} ${paramValues.hobby[0]} ${cookie.rememberMe.value} ${initParam.pageSize}param 这组特别常用。很多搜索页表单提交后用户输入的关键词要回显到输入框里直接用 ${param.keyword} 就省去 request.getParameter(keyword) 这一长串而且 JSP 里塞 Java 代码的动机又少了一个。EL 的运算符也是日常要用的算术、关系、逻辑、条件、空值判断都有。比较有特点的是它提供了一套英文写法eq 等于、ne 不等于、lt 小于、gt 大于、le 小于等于、ge 大于等于以及 and、or、not写出来更接近自然语言。empty 是我最常用的运算符它判断一个值是否为空但“空”的定义很宽null、空字符串、长度为零的数组以及没有元素的 List、Map都会被判定为空。${empty userList} ${empty keyword}这在列表页太好用了数据没查到、搜索关键词没传、某对象还没初始化一个 empty 全部兜住不用再写一堆 null 判断和空集合判断。类似地三元表达式也很实用比如状态列显示${user.status 1 ? 在线 : 离线}2.3 EL 的限制恰恰是 JSTL 存在的理由EL 虽然写起来爽但它本质上只是表达式语言能力边界很清楚它能读取数据、能计算、能判断空值但不能定义复杂变量、不能写循环、不能做多分支流程控制。你可以用三元表达式做二选一但“多个条件逐个判断”这种逻辑EL 是搞不定的。这不是 EL 的缺陷而是设计上的刻意取舍。表达式语言本来就只该负责“读”和“算”如果让 EL 也能写完整逻辑那 JSP 又会退回脚本化开发的老路。所以 EL 的克制恰恰是 JSTL 存在的理由流程控制交给标签。另外说一下 EL 3.0 之后的一个特性新版本支持直接调用 Java 方法比如 ${user.getName()}在 Tomcat 8.5 以上的环境里可以正常执行。但在实际页面里我不太建议这么写一方面方法调用会引入副作用另一方面页面看起来又像 Java 代码了不如在 Servlet 层把数据准备好页面只做展示。3. JSTL 标签库流程控制交给标签3.1 环境准备与版本选择别一上来就踩坑JSTL 全称是 JSP Standard Tag Library是官方标准标签库核心是 c 开头的标签组比如 c:out、c:set、c:if、c:forEach。引入方式不难但版本问题很容易坑人而且大多是在部署阶段才爆发。你需要先搞清楚项目跑在哪个 Servlet 容器版本上。Tomcat 9 及以下对应的是 javax 命名空间用的是 JSTL 1.2taglib uri 是老地址 java.sun.comTomcat 10 开始整个 Servlet 规范把 javax 换成了 jakartaJSTL 也升级到 2.xtaglib uri 变成了 jakarta.tags.core。我把两种环境的配置整理在一起方便对照。Tomcat 9 及以下Maven 依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyJSP 页面开头写% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %Tomcat 10 及以上Maven 依赖要引入两个dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version2.0.0/version /dependency dependency groupIdorg.glassfish.web/groupId artifactIdjakarta.servlet.jsp.jstl/artifactId version2.0.0/version /dependencyJSP 页面开头则写% taglib prefixc urijakarta.tags.core % % taglib prefixfn urijakarta.tags.functions %为什么会出现两套因为 Java EE 后来改成了 Jakarta EE整个技术体系都从 javax 迁移到了 jakartaJSTL 作为标准库自然也要跟着迁移。对开发者来说最直接的影响就是老项目升到 Tomcat 10 的时候JSTL 相关依赖必须同步升级否则运行期直接抛类找不到的异常。关于环境我再多提醒一点IDEA 里新建 JSP 后如果 c 标签一直没有代码提示优先检查 Maven 依赖是否已加载成功以及部署到 Tomcat 的产物里是否包含了 jstl 相关 jar。很多时候不是代码问题而是包没打进去。3.2 核心标签逐个拆解out / set / if / chooseJSTL 核心标签里日常占用率最高的是这么几个c:out、c:set、c:if、c:choose。我按使用频率逐个说。c:out 负责输出功能类似 % %但它有一个重要特性默认对输出的字符串做 HTML 转义。同样输出一个用户昵称如果昵称里含有