JSP 标准标记库(JSP Standard Tag Library,JSTL)是一个实现 Web 应用程序中常见的通用功能的定制标记库集,这些功能包括迭代和条件判断、数据管理格式化、XML 操作以及数据库访问。在 developerWorks 上其新系列的第一篇文章中,软件工程师 Mark Kolb 向您展示了如何使用 JSTL 标记来避免在 JSP 页面中使用脚本编制元素。您还将了解如何通过从表示层删除源代码来简化软件维护。最后,您将了解 JSTL 经过简化的表达式语言,它允许在不必使用功能齐全的编程语言的情况下对 JSTL 操作指定动态属性值。
JavaServer Pages(JSP)是用于 J2EE 平台的标准表示层技术。JSP 技术提供了用于执行计算(这些计算用来动态地生成页面内容)的脚本编制元素和操作。脚本编制元素允许在 JSP 页面中包括程序源代码,在为响应用户请求而呈现页面时可以执行这些源代码。操作将计算操作封装到很象 HTML 或 XML 标记的标记中,JSP 页面的模板文本通常包含这些标记。JSP 规范只将几种操作定义成了标准,但从 JSP 1.1 开始,开发人员已经能够以定制标记库的方式创建其自己的操作了。
JSP 标准标记库(JSTL)是 JSP 1.2 定制标记库集,这些标记库实现大量服务器端 Java 应用程序常用的基本功能。通过为典型表示层任务(如数据格式化和迭代或条件内容)提供标准实现,JSTL 使 JSP 作者可以专注于特定于应用程序的开发需求,而不是为这些通用操作“另起炉灶”。
当然,您可以使用 JSP 脚本编制元素(scriptlet、表达式和声明)来实现此类任务。例如,可以使用三个 scriptlet 实现条件内容,清单 1 中着重显示了这三个 scriptlet。但是,因为脚本编制元素依赖于在页面中嵌入程序源代码(通常是 Java 代码),所以对于使用这些脚本编制元素的 JSP 页面,其软件维护任务的复杂度大大增加了。例如,清单 1 中的 scriptlet 示例严格地依赖于花括号的正确匹配。如果不经意间引入了一个语法错误,则条件内容中的嵌套其它 scriptlet 可能会造成严重破坏,并且在 JSP 容器编译该页面时,要使所产生的错误信息有意义可能会很困难。
清单 1. 通过 scriptlet 实现条件内容
<% if (user.getRole() == "member")) { %>
Welcome, member!
Welcome, guest!
类别 | 标识符 | 描述 | ||||||||
JSP | pageContext | PageContext 实例对应于当前页面的处理 | ||||||||
作用域 | pageScope | 与页面作用域属性的名称和值相关联的 Map 类 | ||||||||
requestScope | 与请求作用域属性的名称和值相关联的 Map 类 | |||||||||
sessionScope | 与会话作用域属性的名称和值相关联的 Map 类 | |||||||||
applicationScope | 与应用程序作用域属性的名称和值相关联的 Map 类 | |||||||||
请求参数 | param | 按名称存储请求参数的主要值的 Map 类 | ||||||||
paramValues | 将请求参数的所有值作为 String 数组存储的 Map 类 | |||||||||
请求头 | header | 按名称存储请求头主要值的 Map 类 | ||||||||
headerValues | 将请求头的所有值作为 String 数组存储的 Map 类 | |||||||||
Cookie | cookie | 按名称存储请求附带的 cookie 的 Map 类 | ||||||||
初始化参数 | initParam | 按名称存储 Web 应用程序上下文初始化参数的 Map 类 |
Your preferred time zone is ${timezone} Welcome, member! Welcome, guest!
尽管 JSP 和 EL 隐式对象中只有一个公共对象( pageContext ),但通过 EL 也可以访问其它 JSP 隐式对象。原因是 pageContext 拥有访问所有其它八个 JSP 隐式对象的特性。实际上,这是将它包括在 EL 隐式对象中的主要理由。
其余所有 EL 隐式对象都是映射,可以用来查找对应于名称的对象。前四个映射表示先前讨论的各种属性作用域。可以用它们来查找特定作用域中的标识符,而不用依赖于 EL 在缺省情况下使用的顺序查找过程。
接下来的四个映射用来获取请求参数和请求头的值。因为 HTTP 协议允许请求参数和请求头具有多个值,所以它们各有一对映射。每对中的第一个映射返回请求参数或头的主要值,通常是恰巧在实际请求中首先指定的那个值。每对中第二个映射允许检索参数或头的所有值。这些映射中的键是参数或头的名称,但这些值是 String 对象的数组,其中的每个元素都是单一参数值或头值。
cookie 隐式对象提供了对由请求设置的 cookie 名称的访问。这个对象将所有与请求相关联的 cookie 名称映射到表示那些 cookie 特性的 Cookie 对象。
最后一个 EL 隐式对象 initParam 是一个映射,它储存与 Web 应用程序相关联的所有上下文的初始化参数的名称和值。初始化参数是通过 web.xml 部署描述符文件指定的,该文件位于应用程序的 WEB-INF 目录中。
存取器
因为 EL 标识符是作为隐式对象或限制了作用域的变量(通过属性来实现)解析的,因此有必要将它们转换成 Java 对象。EL 可以自动包装和解包其相应的 Java 类中的基本类型(例如,可以在后台将 int 强制转换成 Integer 类,反之亦可),但大多数的标识符将成为指向完整的 Java 对象的指针。
结果是,对这些对象的特性或(在对象是数组和集合的情况下)对其元素的访问通常是令人满意的。就为了实现这种用途,EL 提供了两种不同的存取器(点运算符( . )和方括号运算符( [] )),也支持通过 EL 操作特性和元素。
点运算符通常用于访问对象的特性。例如,在表达式 ${user.firstName} 中,使用点运算符来访问 user 标识符所引用对象的名为 firstName 的特性。EL 使用 Java bean 约定访问对象特性,因此必须定义这个特性的 getter 方法(通常是名为 getFirstName() 的方法),以便表达式正确求值。当被访问的特性本身是对象时,可以递归地应用点运算符。例如,如果我们虚构的 user 对象有一个实现为 Java 对象的 address 特性,那么也可以用点运算符来访问这个对象的特性。例如,表达式 ${user.address.city} 将会返回这个地址对象嵌套的 city 特性。
方括号运算符用来检索数组和集合的元素。在数组和有序集合(也即,实现了 java.util.List 接口的集合)的情况下,把要检索的元素的下标放在方括号中。例如,表达式 ${urls[3]} 返回 urls 标识符所引用的数组或集合的第四个元素(和 Java 语言以及 JavaScript 中一样,EL 中的下标是从零开始的)。
对于实现 java.util.Map 接口的集合,方括号运算符使用关联的键查找存储在映射中的值。在方括号中指定键,并将相应的值作为表达式的值返回。例如,表达式 ${commands["dir"]} 返回与 commands 标识符所引用的 Map 中的 "dir" 键相关联的值。
对于上述两种情况,都可允许表达式出现在方括号中。对嵌套表达式求值的结果将被作为下标或键,用来检索集合或数组的适当元素。和点运算符一样,方括号运算符也可以递归应用。这使得 EL 能够从多维数组、嵌套集合或两者的任意组合中检索元素。此外,点运算符和方括号运算符还可以互操作。例如,如果数组的元素本身是对象,则可以使用方括号运算符来检索该数组的元素,并结合点运算符来检索该元素的一个特性(例如 ${urls[3].protocol} )。
假定 EL 充当指定动态属性值的简化语言,EL 存取器有一个有趣的功能(与 Java 语言的存取器不同),那就是它们在应用于 null 时不抛出异常。如果应用 EL 存取器的对象(例如, ${foo.bar} 和 ${foo["bar"]} 中的 foo 标识符)是 null ,那么应用存取器的结果也是 null 。事实证明,在大多数情况下,这是一个相当有用的行为,不久您就会了解这一点。
最后,点运算符和方括号运算符可能实现某种程度的互换。例如,也可以使用 ${user["firstName"]} 来检索 user 对象的 firstName 特性,正如可以用 ${commands.dir} 获取与 commands 映射中的 "dir" 键相关联的值一样。
运算符
EL 还可以通过使用标识符和存取器,遍历包含应用程序数据(通过限制了作用域的变量公开)或关于环境的信息(通过 EL 隐式对象)的对象层次结构。但是,只是访问这些数据,通常不足以实现许多 JSP 应用程序所需的表示逻辑。
最终,EL 还包括了几个用来操作和比较 EL 表达式所访问数据的运算符。表 2 中汇总了这些运算符。
表 2. EL 运算符
类别 运算符
算术运算符 + 、 - 、 * 、 / (或 div )和 % (或 mod )
关系运算符 == (或 eq )、 != (或 ne )、 < (或 lt )、 > (或 gt )、 <= (或 le )和 >= (或 ge )
逻辑运算符 && (或 and )、 || (或 or )和 ! (或 not )
验证运算符 empty
算术运算符支持数值的加法、减法、乘法和除法。还提供了一个求余运算符。注:除法和求余运算符都有替代的、非符号的名称(为的是与 XPath 保持一致)。清单 5 中显示了一个演示算术运算符用法的示例表达式。对几个 EL 表达式应用算术运算符的结果是将该算术运算符应用于这些表达式返回的数值所得的结果。
清单 5. 利用算术运算符的 EL 表达式
${item.price * (1 + taxRate[user.address.zipcode])}
关系运算符允许比较数字或文本数据。比较的结果作为布尔值返回。逻辑运算符允许合并布尔值,返回新的布尔值。因此,可以将 EL 逻辑运算符应用于嵌套的关系或逻辑运算符的结果,如清单 6 所示。
清单 6. 利用关系和逻辑运算符的 EL 表达式
${(x >= min) && (x <= max)}
最后一种 EL 运算符是 empty ,它对于验证数据特别有用。 empty 运算符采用单个表达式作为其变量(也即, ${empty input} ),并返回一个布尔值,该布尔值表示对表达式求值的结果是不是“空”值。求值结果为 null 的表达式被认为是空,即无元素的集合或数组。如果参数是对长度为零的 String 求值所得的结果,则 empty 运算符也将返回 true 。
表 3 显示了 EL 运算符的优先级。正如清单 5 和 6 所示,可以用圆括号对表达式分组,高于普通的优先级规则。
表 3. EL 运算符优先级(自顶到底,从左到右)
[] , .
()
unary - 、 not 、 ! 、 empty
* 、 / 、 div 、 % 、 mod
+ 、binary -
() < 、 > 、 <= 、 >= 、 lt 、 gt 、 le 、 ge
== 、 != 、 eq 、 ne
&& 、 and
|| 、 or
文字
在 EL 表达式中,数字、字符串、布尔值和 null 都可以被指定为文字值。字符串可以用单引号或双引号定界。布尔值被指定为 true 和 false 。
Taglib 伪指令
正如我们先前讨论的,JSTL 1.0 包括四个定制标记库。为了演示 JSTL 标记和表达式语言的交互,我们将研究几个来自 JSTL core 库的标记。和使用任何 JSP 定制标记库一样,必须在您想要使用这个库标记的任何页面中包括 taglib 伪指令。清单 7 显示了用于这个特定库的伪指令。
清单 7. 用于 JSTL core 库 EL 版本的 taglib 伪指令
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %>
实际上,对应于 JSTL core 库的 taglib 伪指令有两种,因为在 JSTL 1.0 中,EL 是可选的。所有四个 JSTL 1.0 定制标记库都有使用 JSP 表达式(而不是 EL)指定动态属性值的备用版本。因为这些备用库依赖于 JSP 的更传统的请求时属性值,所以它们被称为 RT库,而那些使用表达式语言的则被称为 EL 库。开发人员用不同的 taglib 伪指令来区分每个库的这两个版本。清单 8 显示了使用 core 库的 RT 版本的伪指令。但是,由于现在我们讨论的重点是 EL,所以首先需要这些伪指令。
清单 8. 用于 JSTL core 库 RT 版本的 taglib 伪指令
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c_rt" %>
变量标记
我们首先要考虑的 JSTL 定制标记是
清单 9.
scope 属性是可选的,其缺省值是 page 。
清单 10 中显示了
清单 10.
您还可以将限制了作用域的变量的值指定为
清单 11. 通过主体内容指定
JSTL core 库包含第二个用于管理限制了作用域的变量的标记 ―
清单 12.
输出
尽管
清单 13.
escapeXml 属性也是可选的。它控制当用
例如,假定有一个名为 user 的会话作用域变量,它是一个类的实例,该类为用户定义了两个特性: username 和 company 。每当用户访问站点时,这个对象被自动分配给会话,但直到用户实际登录后,才会设置这两个特性。假定是这种方案,请考虑清单 14 中的 JSP 片段。在用户登录之后,这个片段将显示单词“Hello”,其后是他/她的用户名和一个惊叹号。但是,在用户登录之前,由这个片段生成的内容则是短语 “Hello Guest!”。在这种情况下,因为 username 特性还有待初始化,所以
清单 14. 带缺省内容的
Hello
接下来,考虑清单 15,它使用了
清单 15. 禁用转义的
用缺省值设置变量
除了简化动态数据的显示之外,当通过
清单 16 中说明了这种方法。外部
清单 16. 合并
但是,请考虑以下情况,用户是第一次尝试使用这段代码的 Web 应用程序。结果是,请求中没有提供名为 tzPref 的 cookie。这意味着使用隐式对象的查找将返回 null ,在这种情况下整个表达式将返回 null 。因为对
EL 和 JSP 2.0
目前,表达式语言仅可用于指定 JSTL 定制标记中的动态属性值。但 JSTL 1.0 表达式语言的一个扩展已经被提出,会把它包括到 JSP 2.0 中去,眼下正在进行最后评审。这个扩展将允许开发人员通过自己的定制标记来使用 EL。页面作者将可以在目前允许使用 JSP 表达式的任何地方使用 EL 表达式,譬如将动态值插入模板文本中:
这个 JSP 2.0 功能(就象 JSTL 本身一样)将支持页面作者进一步减少对 JSP 编制脚本元素的依赖,从而改进 JSP 应用程序的可维护性。
结束语
EL(与四个 JSTL 定制标记库提供的操作结合起来)允许页面作者不使用脚本元素即可实现表示层逻辑。例如,对比本文开头 清单 1 中的 JSP 代码和清单 17 中显示的通过 JSTL 实现的同样功能。(JSTL core 库中其余的标记,包括
清单 17. 合并
通过提供大多数 Web 应用程序常用功能的标准实现,JSTL 有助于加速开发周期。与 EL 结合起来,JSTL 可以不需要对表示层程序编写代码,这极大地简化了 JSP 应用程序的维护。