本节摘要:EL(表达式语言)用
${...}一套语法完成视图层的全部取值:点操作符与方括号操作符沿对象链逐层取属性,四级作用域按窄到宽就近查找,param、header、cookie 等隐式对象直达请求元数据,取不到值时宽容地输出空白而非报错。本章大扫除的第一步就是掌握这套规则——它决定了页面里每一个取值表达式的正确写法与排错思路。
第 4 章的推模式立下契约:控制器把数据放进请求作用域,页面负责取出来画。EL 就是取这一步的标准写法:
<%-- 控制器放:request.setAttribute("order", order) --%> <p>订单号:${order.orderNo}</p> <p>收货人:${order.customer.name}</p>
${order.orderNo} 的求值过程分三步。第一步,在作用域里找名为 order 的对象——没写前缀,就按 page、request、session、application 的顺序逐层搜;第二步,对找到的对象解析属性链,orderNo 映射到取值方法,customer.name 则连取两层;第三步,把结果转成字符串写进输出流。方括号操作符是点操作符的通用形态:${order["order-no"]} 能处理带连字符的名字,${bookList[0]} 能按索引取列表元素,${param["user-id"]} 能取请求参数——凡名字不合标识符规范或需要动态拼接的场合,都用方括号。
整条解析管线画出来是这样的:

注意措辞:EL 的隐式对象与第 2 章那九个不是同一套。第 2 章的是 Java 对象,拿来调方法;EL 的隐式对象本质是几张现成的查找表,拿来取值。最常用的一批见表:
| 隐式对象 | 取什么 | 写法示例 |
|---|---|---|
| param | 请求参数单值 | ${param.keyword} |
| paramValues | 请求参数多值 | ${paramValues.tag[0]} |
| header | 请求头 | ${header["Accept-Language"]} |
| cookie | Cookie 值 | ${cookie.theme.value} |
| pageScope 等四个 | 指定作用域的属性 | ${requestScope.order} |
| initParam | 应用级初始化参数 | ${initParam.uploadLimit} |
这套表的用法里最有价值的是显式作用域前缀。 ${order} 依赖就近查找,读代码的人不知道数据来自哪一层;${requestScope.order} 则一句话说清出处。青梧书肆的规范因此定了一条:控制器递来的数据一律带 requestScope 前缀写——这是把 4.1 节的"托盘契约"在页面上落成可见的痕迹,也让重名遮挡问题从根源上消失。
运算符方面,EL 提供算术、比较、逻辑三族,外加一个视图层最常用的 empty:${empty keyword} 对 null、空字符串、空集合一律返回真。配合三目写法,页面里最常见的那类"有值才显示"就能写干净:
<c:if test="${not empty order.coupon}"> <p>已优惠:${order.coupon.amount} 元</p> </c:if>
背景:订单完成页上线后接到反馈:部分顾客的页面"下单成功"后面订单号是空白。没有报错、没有异常日志,前端的样式却因此错位。
操作:按管线三步排查。第一步怀疑空值:对照控制器代码,正常路径确实会放 order 对象——但异常恢复路径(支付超时后重新进入)只放了订单号字符串,没放 order 对象,EL 取 ${order.orderNo} 整链为空,安静输出空白。第二步核对作用域:数据确实在 request 作用域,没有放错层。第三步查重名:会话里存在一个旧版 order 属性(历史代码遗留),不带前缀的写法在某些入口会被它遮住。
结果:控制器补齐异常路径的数据装配;页面表达式统一改成 ${requestScope.order.orderNo},并在服务层保证对象必非空后,用 ${not empty ...} 做展示级判断。
解读:这个案例把宽容失败的两面性演全了:好处是页面没有因一个缺失对象整体 500;坏处是缺陷以空白形态带病上线,靠用户反馈才暴露。对策不是回到抛异常的老路,而是让空值在服务层被拦截、在视图层被显式判断——空值可以容忍,但必须是被看见的容忍。
变式:如果整页 EL 全部原样显示成 ${...} 字符串,那就不是取值问题,而是 1.2 节讲过的旧规范版本或忽略开关把 EL 当成了纯文本——查工程声明的规范版本与部署描述里的开关即可。两种"不取值"病因不同,处理方向完全不同。
💡 关键直觉:EL 是一门"只许提问、不许动手"的语言——只能取值、判断,不能改状态、调任意方法。这个限制当年是为了防止业务逻辑回流向页面;今天读老代码时,它同样是把尺子:页面上用 EL 拼业务规则,一定是把逻辑放错了层。
EL 自己不调方法,但它留了一个官方外挂口:函数扩展。把工具类里的公开静态方法在标签库描述文件里登记一个名字,页面就能以"前缀加冒号加函数名"的形态调用它——第 5.2 节用到的字符串取长、截断函数,走的正是这套机制。对维护期的价值在两个场景:一是格式化需求(工单号分段显示、金额单位换算)标准函数覆盖不了时,铸一个纯函数给全站复用,比在视图里堆脚本体面得多;二是把散落的日期转换小逻辑收拢进一个工具类,注册后全站统一调用。边界也要划清:函数必须是无副作用的纯函数,入参出参来去干净——它依然活在"只许提问不许动手"的宪法之下,想在函数里偷偷改状态,路是没有的。
把本节的排查经验固化成走查单,下次遇到空白输出直接照单走查。第一格:表达式带作用域前缀吗?带,去核对数据是否真放在那个作用域;不带,先补上前缀排除重名遮挡。第二格:属性链的每一段都非空吗?从根对象开始逐段核对——控制器装配代码里找到放值的语句,确认对象图每一层都有值。第三格:名字完全一致吗?托盘清单上的属性名与页面表达式逐字符对,大小写也不放过。第四格:拼错的是表达式还是数据?临时把表达式换成常量输出,区分"取值语法问题"还是"数据装配问题"。第五格:整个站点都失灵还是个别页面?个别页面走上面四格,全站失灵直接查 1.2 节的规范版本开关。走查单的价值在于顺序:从最常见的原因查起,多数问题在第二格就现形,不会在错误的方向上空耗。
取值干净了,页面里还剩判断与循环两类脚本。下一站请出 JSTL 标准标签,把大扫除进行到底。