<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->(1)JSTL 入门: 表示就是一切
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 容器编译该页面时,要使所产生的错误信息有意义可能会很困难。
|
修正此类问题通常需要相当丰富的编程经验。尽管通常会由十分精通页面布局和图形设计的设计人员来开发和维护 JSP,但是同一页面中的脚本编制元素出现问题时,需要程序员的介入。这种状况将单个文件中代码的责任分担给多人,因而使得开发、调试和增强此类 JSP 页面成为很麻烦的任务。通过将常用功能包装到定制标记库的标准集合中,JSTL 使 JSP 作者可以减少对编制脚本元素的需求,甚至可以不需要它们,并避免了相关的维护成本。
|
|
JSTL 1.0 发布于 2002 年 6 月,由四个定制标记库(
core
、
format
、
xml
和
sql
)和一对通用标记库验证器(
ScriptFreeTLV
和
PermittedTaglibsTLV
)组成。
core
标记库提供了定制操作,通过限制了作用域的变量管理数据,以及执行页面内容的迭代和条件操作。它还提供了用来生成和操作 URL 的标记。顾名思义,
format
标记库定义了用来格式化数据(尤其是数字和日期)的操作。它还支持使用本地化资源束进行 JSP 页面的国际化。
xml
库包含一些标记,这些标记用来操作通过 XML 表示的数据,而
sql
库定义了用来查询关系数据库的操作。
两个 JSTL 标记库验证器允许开发人员在其 JSP 应用程序中强制使用编码标准。可以配置
ScriptFreeTLV
验证器以在 JSP 页面中禁用各种类型的 JSP 脚本元素 ― scriptlet、表达式和声明。类似地,
PermittedTaglibsTLV
验证器可以用来限制可能由应用程序的 JSP 页面访问的定制标记库集(包括 JSTL 标记库)。
尽管 JSTL 最终将会成为 J2EE 平台的必需组件,但目前只有少数应用程序服务器包括它。JSTL 1.0 的参考实现可作为 Apache 软件基金会(Apache Software Foundation)的 Jakarta Taglibs 项目(请参阅 参考资料 )的一部分而获得。可以将该参考实现中的定制标记库合并到任何支持 JSP 1.2 和 Servlet 2.3 规范的服务器,以添加对 JSTL 的支持。
|
|
在 JSP 1.2 中,可以使用静态字符串或表达式(如果允许的话)指定 JSP 操作的属性。例如,在清单 2 中,对
<jsp:setProperty>
操作的
name
和
property
属性指定了静态值,而用表达式指定了其
value
属性。这个操作的效果是将请求参数的当前值赋予命名的 bean 特性。以这种形式使用的表达式被称为
请求时属性值(request-time attribute value)
,这是构建到 JSP 规范中的用于动态指定属性值的唯一机制。
|
因为请求时属性值是用表达式指定的,所以它们往往有和其它脚本元素一样的软件维护问题。因此,JSTL 定制标记支持另一种用于指定动态属性值的机制。可以用简化的 表达式语言 (EL)而不使用完整的 JSP 表达式来指定 JSTL 操作的属性值。EL 提供了一些标识符、存取器和运算符,用来检索和操作驻留在 JSP 容器中的数据。EL 在某种程度上以 EcmaScript(请参阅 参考资料 )和 XML 路径语言(XML Path Language,XPath)为基础,因此页面设计人员和程序员都应该熟悉它的语法。EL 擅长寻找对象及其特性,然后对它们执行简单操作;它不是编程语言,甚至不是脚本编制语言。但是,与 JSTL 标记一起使用时,它就能使用简单而又方便的符号来表示复杂的行为。EL 表达式的格式是这样的:用美元符号($)定界,内容包括在花括号({})中,如清单 3 所示。
|
此外,您可以将多个表达式与静态文本组合在一起以通过字符串并置来构造动态属性值,如清单 4 所示。单独的表达式由标识符、存取器、文字和运算符组成。标识符用来引用存储在数据中心中的数据对象。EL 有 11 个保留标识符,对应于 11 个 EL 隐式对象。假定所有其它标识符都引用 限制了作用域的变量 。存取器用来检索对象的特性或集合的元素。文字表示固定的值 ― 数字、字符、字符串、布尔型或空值。运算符允许对数据和文字进行组合以及比较。
清单 4. 组合静态文本和多个 EL 表达式以指定动态属性值
|
|
|
JSP API 通过
<jsp:useBean>
操作允许从 JSP 容器内的四个不同作用域中存储和检索数据。JSTL 通过提供用于指定和除去这些作用域中的对象的附加操作来扩展这一能力。此外,EL 提供将这些对象作为限制了作用域的变量进行检索的内置支持。特别地,任何出现在 EL 表达式中但不对应于任何 EL 隐式对象的标识符,都被自动假定为引用存储在四个 JSP 作用域的其中某个中的对象,这四个作用域是:
- 页面作用域
- 请求作用域
- 会话作用域
- 应用程序作用域
您可能还记得,只有在为特定请求处理页面期间才能检索存储在该页面作用域中的对象。如果对象是存储在请求作用域中的,可以在处理所有参与处理某请求的页面期间检索这些对象(譬如在对某个请求的处理中遇到了一个或多个
<jsp:include>
或
<jsp:forward>
操作)。如果对象是存储在会话作用域中的,则在与 Web 应用程序的交互式会话期间,可以由用户访问的任何页面检索它(即,直到与该用户交互相关联的
HttpSession
对象无效为止)。可以由任何用户从任何页面访问存储在应用程序作用域中的对象,直到卸载 Web 应用程序本身为止(通常是由于关闭 JSP 容器所致)。
通过将字符串映射为期望作用域中的对象来将对象存储到该作用域。然后,就可以通过提供相同字符串来从该作用域检索该对象。在作用域的映射中查找字符串,并返回被映射的对象。在 Servlet API 中,将此类对象称为相应作用域的 属性 。但是,在 EL 的上下文中,也将与属性相关联的字符串看作变量的名称,该变量通过属性映射的方式获得特定的值。
在 EL 中,与隐式对象无关联的标识符被认为是存储在四个 JSP 作用域中的名称对象。首先对页面作用域检查是否存在这样的标识符,其次对请求作用域、然后对会话作用域、最后对应用程序作用域依次进行这样的检查,然后测试该标识符的名称是否与存储在该作用域中的某个对象的名称匹配。第一个这样的匹配作为 EL 标识符的值被返回。通过这种方法,可以将 EL 标识符看作引用限制了作用域的变量。
从更技术的方面来说,没有映射到隐式对象的标识符是用
PageContext
实例的
findAttribute()
方法求值的,该实例表示对页面的处理,在该页面上,当前正在处理用于请求的表达式。标识符的名称作为参数传递给这个方法,然后该方法依次在四个作用域中搜索具有相同名称的属性。并将所找到的第一个匹配项作为
findAttribute()
方法的值返回。如果未在这四个作用域中找到这样的属性,则返回
null
。
最终,限制了作用域的变量是四个 JSP 作用域的属性,这些属性具有可以用作 EL 标识符的名称。只要对限制了作用域的变量赋予由字母数字组成的名称,就可以通过 JSP 中提供的用于设置属性的任何机制来创建它们。这包括内置的
<jsp:useBean>
操作,以及由 Servlet API 中的几个类定义的
setAttribute()
方法。此外,四个 JSTL 库中定义的许多定制标记本身就能够设置作为限制了作用域的变量使用的属性值。
|
|
表 1 中列出了 11 个 EL 隐式对象的标识符。不要将这些对象与 JSP 隐式对象(一共只有九个)混淆,其中只有一个对象是它们所共有的。
类别 | 标识符 | 描述 |
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
类
|
尽管 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 中汇总了这些运算符。
类别 | 运算符 |
算术运算符 |
+
、
-
、
*
、
/
(或
div
)和
%
(或
mod
)
|
关系运算符 |
==
(或
eq
)、
!=
(或
ne
)、
<
(或
lt
)、
>
(或
gt
)、
<=
(或
le
)和
>=
(或
ge
)
|
逻辑运算符 |
&&
(或
and
)、
||
(或
or
)和
!
(或
not
)
|
验证运算符 |
empty
|
算术运算符支持数值的加法、减法、乘法和除法。还提供了一个求余运算符。注:除法和求余运算符都有替代的、非符号的名称(为的是与 XPath 保持一致)。清单 5 中显示了一个演示算术运算符用法的示例表达式。对几个 EL 表达式应用算术运算符的结果是将该算术运算符应用于这些表达式返回的数值所得的结果。
|
关系运算符允许比较数字或文本数据。比较的结果作为布尔值返回。逻辑运算符允许合并布尔值,返回新的布尔值。因此,可以将 EL 逻辑运算符应用于嵌套的关系或逻辑运算符的结果,如清单 6 所示。
|
最后一种 EL 运算符是
empty
,它对于验证数据特别有用。
empty
运算符采用单个表达式作为其变量(也即,
${empty input}
),并返回一个布尔值,该布尔值表示对表达式求值的结果是不是“空”值。求值结果为
null
的表达式被认为是空,即无元素的集合或数组。如果参数是对长度为零的
String
求值所得的结果,则
empty
运算符也将返回
true
。
表 3 显示了 EL 运算符的优先级。正如清单 5 和 6 所示,可以用圆括号对表达式分组,高于普通的优先级规则。
[]
,
.
|
()
|
unary
-
、
not
、
!
、
empty
|
*
、
/
、
div
、
%
、
mod
|
+
、binary
-
|
()
<</code>
、
>
、
<=
、
>=
、
lt
、
gt
、
le
、
ge
|
==
、
!=
、
eq
、
ne
|
&&
、
and
|
||
、
or
|
|
|
在 EL 表达式中,数字、字符串、布尔值和
null
都可以被指定为文字值。字符串可以用单引号或双引号定界。布尔值被指定为
true
和
false
。
|
|
正如我们先前讨论的,JSTL 1.0 包括四个定制标记库。为了演示 JSTL 标记和表达式语言的交互,我们将研究几个来自 JSTL
core
库的标记。和使用任何 JSP 定制标记库一样,必须在您想要使用这个库标记的任何页面中包括
taglib
伪指令。清单 7 显示了用于这个特定库的伪指令。
清单 7. 用于 JSTL core 库 EL 版本的 taglib 伪指令
|
实际上,对应于 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 伪指令
|
|
|
我们首先要考虑的 JSTL 定制标记是
<c:set>
操作。正如已经说明的,限制了作用域的变量在 JSTL 中起关键作用,
<c:set>
操作提供基于标记的机制来创建和设置限制了作用域的变量。清单 9 中显示了该操作的语法,其中
var
属性指定了限制了作用域的变量的名称,
scope
属性表明了该变量驻留在哪个作用域中,
value
属性指定了分配给该变量的值。如果指定变量已经存在,则简单地将所指明的值赋给它。如果不存在,则创建新的限制了作用域的变量,并用该值初始化这个变量。
|
scope
属性是可选的,其缺省值是
page
。
清单 10 中显示了
<c:set>
的两个示例。在第一个示例中,将会话作用域变量设置成
String
值。在第二个示例中,用表达式来设置数值:将页面作用域内名为
square
的变量赋值为名为
x
的请求参数的值的平方。
|
您还可以将限制了作用域的变量的值指定为
<c:set>
操作的主体内容,而不是使用属性。使用这种方法,您可以重新编写清单 10 中的第一个示例,如清单 11 所示。此外,正如我们马上可以看到的,
<c:set>
标记的主体内容本身也可以使用定制标记。
<c:set>
主体内生成的所有内容都将作为一个
String
值赋给指定变量。
|
JSTL core 库包含第二个用于管理限制了作用域的变量的标记 ―
<c:remove>
。顾名思义,
<c:remove>
操作是用来删除限制了作用域的变量的,它获取两个属性。
var
属性指定待删除变量的名称,
scope
属性是可选的,它表示待删除变量来自哪个作用域,缺省为
page
,如清单 12 所示。
|
|
|
尽管
<c:set>
操作允许将表达式结果赋给限制了作用域的变量,但开发人员通常会希望只显示表达式的值,而不存储它。JSTL
<c:out>
定制标记承担这一任务,其语法如清单 13 所示。该标记对由其
value
属性指定的表达式进行求值,然后打印结果。如果指定了可选属性
default
,那么,在对
value
属性的表达式求值所得结果为
null
或空
String
的情况下,
<c:out>
将打印其值。
|
escapeXml
属性也是可选的。它控制当用
<c:out>
标记输出诸如“<”、“>”和“&”之类的字符(在 HTML 和 XML 中具有特殊意义)时是否应该进行转义。如果将
escapeXml
设置为 true,则会自动将这些字符转换成相应的 XML 实体(此处提到的字符分别转换成
<
、
>
和
&
)。
例如,假定有一个名为
user
的会话作用域变量,它是一个类的实例,该类为用户定义了两个特性:
username
和
company
。每当用户访问站点时,这个对象被自动分配给会话,但直到用户实际登录后,才会设置这两个特性。假定是这种方案,请考虑清单 14 中的 JSP 片段。在用户登录之后,这个片段将显示单词“Hello”,其后是他/她的用户名和一个惊叹号。但是,在用户登录之前,由这个片段生成的内容则是短语“Hello Guest!”。在这种情况下,因为
username
特性还有待初始化,所以
<c:out>
标记将转而打印出
default
属性的值(即字符串“Guest”)。
|
接下来,考虑清单 15,它使用了
<c:out>
标记的
escapeXml
属性。如果在这种情况下已经将
company
特性设置成 Java
String
值
"Flynn & Sons"
,那么,实际上该操作生成的内容将是
Flynn & Sons
。如果这个操作是生成 HTML 或 XML 内容的 JSP 页面的一部分,那么,这个字符串中间的“&”符号最终可能被解释为 HTML 或 XML 控制字符,从而妨碍了对该内容的显示或解析。但是,如果将
escapeXml
属性值设置成
true
,则所生成的内容将是
Flynn & Sons
。浏览器或解析器不会因在解释时遇到这种内容而出问题。假定 HTML 和 XML 是 JSP 应用程序中最常见的内容类型,所以
escapeXml
属性的缺省值是
true
就不足为奇了。
|
|
|
除了简化动态数据的显示之外,当通过
<c:set>
设置变量值时,
<c:out>
指定缺省值的能力也很有用。正如
清单 11
所示,用来赋给限制了作用域的变量的值可以指定为
<c:set>
标记的主体内容,也可以通过其值属性来指定。通过将
<c:out>
操作嵌套在
<c:set>
标记的主体内容中,变量赋值就可以利用其缺省值能力。
清单 16 中说明了这种方法。外部
<c:set>
标记的行为非常简单:它根据其主体内容设置会话作用域
timezone
变量的值。但是,在这种情况下,主体内容是通过
<c:out>
操作生成的。这个嵌套操作的值属性是表达式
${cookie['tzPref'].value}
,它尝试通过
cookie
隐式对象返回名为
tzPref
的 cookie 值。(
cookie
隐式对象将 cookie 名称映射到相应的
Cookie
实例,这意味着必须通过对象的
value
特性使用点运算符来检索储存在 cookie 中的实际数据。)
清单 16. 合并 <c:set> 和 <c:out> 以提供缺省变量值
|
但是,请考虑以下情况,用户是第一次尝试使用这段代码的 Web 应用程序。结果是,请求中没有提供名为
tzPref
的 cookie。这意味着使用隐式对象的查找将返回
null
,在这种情况下整个表达式将返回
null
。因为对
<c:out>
标记的
value
属性求值的结果是
null
,所以
<c:out>
标记会转而输出对其
default
属性求值的结果。在这里是字符串
CST
。因此,实际的结果是将
timezone
限制了作用域的变量设置成用户的
tzPref
cookie 中存储的时区,或者,如果没有,则使用缺省时区
CST
。
|
|
|
EL(与四个 JSTL 定制标记库提供的操作结合起来)允许页面作者不使用脚本元素即可实现表示层逻辑。例如,对比本文开头
清单 1
中的 JSP 代码和清单 17 中显示的通过 JSTL 实现的同样功能。(JSTL
core
库中其余的标记,包括
<c:choose>
及其子标记,将在本系列的下一篇文章中讨论。)尽管显然执行了条件逻辑,但是 JSTL 版本中没有 Java 语言源代码,并且标记之间的关系(尤其是关于嵌套需求)对于任何精通 HTML 语法的人都应该是熟悉的。
清单 17. 合并 <c:set> 和 <c:out> 以提供缺省变量值
|
通过提供大多数 Web 应用程序常用功能的标准实现,JSTL 有助于加速开发周期。与 EL 结合起来,JSTL 可以不需要对表示层程序编写代码,这极大地简化了 JSP 应用程序的维护。
(2)JSTL 入门: 探讨 core
顾名思义,JSP 标准标记库(JSP Standard Tag Library,JSTL)<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->core
库为一些基本功能(如,管理限定了作用域的变量和与 URL 交互等)和基本操作(如,迭代和条件化)提供了定制标记。这些标记不仅可以由页面设计人员直接利用,而且还为与其它 JSTL 库相结合从而提供更复杂的表示逻辑奠定了基础。Mark Kolb 在本文中继续对 JSTL 和core
库进行探讨,研究用标记来协助流控制和 URL 管理。
通过阅读本系列的 第一篇文章 ,您对 JSTL 有了初步的了解。我们描述了使用其 表达式语言 (EL)来访问数据和操作数据。正如您所了解的那样,EL 用来为 JSTL 定制标记的属性赋予动态值,因此,它所起的作用与 JSP 表达式一样,为内置操作及其它定制标记库指定请求时的属性值。
为了演示 EL 的用法,我们介绍了
core
库中的三个标记:
<c:set>
、
<c:remove>
和
<c:out>
。
<c:set>
和
<c:remove>
用于管理限定了作用域的变量;而
<c:out>
用于显示数据,尤其是显示用 EL 计算出的值。在此基础上,接下来本文把注意力集中在
core
库的其余标记上,这些标记可以大致归为两大类别:流控制和 URL 管理。
为了演示 JSTL 标记,我们将使用来自一个工作应用程序的示例,本系列中余下的文章都将使用此应用程序。由于基于 Java 的 Weblog 日渐流行及为人们所熟悉,因此我们将出于此目的使用一个简单的基于 Java 的 Weblog;参阅 参考资料 以下载该应用程序的 JSP 页面和源代码。Weblog(也称为 blog)是一种基于 Web 的简短注释的日志,这些注释是有关 Weblog 的作者所感兴趣的主题,通常带有与 Web 上其它地方的相关文章及讨论的链接。图 1 中显示了该应用程序正在运行时的抓屏。
虽然完整的实现需要二十四个 Java 类,但在表示层中却只涉及 Weblog 应用程序中的两个类,
Entry
和
UserBean
。这样,对于理解 JSTL 示例而言,只有这两个类比较重要。图 2 显示了
Entry
和
UserBean
的类图。
Entry
类表示 Weblog 中一个标有日期的项。其
id
属性用于在数据库中存储及检索该项,而
title
和
text
属性则表示该项的实际内容。
created
和
lastModified
属性引用了 Java 语言中
Date
类的两个实例,分别用来表示最初创建该项的时间和最后编辑该项的时间。
author
属性引用了标识该项的创建者的
UserBean
实例。
UserBean
类存储了有关应用程序的已认证用户的信息,如用户名、全名和电子邮件地址。该类还包含一个用于与相关数据库进行交互的
id
属性。其最后一个属性
roles
引用一列
String
值,这列值标识与相应用户相关的、特定于应用程序的角色。对于 Weblog 应用程序,相关的角色是“User”(所有应用程序用户常用的缺省角色)和“Author”(该角色指定可以创建和编辑 Weblog 项的用户)。
|
|
由于可以用 EL 替代 JSP 表达式来指定动态属性值,因此页面创作人员无需使用脚本编制元素。因为脚本编制元素可能是引起 JSP 页面中维护问题的主要原因,所以 JSTL 的主要优点就在于提供了这样简单(且标准)的替代方法。
EL 从 JSP 容器检索数据,遍历对象层次结构,然后对结果执行简单的操作。不过,除了访问和操作数据之外,JSP 脚本编制元素的另一个常见用法是流控制。尤其是,页面创作人员常借助 scriptlet 来实现迭代或条件内容。然而,因为这样的操作超出了 EL 的能力,所以
core
库提供了几个定制操作来管理流控制,其形式有
迭代
、
条件化
和
异常处理
。
在 Web 应用程序环境中,迭代主要用于访存和显示数据集,通常是以列表或表中的一系列行的形式显示。实现迭代内容的主要 JSTL 操作是
<c:forEach>
定制标记。该标记支持两种不同样式的迭代:整数范围上的迭代(类似 Java 语言的
for
语句)和集合上的迭代(类似 Java 语言的
Iterator
和
Enumeration
类)。
进行整数范围迭代用到了清单 1 中所示的
<c:forEach>
标记的语法。
begin
和
end
属性要么是静态整数值,要么是可以得出整数值的表达式。它们分别指定迭代索引的初始值以及迭代索引的终止值。当使用
<c:forEach>
在整数范围内进行迭代时,这两个属性是必需的,而其它所有属性都是可选的。
清单 1. 通过 <c:forEach> 操作进行数字迭代的语法
|
当出现
step
时,它也必须是整数值。它指定每次迭代后索引的增量。这样,迭代索引从
begin
属性的值开始,以
step
属性的值为增量进行递增,在迭代索引超过
end
属性的值时停止迭代。注:如果省略了
step
属性,那么步长缺省为 1。
如果指定了
var
属性,那么将会创建一个带有指定名称的并限定了作用域的变量,并将每次迭代的当前索引值赋给该变量。这一限定了作用域的变量具有嵌套式可视性 ― 只可以在
<c:forEach>
标记体内对其进行访问。(我们很快将讨论可选属性
varStatus
的用法。)清单 2 显示了对一组固定整数值进行迭代的
<c:forEach>
操作示例。
清单 2. 使用 <c:forEach> 标记来生成表列数据,这些数据对应于某一范围内的数值
|
如图 3 中所示,上面的示例代码生成了一张表,显示前五个偶数及其平方。这是通过将
begin
和
step
属性值指定为 2,而将
end
属性值指定为 10 实现的。此外,用
var
属性创建用于存储索引值的限定了作用域的变量,
<c:forEach>
标记体内引用了该变量。尤其是,使用了一对
<c:out>
操作来显示索引及其平方,其中索引的平方是使用一个简单的表达式计算得来的。
在对集合的成员进行迭代时,用到了
<c:forEach>
标记的另一个属性:
items
属性,清单 3 中显示了该属性。当使用这种形式的
<c:forEach>
标记时,
items
属性是唯一必需的属性。
items
属性的值应该是一个集合,对该集合的成员进行迭代,通常使用 EL 表达式指定值。如果变量名称是通过
<c:forEach>
标记的
item
属性指定的,那么对于每次迭代该已命名变量都将被绑定到集合后续元素上。
清单 3. 通过 <c:forEach> 操作对集合进行迭代的语法
|
<c:forEach>
标记支持 Java 平台所提供的所有标准集合类型。此外,您可以使用该操作来迭代数组(包括基本类型数组)中的元素。表 1 列出了
items
属性所支持的所有值。正如表的最后一行所指出的那样,JSTL 定义了它自己的接口
javax.servlet.jsp.jstl.sql.Result
,用来迭代 SQL 查询的结果。(我们将在本系列后面的文章中详细讨论这一功能。)
表 1. <c:forEach> 标记的 items 属性所支持的集合
items
的值
|
所产生的
item
值
|
java.util.Collection
|
调用
iterator()
所获得的元素
|
java.util.Map
|
java.util.Map.Entry
的实例
|
java.util.Iterator
|
迭代器元素 |
java.util.Enumeration
|
枚举元素 |
Object
实例数组
|
数组元素 |
基本类型值数组 | 经过包装的数组元素 |
用逗号定界的
String
|
子字符串 |
javax.servlet.jsp.jstl.sql.Result
|
SQL 查询所获得的行 |
可以使用
begin
、
end
和
step
属性来限定在迭代中包含集合中哪些元素。和通过
<c:forEach>
进行数字迭代的情形一样,在迭代集合中的元素时同样要维护一个迭代索引。
<c:forEach>
标记实际上只处理那些与索引值相对应的元素,这些索引值与指定的
begin
、
end
和
step
值相匹配。
清单 4 显示了用来迭代集合的
<c:forEach>
标记。对于该 JSP 代码段,
entryList
这一限定了作用域的变量被设置成了
Entry
对象列表(确切的说,
ArrayList
)。
<c:forEach>
标记依次处理列表中的每个元素,将其赋给一个限定了作用域的变量
blogEntry
,然后生成两个表行 ― 一个用于 Weblog 项的
title
,另一个则用于该项
text
。这些特性是通过一对带有相应 EL 表达式的
<c:out>
操作从
blogEntry
变量检索得到的。注:由于 Weblog 项的标题和文本都可能包含 HTML 标记,因此这两个
<c:out>
标记的
escapeXml
属性都被设置成了 false。图 4 显示了结果。
清单 4. 使用 <c:forEach> 标记显示表示给定日期的 Weblog 项
|
图 4. 清单 4 的输出
不论是对整数还是对集合进行迭代,
<c:forEach>
剩余的属性
varStatus
所起的作用相同。和
var
属性一样,
varStatus
用于创建限定了作用域的变量。不过,由
varStatus
属性命名的变量并不存储当前索引值或当前元素,而是赋予
javax.servlet.jsp.jstl.core.LoopTagStatus
类的实例。该类定义了一组特性,它们描述了迭代的当前状态,表 2 中列出了这些特性。
特性 | Getter | 描述 |
current |
getCurrent()
|
当前这次迭代的(集合中的)项 |
index |
getIndex()
|
当前这次迭代从 0 开始的迭代索引 |
count |
getCount()
|
当前这次迭代从 1 开始的迭代计数 |
first |
isFirst()
|
用来表明当前这轮迭代是否为第一次迭代的标志 |
last |
isLast()
|
用来表明当前这轮迭代是否为最后一次迭代的标志 |
begin |
getBegin()
|
begin
属性值
|
end |
getEnd()
|
end
属性值
|
step |
getStep()
|
step
属性值
|
清单 5 显示了关于如何使用
varStatus
属性的一个示例。这个示例修改了清单 4 中的代码,将 Weblog 项的编号添加到显示 Weblog 标题(title)的表行。它是通过为
varStatus
属性指定一个值,然后访问所生成的限定了作用域的变量的
count
特性来实现这一点的。结果显示在图 5 中。
清单 5. 使用 varStatus 属性来显示 Weblog 项的数目
|
图 5. 清单 5 的输出
除
<c:forEach>
以外,
core
库还提供了另一个迭代标记:
<c:forTokens>
。JSTL 的这个定制操作与 Java 语言的
StringTokenizer
类的作用相似。清单 6 中显示的
<c:forTokens>
标记除了比
<c:forEach>
的面向集合版本多一个属性之外,其它属性都相同。对于
<c:forTokens>
而言,通过
items
属性指定要标记化的字符串,而通过
delims
属性提供用于生成标记的一组定界符。和
<c:forEach>
的情形一样,可以使用
begin
、
end
和
step
属性将要处理的标记限定为那些与相应索引值相匹配的标记。
清单 6. 使用 <c:forTokens> 操作来迭代字符串标记的语法
|
对于包含动态内容的 Web 页面,您可能希望不同类别的用户看到不同形式的内容。例如,在我们的 Weblog 中,访问者应该能够阅读各项,也许还应该能够提交反馈,但只有经过授权的用户才能公布新项,或编辑已有内容。
在同一个 JSP 页面内实现这样的功能,然后使用条件逻辑来根据每条请求控制所显示的内容,这样做常常能够改善实用性和软件维护。
core
库提供了两个不同的条件化标记 ―
<c:if>
和
<c:choose>
― 来实现这些功能。
<c:if>
是这两个操作中较简单的一个,它简单地对单个测试表达式进行求值,接下来,仅当对表达式求出的值为
true
时,它才处理标记的主体内容。如果求出的值不为
true
,就忽略该标记的主体内容。如清单 7 所示,
<c:if>
可以通过其
var
和
scope
属性(它们所起的作用和在
<c:set>
中所起的作用一样)选择将测试结果赋给限定了作用域的变量。当测试代价非常高昂时,这种能力尤为有用:可以将结果高速缓存在限定了作用域的变量中,然后在随后对
<c:if>
或其它 JSTL 标记的调用中检索该结果。
|
清单 8 显示了与
<c:forEach>
标记的
LoopTagStatus
对象的
first
特性一起使用的
<c:if>
。如图 6 中所示,在这种情况下,只在 Weblog 项的第一项上显示这组项的创建日期,而不在任何其它项前面重复该日期。
清单 8. 使用 <c:if> 来为 Weblog 项显示日期
|
图 6. 清单 8 的输出
如清单 8 所示,
<c:if>
标记为条件化内容的一些简单情形提供了一种非常简洁的表示法。对于需要进行互斥测试来确定应该显示什么内容的情况下,JSTL
core
库还提供了
<c:choose>
操作。清单 9 中显示了
<c:choose>
的语法。
|
每个要测试的条件都由相应的
<c:when>
标记来表示,至少要有一个
<c:when>
标记。只会处理第一个其
test
值为
true
的
<c:when>
标记体内的内容。如果没有一个
<c:when>
测试返回
true
,那么会处理
<c:otherwise>
标记的主体内容。注:尽管如此,
<c:otherwise>
标记却是可选的;
<c:choose>
标记至多可有一个嵌套的
<c:otherwise>
标记。如果所有
<c:when>
测试都为
false
,而且又没有给出
<c:otherwise>
操作,那么不会处理任何
<c:choose>
标记的主体内容。
清单 10 显示了运用
<c:choose>
标记的示例。在这里,检索请求对象而获得协议信息(通过 EL 的
pageContext
隐式对象),并用简单的字符串比较对协议信息进行测试。根据这些测试的结果,会显示相应的文本消息。
|
最后一个流控制标记是
<c:catch>
,它允许在 JSP 页面内进行初级的异常处理。更确切地说,在该标记的主体内容中产生的任何异常都会被捕获并被忽略(即,不会调用标准的 JSP 错误处理机制)。然而,如果产生了一个异常并且已经指定了
<c:catch>
标记的可选属性
var
,那么会将异常赋给(具有页面作用域的)指定的变量,这使得能够在页面自身内部进行定制错误处理。清单 11 显示了
<c:catch>
的语法(稍后在
清单 18
中给出一个示例)。
|
|
|
JSTL
core
库中的其余标记主要是关于 URL。这些标记中的第一个被适当地命名为
<c:url>
标记,用于生成 URL。尤其是,
<c:url>
提供了三个功能元素,它们在为 J2EE Web 应用程序构造 URL 时特别有用:
- 在前面附加当前 servlet 上下文的名称
- 为会话管理重写 URL
- 请求参数名称和值的 URL 编码
清单 12 显示了
<c:url>
标记的语法。
value
属性用来指定基本 URL,然后在必要时标记对其进行转换。如果这个基本 URL 以一个斜杠开始,那么会在它前面加上 servlet 的上下文名称。可以使用
context
属性提供显式的上下文名称。如果省略该属性,那么就使用当前 servlet 上下文的名称。这一点特别有用,因为 servlet 上下文名称是在部署期间而不是开发期间决定的。(如果这个基本 URL 不是以斜杠开始的,那么就认为它是一个相对 URL,这时就不必添加上下文名称。)
|
URL 重写是由
<c:url>
操作自动执行的。如果 JSP 容器检测到一个存储用户当前会话标识的 cookie,那么就不必进行重写。但是,如果不存在这样的 cookie,那么
<c:url>
生成的所有 URL 都会被重写以编码会话标识。注:如果在随后的请求中存在适当的 cookie,那么
<c:url>
将停止重写 URL 以包含该标识。
如果为
var
属性提供了一个值(还可以同时为
scope
属性提供一个相应的值,这是可选的),那么将生成的 URL 赋值给这个限定了作用域的指定变量。否则,将使用当前的
JspWriter
输出生成的 URL。这种直接输出其结果的能力允许
<c:url>
标记作为值出现,例如,作为 HTML
<a>
标记的
href
属性的值,如清单 13 中所示。
|
最后,如果通过嵌套
<c:param>
标记指定了任何请求参数,那么将会使用 HTTP GET 请求的标准表示法将它们的名称和值添加到生成的 URL 后面。此外,还进行 URL 编码:为了生成有效的 URL,将对这些参数的名称或值中出现的任何字符适当地进行转换。清单 14 演示了
<c:url>
的这种行为。
|
清单 14 中的 JSP 代码被部署到一个名为
blog
的 servlet 上下文,限定了作用域的变量
searchTerm
的值被设置为
"core library"
。如果检测到了会话 cookie,那么清单 14 生成的 URL 将类似于清单 15 中的 URL。注:在前面添加上下文名称,而在后面附加请求参数。此外,
keyword
参数值中的空格和
month
参数值中的斜杠都被按照 HTTP GET 参数的需要进行了编码(确切地说,空格被转换成了
+
,而斜杠被转换成了
%2F
序列)。
|
当没有会话 cookie 时,生成的结果如清单 16 中所示。同样,servlet 上下文被添加到了前面,而 URL 编码的请求参数被附加到了后面。不过,除此以外还重写了基本 URL 以包含指定的会话标识。当浏览器发送用这种方式重写的 URL 请求时,JSP 容器自动抽取会话标识,并将请求与相应的会话进行关联。这样,需要会话管理的 J2EE 应用程序就无需依赖由应用程序用户启用的 cookie 了。
|
|
|
JSP 有两种内置机制可以将来自不同 URL 的内容合并到一个 JSP 页面:
include
伪指令和
<jsp:include>
操作。不过,不管是哪种机制,要包含的内容都必须属于与页面本身相同的 Web 应用程序(或 servlet 上下文)。两个标记之间的主要区别在于:
include
伪指令在页面编译期间合并被包含的内容,而
<jsp:include>
操作却在请求处理 JSP 页面时进行。
从本质上讲,
core
库的
<c:import>
操作是更通用、功能更强大的
<jsp:include>
版本(好像是
<jsp:include>
“服用了兴奋剂”的)。和
<jsp:include>
一样,
<c:import>
也是一种请求时操作,它的基本任务就是将其它一些 Web 资源的内容插入 JSP 页面中。如清单 17 中所示,它的语法非常类似于
<c:url>
的语法。
|
通过
url
属性指定将要导入内容的 URL,这个属性是
<c:import>
的唯一一个必选属性。这里允许使用相对 URL,并且根据当前页面的 URL 来解析这个相对 URL。但是,如果
url
属性的值以斜杠开始,那么它就被解释成本地 JSP 容器内的绝对 URL。如果没有为
context
属性指定值,那么就认为这样的绝对 URL 引用当前 servlet 上下文内的资源。如果通过
context
属性显式地指定了上下文,那么就根据指定的 servlet 上下文解析绝对(本地)URL。
但
<c:import>
操作并不仅仅限于访问本地内容。也可以将包含协议和主机名的完整 URI 指定为
url
属性的值。实际上,协议甚至不仅局限于 HTTP。
<c:import>
的
url
属性值可以使用
java.net.URL
类所支持的任何协议。清单 18 中显示了这种能力。
其中,
<c:import>
操作用来包含通过 FTP 协议访问的文档内容。此外,还使用了
<c:catch>
操作,以便在本地处理 FTP 文件传送期间可能发生的任何错误。错误处理是这样实现的:使用
<c:catch>
的
var
属性为异常指定一个限定了作用域的变量,然后使用
<c:if>
检查其值。如果产生了异常,那么就会对那个限定了作用域的变量进行赋值:如清单 18 中的 EL 表达式所显示的那样,该变量的值将
不
会为空。由于 FTP 文档的检索将会失败,因此会显示有关这种情况的错误消息。
清单 18. 将 <c:import> 与 <c:catch> 相结合的示例
|
<c:import>
操作的最后两个(可选的)属性是
var
和
scope
。
var
属性会导致从指定 URL 获取的内容(作为
String
值)被存储在一个限定了作用域的变量中,而不是包含在当前 JSP 页面中。
scope
属性控制该变量的作用域,缺省情况下是页面作用域。如同我们在今后的文章中将要看到的那样,JSTL
xml
库中的标记利用了
<c:import>
这种能力,即将整个文档存储在一个限定了作用域的变量中。
还要注意的是,可以使用(可选的)嵌套的
<c:param>
标记来为正在导入的 URL 指定请求参数。与在
<c:url>
中嵌套
<c:param>
标记一样,必要时也要对参数名称和参数值进行 URL 编码。
|
|
最后一个
core
库标记是
<c:redirect>
。该操作用于向用户的浏览器发送 HTTP 重定向响应,它是 JSTL 中与
javax.servlet.http.HttpServletResponse
的
sendRedirect()
方法功能相当的标记。清单 19 中显示了该标记的
url
和
context
属性,它们的行为分别等同于
<c:import>
的
url
和
context
属性的行为,是嵌套任何
<c:param>
标记的结果。
|
清单 20 显示了
<c:redirect>
操作,它用一个到指定错误页面的重定向代替了清单 18 中的错误消息。在该示例中,
<c:redirect>
标记的用法与标准
<jsp:forward>
操作的用法类似。不过请回忆一下:通过请求分派器进行转发是在服务器端实现的,而重定向却是由浏览器来执行的。从开发人员的角度来讲,转发比重定向更有效率,但
<c:redirect>
操作却更灵活一些,因为
<jsp:forward>
只能分派到当前 servlet 上下文内的其它 JSP 页面。
|
从用户的角度来看,主要区别在于重定向会更新浏览器所显示的 URL,并因此影响书签的设置。转发却不这样,它对最终用户是透明的。这样,选择
<c:redirect>
还是
<jsp:forward>
还取决于所期望的用户体验。
|
|
JSTL
core
库含有多种通用的定制标记,广大的 JSP 开发人员都会使用这些标记。例如,URL 和异常处理标记很好地补充了现有的 JSP 功能,如
<jsp:include>
和
<jsp:forward>
操作、
include
伪指令以及
page
伪指令的
errorpage
属性。迭代和条件操作使得无需脚本编制元素就能够实现复杂的表示逻辑,尤其在将变量标记(
<c:set>
和
<c:remove>
)与 EL 相结合使用时更是如此。
(3)JSTL 入门: 表达式语言
JSP 标准标记库(JSP Standard Tag Library,JSTL)是一个实现 Web 应用程序中常见的通用功能的定制标记库集,这些功能包括迭代和条件判断、数据管理格式化、XML 操作以及数据库访问。在 developerWorks 上其新系列的第一篇文章中,软件工程师 Mark Kolb 向您展示了如何使用 JSTL 标记来避免在 JSP 页面中使用脚本编制元素。您还将了解如何通过从表示层删除源代码来简化软件维护。最后,您将了解 JSTL 经过简化的表达式语言,它允许在不必使用功能齐全的编程语言的情况下对 JSTL 操作指定动态属性值。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
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 容器编译该页面时,要使所产生的错误信息有意义可能会很困难。
|
修正此类问题通常需要相当丰富的编程经验。尽管通常会由十分精通页面布局和图形设计的设计人员来开发和维护 JSP,但是同一页面中的脚本编制元素出现问题时,需要程序员的介入。这种状况将单个文件中代码的责任分担给多人,因而使得开发、调试和增强此类 JSP 页面成为很麻烦的任务。通过将常用功能包装到定制标记库的标准集合中,JSTL 使 JSP 作者可以减少对编制脚本元素的需求,甚至可以不需要它们,并避免了相关的维护成本。
|
|
JSTL 1.0 发布于 2002 年 6 月,由四个定制标记库(
core
、
format
、
xml
和
sql
)和一对通用标记库验证器(
ScriptFreeTLV
和
PermittedTaglibsTLV
)组成。
core
标记库提供了定制操作,通过限制了作用域的变量管理数据,以及执行页面内容的迭代和条件操作。它还提供了用来生成和操作 URL 的标记。顾名思义,
format
标记库定义了用来格式化数据(尤其是数字和日期)的操作。它还支持使用本地化资源束进行 JSP 页面的国际化。
xml
库包含一些标记,这些标记用来操作通过 XML 表示的数据,而
sql
库定义了用来查询关系数据库的操作。
两个 JSTL 标记库验证器允许开发人员在其 JSP 应用程序中强制使用编码标准。可以配置
ScriptFreeTLV
验证器以在 JSP 页面中禁用各种类型的 JSP 脚本元素 ― scriptlet、表达式和声明。类似地,
PermittedTaglibsTLV
验证器可以用来限制可能由应用程序的 JSP 页面访问的定制标记库集(包括 JSTL 标记库)。
尽管 JSTL 最终将会成为 J2EE 平台的必需组件,但目前只有少数应用程序服务器包括它。JSTL 1.0 的参考实现可作为 Apache 软件基金会(Apache Software Foundation)的 Jakarta Taglibs 项目(请参阅 参考资料 )的一部分而获得。可以将该参考实现中的定制标记库合并到任何支持 JSP 1.2 和 Servlet 2.3 规范的服务器,以添加对 JSTL 的支持。
|
|
在 JSP 1.2 中,可以使用静态字符串或表达式(如果允许的话)指定 JSP 操作的属性。例如,在清单 2 中,对
<jsp:setProperty>
操作的
name
和
property
属性指定了静态值,而用表达式指定了其
value
属性。这个操作的效果是将请求参数的当前值赋予命名的 bean 特性。以这种形式使用的表达式被称为
请求时属性值(request-time attribute value)
,这是构建到 JSP 规范中的用于动态指定属性值的唯一机制。
|
因为请求时属性值是用表达式指定的,所以它们往往有和其它脚本元素一样的软件维护问题。因此,JSTL 定制标记支持另一种用于指定动态属性值的机制。可以用简化的 表达式语言 (EL)而不使用完整的 JSP 表达式来指定 JSTL 操作的属性值。EL 提供了一些标识符、存取器和运算符,用来检索和操作驻留在 JSP 容器中的数据。EL 在某种程度上以 EcmaScript(请参阅 参考资料 )和 XML 路径语言(XML Path Language,XPath)为基础,因此页面设计人员和程序员都应该熟悉它的语法。EL 擅长寻找对象及其特性,然后对它们执行简单操作;它不是编程语言,甚至不是脚本编制语言。但是,与 JSTL 标记一起使用时,它就能使用简单而又方便的符号来表示复杂的行为。EL 表达式的格式是这样的:用美元符号($)定界,内容包括在花括号({})中,如清单 3 所示。
|
此外,您可以将多个表达式与静态文本组合在一起以通过字符串并置来构造动态属性值,如清单 4 所示。单独的表达式由标识符、存取器、文字和运算符组成。标识符用来引用存储在数据中心中的数据对象。EL 有 11 个保留标识符,对应于 11 个 EL 隐式对象。假定所有其它标识符都引用 限制了作用域的变量 。存取器用来检索对象的特性或集合的元素。文字表示固定的值 ― 数字、字符、字符串、布尔型或空值。运算符允许对数据和文字进行组合以及比较。
清单 4. 组合静态文本和多个 EL 表达式以指定动态属性值
|
|
|
JSP API 通过
<jsp:useBean>
操作允许从 JSP 容器内的四个不同作用域中存储和检索数据。JSTL 通过提供用于指定和除去这些作用域中的对象的附加操作来扩展这一能力。此外,EL 提供将这些对象作为限制了作用域的变量进行检索的内置支持。特别地,任何出现在 EL 表达式中但不对应于任何 EL 隐式对象的标识符,都被自动假定为引用存储在四个 JSP 作用域的其中某个中的对象,这四个作用域是:
- 页面作用域
- 请求作用域
- 会话作用域
- 应用程序作用域
您可能还记得,只有在为特定请求处理页面期间才能检索存储在该页面作用域中的对象。如果对象是存储在请求作用域中的,可以在处理所有参与处理某请求的页面期间检索这些对象(譬如在对某个请求的处理中遇到了一个或多个
<jsp:include>
或
<jsp:forward>
操作)。如果对象是存储在会话作用域中的,则在与 Web 应用程序的交互式会话期间,可以由用户访问的任何页面检索它(即,直到与该用户交互相关联的
HttpSession
对象无效为止)。可以由任何用户从任何页面访问存储在应用程序作用域中的对象,直到卸载 Web 应用程序本身为止(通常是由于关闭 JSP 容器所致)。
通过将字符串映射为期望作用域中的对象来将对象存储到该作用域。然后,就可以通过提供相同字符串来从该作用域检索该对象。在作用域的映射中查找字符串,并返回被映射的对象。在 Servlet API 中,将此类对象称为相应作用域的 属性 。但是,在 EL 的上下文中,也将与属性相关联的字符串看作变量的名称,该变量通过属性映射的方式获得特定的值。
在 EL 中,与隐式对象无关联的标识符被认为是存储在四个 JSP 作用域中的名称对象。首先对页面作用域检查是否存在这样的标识符,其次对请求作用域、然后对会话作用域、最后对应用程序作用域依次进行这样的检查,然后测试该标识符的名称是否与存储在该作用域中的某个对象的名称匹配。第一个这样的匹配作为 EL 标识符的值被返回。通过这种方法,可以将 EL 标识符看作引用限制了作用域的变量。
从更技术的方面来说,没有映射到隐式对象的标识符是用
PageContext
实例的
findAttribute()
方法求值的,该实例表示对页面的处理,在该页面上,当前正在处理用于请求的表达式。标识符的名称作为参数传递给这个方法,然后该方法依次在四个作用域中搜索具有相同名称的属性。并将所找到的第一个匹配项作为
findAttribute()
方法的值返回。如果未在这四个作用域中找到这样的属性,则返回
null
。
最终,限制了作用域的变量是四个 JSP 作用域的属性,这些属性具有可以用作 EL 标识符的名称。只要对限制了作用域的变量赋予由字母数字组成的名称,就可以通过 JSP 中提供的用于设置属性的任何机制来创建它们。这包括内置的
<jsp:useBean>
操作,以及由 Servlet API 中的几个类定义的
setAttribute()
方法。此外,四个 JSTL 库中定义的许多定制标记本身就能够设置作为限制了作用域的变量使用的属性值。
|
|
表 1 中列出了 11 个 EL 隐式对象的标识符。不要将这些对象与 JSP 隐式对象(一共只有九个)混淆,其中只有一个对象是它们所共有的。
类别 | 标识符 | 描述 |
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
类
|
尽管 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 中汇总了这些运算符。
类别 | 运算符 |
算术运算符 |
+
、
-
、
*
、
/
(或
div
)和
%
(或
mod
)
|
关系运算符 |
==
(或
eq
)、
!=
(或
ne
)、
<
(或
lt
)、
>
(或
gt
)、
<=
(或
le
)和
>=
(或
ge
)
|
逻辑运算符 |
&&
(或
and
)、
||
(或
or
)和
!
(或
not
)
|
验证运算符 |
empty
|
算术运算符支持数值的加法、减法、乘法和除法。还提供了一个求余运算符。注:除法和求余运算符都有替代的、非符号的名称(为的是与 XPath 保持一致)。清单 5 中显示了一个演示算术运算符用法的示例表达式。对几个 EL 表达式应用算术运算符的结果是将该算术运算符应用于这些表达式返回的数值所得的结果。
|
关系运算符允许比较数字或文本数据。比较的结果作为布尔值返回。逻辑运算符允许合并布尔值,返回新的布尔值。因此,可以将 EL 逻辑运算符应用于嵌套的关系或逻辑运算符的结果,如清单 6 所示。
|
最后一种 EL 运算符是
empty
,它对于验证数据特别有用。
empty
运算符采用单个表达式作为其变量(也即,
${empty input}
),并返回一个布尔值,该布尔值表示对表达式求值的结果是不是“空”值。求值结果为
null
的表达式被认为是空,即无元素的集合或数组。如果参数是对长度为零的
String
求值所得的结果,则
empty
运算符也将返回
true
。
表 3 显示了 EL 运算符的优先级。正如清单 5 和 6 所示,可以用圆括号对表达式分组,高于普通的优先级规则。
[]
,
.
|
()
|
unary
-
、
not
、
!
、
empty
|
*
、
/
、
div
、
%
、
mod
|
+
、binary
-
|
()
<</code>
、
>
、
<=
、
>=
、
lt
、
gt
、
le
、
ge
|
==
、
!=
、
eq
、
ne
|
&&
、
and
|
||
、
or
|
|
|
在 EL 表达式中,数字、字符串、布尔值和
null
都可以被指定为文字值。字符串可以用单引号或双引号定界。布尔值被指定为
true
和
false
。
|
|
正如我们先前讨论的,JSTL 1.0 包括四个定制标记库。为了演示 JSTL 标记和表达式语言的交互,我们将研究几个来自 JSTL
core
库的标记。和使用任何 JSP 定制标记库一样,必须在您想要使用这个库标记的任何页面中包括
taglib
伪指令。清单 7 显示了用于这个特定库的伪指令。
清单 7. 用于 JSTL core 库 EL 版本的 taglib 伪指令
|
实际上,对应于 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 伪指令
|
|
|
我们首先要考虑的 JSTL 定制标记是
<c:set>
操作。正如已经说明的,限制了作用域的变量在 JSTL 中起关键作用,
<c:set>
操作提供基于标记的机制来创建和设置限制了作用域的变量。清单 9 中显示了该操作的语法,其中
var
属性指定了限制了作用域的变量的名称,
scope
属性表明了该变量驻留在哪个作用域中,
value
属性指定了分配给该变量的值。如果指定变量已经存在,则简单地将所指明的值赋给它。如果不存在,则创建新的限制了作用域的变量,并用该值初始化这个变量。
|
scope
属性是可选的,其缺省值是
page
。
清单 10 中显示了
<c:set>
的两个示例。在第一个示例中,将会话作用域变量设置成
String
值。在第二个示例中,用表达式来设置数值:将页面作用域内名为
square
的变量赋值为名为
x
的请求参数的值的平方。
|
您还可以将限制了作用域的变量的值指定为
<c:set>
操作的主体内容,而不是使用属性。使用这种方法,您可以重新编写清单 10 中的第一个示例,如清单 11 所示。此外,正如我们马上可以看到的,
<c:set>
标记的主体内容本身也可以使用定制标记。
<c:set>
主体内生成的所有内容都将作为一个
String
值赋给指定变量。
|
JSTL core 库包含第二个用于管理限制了作用域的变量的标记 ―
<c:remove>
。顾名思义,
<c:remove>
操作是用来删除限制了作用域的变量的,它获取两个属性。
var
属性指定待删除变量的名称,
scope
属性是可选的,它表示待删除变量来自哪个作用域,缺省为
page
,如清单 12 所示。
|
|
|
尽管
<c:set>
操作允许将表达式结果赋给限制了作用域的变量,但开发人员通常会希望只显示表达式的值,而不存储它。JSTL
<c:out>
定制标记承担这一任务,其语法如清单 13 所示。该标记对由其
value
属性指定的表达式进行求值,然后打印结果。如果指定了可选属性
default
,那么,在对
value
属性的表达式求值所得结果为
null
或空
String
的情况下,
<c:out>
将打印其值。
|
escapeXml
属性也是可选的。它控制当用
<c:out>
标记输出诸如“<”、“>”和“&”之类的字符(在 HTML 和 XML 中具有特殊意义)时是否应该进行转义。如果将
escapeXml
设置为 true,则会自动将这些字符转换成相应的 XML 实体(此处提到的字符分别转换成
<
、
>
和
&
)。
例如,假定有一个名为
user
的会话作用域变量,它是一个类的实例,该类为用户定义了两个特性:
username
和
company
。每当用户访问站点时,这个对象被自动分配给会话,但直到用户实际登录后,才会设置这两个特性。假定是这种方案,请考虑清单 14 中的 JSP 片段。在用户登录之后,这个片段将显示单词“Hello”,其后是他/她的用户名和一个惊叹号。但是,在用户登录之前,由这个片段生成的内容则是短语“Hello Guest!”。在这种情况下,因为
username
特性还有待初始化,所以
<c:out>
标记将转而打印出
default
属性的值(即字符串“Guest”)。
|
接下来,考虑清单 15,它使用了
<c:out>
标记的
escapeXml
属性。如果在这种情况下已经将
company
特性设置成 Java
String
值
"Flynn & Sons"
,那么,实际上该操作生成的内容将是
Flynn & Sons
。如果这个操作是生成 HTML 或 XML 内容的 JSP 页面的一部分,那么,这个字符串中间的“&”符号最终可能被解释为 HTML 或 XML 控制字符,从而妨碍了对该内容的显示或解析。但是,如果将
escapeXml
属性值设置成
true
,则所生成的内容将是
Flynn & Sons
。浏览器或解析器不会因在解释时遇到这种内容而出问题。假定 HTML 和 XML 是 JSP 应用程序中最常见的内容类型,所以
escapeXml
属性的缺省值是
true
就不足为奇了。
|
|
|
除了简化动态数据的显示之外,当通过
<c:set>
设置变量值时,
<c:out>
指定缺省值的能力也很有用。正如
清单 11
所示,用来赋给限制了作用域的变量的值可以指定为
<c:set>
标记的主体内容,也可以通过其值属性来指定。通过将
<c:out>
操作嵌套在
<c:set>
标记的主体内容中,变量赋值就可以利用其缺省值能力。
清单 16 中说明了这种方法。外部
<c:set>
标记的行为非常简单:它根据其主体内容设置会话作用域
timezone
变量的值。但是,在这种情况下,主体内容是通过
<c:out>
操作生成的。这个嵌套操作的值属性是表达式
${cookie['tzPref'].value}
,它尝试通过
cookie
隐式对象返回名为
tzPref
的 cookie 值。(
cookie
隐式对象将 cookie 名称映射到相应的
Cookie
实例,这意味着必须通过对象的
value
特性使用点运算符来检索储存在 cookie 中的实际数据。)
清单 16. 合并 <c:set> 和 <c:out> 以提供缺省变量值
|
但是,请考虑以下情况,用户是第一次尝试使用这段代码的 Web 应用程序。结果是,请求中没有提供名为
tzPref
的 cookie。这意味着使用隐式对象的查找将返回
null
,在这种情况下整个表达式将返回
null
。因为对
<c:out>
标记的
value
属性求值的结果是
null
,所以
<c:out>
标记会转而输出对其
default
属性求值的结果。在这里是字符串
CST
。因此,实际的结果是将
timezone
限制了作用域的变量设置成用户的
tzPref
cookie 中存储的时区,或者,如果没有,则使用缺省时区
CST
。
|
|
|
EL(与四个 JSTL 定制标记库提供的操作结合起来)允许页面作者不使用脚本元素即可实现表示层逻辑。例如,对比本文开头
清单 1
中的 JSP 代码和清单 17 中显示的通过 JSTL 实现的同样功能。(JSTL
core
库中其余的标记,包括
<c:choose>
及其子标记,将在本系列的下一篇文章中讨论。)尽管显然执行了条件逻辑,但是 JSTL 版本中没有 Java 语言源代码,并且标记之间的关系(尤其是关于嵌套需求)对于任何精通 HTML 语法的人都应该是熟悉的。
清单 17. 合并 <c:set> 和 <c:out> 以提供缺省变量值
|
通过提供大多数 Web 应用程序常用功能的标准实现,JSTL 有助于加速开发周期。与 EL 结合起来,JSTL 可以不需要对表示层程序编写代码,这极大地简化了 JSP 应用程序的维护。
(4)JSTL 入门: 访问SQL和XML内容
Web应用程序的标志是多个子系统的集成。SQL和XML是在这类子系统之间交换数据的两种最通用的机制。在本文中,Mark Kolb介绍访问JSP页面数据库和XML内容的sql和xml库并对JSTL进行了总结。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
Web应用程序的模板式(stereotypical)架构分为三层:处理请求的Web服务器、实施业务逻辑的应用程序服务器以及管理永久性数据的数据库。应用程序和数据库层之间的联接通常采用关系数据库中的SQL调用格式。当业务逻辑被写入到Java语言中时,JDBC用于实现这些调用。
如果应用程序调用与其它服务器(本地或远程)的集成,我们将需要用于在不同子系统之间交换数据的更深层次的机制。在Web应用程序内部和应用程序之间传送数据采用的越来越普遍的一种方法是XML文件的交换。
迄今为止,在我们的JSTL之旅中,我们讨论了JSTL 表达式语言(expression language ,EL)和 core 和 fmt 标记库。在最后一部分,我们将考虑sql和xml库--正如它们的名字表示的一样 -- 提供定制标记来接入和管理从SQL数据库和XML文件检索到的数据。
|
根据设计,XML提供灵活的方式来表示结构化数据,这些数据同时准备进行验证,因此它尤其适应于在松散联合的系统之间交换数据。这反过来使其成为Web应用程序极具吸引力的集成技术。
与使用XML表示的数据进行交互的第一步是把数据作为一个XML文件,对其进行检索并进行分解,以创建数据结构来接入该文件中的内容。在分解文件后,您可以有选择的对其进行转换以创建新的XML文件,您可以对新的XML文件进行相同的操作。最终,文件中的数据可以被提取,然后显示或使用作为输入数据来运行其它操作。
这些步骤都在用于控制XML的JSTL标记中反映出。根据我们在第2部分 探讨核心 中所讨论的,我们使用core库中的<c:import>标记来检索XML文件。然后使用<x:parse>标记来分解该文件,支持标准的XML分解技术,如文件对象模式(Document Object Model,DOM)和简单XML API(Simple API for XML,SAX)。<x:transform>标记可用于转换XML文件并依赖标准技术来转换XML数据:扩展样式表语言( Extensible Stylesheet Language, XSL)。最后,我们提供多个标记来接入和控制分解后的XML数据,但是所有这一切都依赖于另一种标准- XML路径语言(XML Path Language, XPath),以引用分解后的XML文件中的内容。
<x:parse>标记有多种格式,取决于用户希望的分解类型。这一项操作最基本的格式使用以下语法:
|
在这五种属性中,只有xml属性是需要的,其值应该是包含要分解的XML文件的字符串,或者是java.io.Reader实例,通过它可以读取要被分解的文件。此外,您可以使用以下语法,根据<x:parse>标记的主体内容来规定要被分解的文件:
|
var和scope属性规定存储分解后的文件的scoped变量。然后xml库中的其它标记可以使用这一变量来运行其它操作。注意,当var和 scope 属性存在时,JSTL用于表示分解后的文件的数据结构类型以实施为导向,从而厂商可以对其进行优化。
如果应用程序需要对JSTL提供的分解后的文件进行处理,它可以使用另一种格式的<x:parse>,它要求分解后的文件坚持使用一个标准接口。在这种情况下,该标记的语法如下:
|
当您使用<x:parse>的这一版本时,表示分解后的XML文件的对象必须使用org.w3c.dom.Document接口。当根据<x:parse>中的主体内容来规定XML文件时,您还可以使用varDom和scopeDom属性来代替var 和 scope属性,语法如下:
|
其它两个属性filter 和 systemId 可以实现对分解流程的精确控制。filter 属性规定org.xml.sax.XMLFilter类的一个实例,以在分解之前对文件进行过滤。如果要被分解的文件非常大,但目前的工作只需要处理一小部分内容时这一属性尤其有用。systemId属性表示要被分解的文件的URI并解析文件中出现的任何相关的路径。当被分解的XML文件使用相关的URL来引用分解流程中需要接入的其它文件或资源时需要这种属性
清单1展示了<x:parse> 标记的使用,包括与 <c:import>的交互。此处<c:import> 标记用于检索众所周知的Slashdot Web 网站的RDF Site Summary (RSS)反馈,然后使用<x:parse>分解表示RSS 反馈的XML文件,表示分解后的文件的以实施为导向的数据结构被保存到名为rss的变量(带有page 范围)中。
|
XML通过XSL样式表来转换。JSTL使用<x:transform>标记来支持这一操作。与<x:parse>的情况一样,<x:transform> 标记支持多种不同的格式。<x:transform> 最基本的格式的语法是:
|
|
此处,xml 属性规定要被转换的文件,xslt 属性规定定义这次转换的样式表。这两种属性是必要的,其它属性为可选。
与<x:parse>的xml属性一样,<x:transform>的xml 属性值可以是包含XML文件的字符串,或者是接入这类文件的Reader。此外,它还可以采用 org.w3c.dom.Document 类或javax.xml.transform.Source 类的实例格式。最后,它还可以是使用<x:parse> 操作的var或varDom属性分配的变量值。
而且,您可以根据<x:transform> 操作的主体内容来包含要被转换的XML文件。在这种情况下,<x:transform> 的语法是:
|
在这两种情况下,规定XSL 样式表的xslt 属性应是字符串、Reader或javax.xml.transform.Source实例。
如果var 属性存在,转换后的XML文件将分配给相应的scoped变量,作为org.w3c.dom.Document 类的一个实例。通常,scope属性规定这类变量分配的范围。
<x:transform> 标记还支持将转换结果存储到javax.xml.transform.Result 类的一个实例中,而不是作为org.w3c.dom.Document的一个实例。如果var 和 scope 属性被省略,result对象规定作为result属性的值,<x:transform>标记将使用该对象来保存应用该样式表的结果。清单2中介绍了使用<x:transform> 的result属性的这两种语法的变化:
清单2:使用result属性来提供javax.xml.transform.Result实例时,<x:transform>操作的语法变化
|
无论您采用这两种<x:transform>格式中的那一种,您都必须从定制标记单独创建javax.xml.transform.Result对象。该对象自身作为result属性的值提供。
如果既不存在var 属性,也不存在result属性,转换的结果将简单地插入到JSP页面,作为处理<x:transform> 操作的结果。当样式表用于将数据从XML转换成HTML时尤其有用,如清单3所示:
|
在本例中,使用 <c:import> 标记来读取RSS反馈和适当的样式表。样式表的输出结果是HTML,通过忽略<x:transform>的var和result 属性来直接显示。图1显示了实例结果:
与<x:parse>的systemId 属性一样,<x:transform>的xmlSystemId 和 xsltSystemId 属性用于解析XML文件中相关的路径。在这种情况下,xmlSystemId 属性应用于根据标记的 xml属性值提供的文件,而xsltSystemId 属性用于解析根据标记的xslt属性规定的样式表中的相关路径。
如果正在推动文件转换的样式表使用了参数,我们使用<x:param> 标记来规定这些参数。如果参数存在,那么这些标记必须在<x:transform> 标记主体内显示。如果根据主体内容规定了要被转换的XML文件,那么它必须先于任何 <x:param> 标记。
<x:param> 标记有两种必要的属性 -- name 和 value -- 就象本系列 第2部分 和 第3部分 中讨论的<c:param> 和 <fmt:param> 标记一样。
XML文件的分解和转换操作都是基于整个文件来进行。但是,在您将文件转换成一种可用的格式之后,一项应用程序通常只对文件中包含的一部分数据感兴趣。鉴于这一原因,xml 库包括多个标记来接入和控制XML文件内容的各个部分。
如果您已经阅读了本系列第2部分( 探讨核心 ),您将对这些xml 标记的名字非常熟悉。它们都基于JSTL core 库相应的标记。但是,这些core 库标记使用EL表达式,通过它们的value属性来接入JSP容器中的数据,而它们在xml 库中的副本使用XPath表达式,通过select属性接入XML文件中的数据。
XPath是引用XML文件中元素及它们的属性值和主体内容的标准化符号。正如其名字代表的一样,这种符号与文件系统路径的表示方法类似,使用短斜线来分开XPath语句的组分。这些组分对映于XML文件的节点,连续的组分匹配嵌套的Element。此外,星号可以用于作为通配符来匹配多个节点,括号内的表达式可以用于匹配属性值和规定索引。有多种在线参考资料介绍XPath和它的使用(见 参考资料 )。
要显示XML文件的数据的一个Element,使用<x:out> 操作,它与core 库的<c:out> 标记类似。 但是,<c:out> 使用名为value 和escapeXml的属性,<x:out> 的属性为select 和escapeXml:
|
当然,两者的区别在于<x:out> 的select 属性值必须是XPath表达式,而<c:out> 的value 属性必须是EL表达式。两种标记的escapeXml 属性的意义是相同的。
清单4显示了<x:out> 操作的使用。注意,规定用于select 属性的XPath表达式由一个EL表达式规定为scoped变量,尤其是$rss。这一EL表达式根据将被求值的XPath语句来识别分解后的XML文件。该语句在此处查找名为title且父节点名为channel的Element,从而选择它找到的第一个Element(根据表达式尾部[1]索引规定)。这一<x:out> 操作的结果是显示这一Element的主体内容,关闭正在转义(Escaping)的XML字符。
清单4:使用<x:out>操作来显示XML Element的主体内容
|
除了<x:out>之外,JSTL xml 库包括以下控制XML数据的标记:
- <x:set> ,向JSTL scoped 变量分配XPath表达式的值
- <x:if> ,根据XPath表达式的布尔值来条件化内容
- <x:choose>、<x:when>和<x:otherwise>,根据XPath表达式来实施互斥的条件化
- <x:forEach> ,迭代根据XPath表达式匹配的多个Elements
每个这些标记的操作与core库中相应的标记类似。例如,清单5中显示的<x:forEach>的使用, <x:forEach> 操作用于迭代XML文件中表示RSS反馈的所有名为item 的Element。注意,<x:forEach>主体内容中嵌套的两个<x:out> 操作中的XPath表达式与<x:forEach>标记正在迭代的节点相关。它们用于检索每个item element的子节点link 和 title。
清单5:使用<x:out> 和<x:forEach>操作来选择和显示XML数据
|
清单5中JSP程序代码的输出结果与 清单3 类似,它在 图1 中显示。xml 库以XPath为导向的标记提供备选的样式表来转换XML内容,尤其是当最后的输出结果是HTML的情况。
|
|
JSTL第4个也是最后一个操作是sql定制标记库。正如其名字代表的一样,该库提供与关系数据库交互的标记。尤其是sql 库定义规定数据源、发布查询和更新以及将查询和更新编组到事务处理中的标记。
Datasource
Datasource是获得数据库连接的工厂。它们经常实施某些格式的连接库来最大限度地降低与创建和初始化连接相关的开销。Java 2 Enterprise Edition (J2EE)应用程序服务器通常内置了Datasource支持,通过 Java命名和目录接口( Java Naming and Directory Interface,JNDI)它可用于J2EE应用程序。
JSTL的sql 标记依赖于Datasource来获得连接。实际上包括可选的dataSource 属性以明确规定它们的连接工厂作为 javax.sql.DataSource 接口实例,或作为JNDI名。
您可以使用<sql:setDataSource> 标记来获得javax.sql.DataSource 实例,它采用以下两种格式:
|
第一种格式只需要dataSource 属性,而第二种格式只需要url 属性。
通过提供JNDI名作为dataSource属性值,您可以使用第一种格式来接入与JNDI名相关的datasource。第二种格式将创建新的datasource,使用作为url属性值提供的JDBC URL。可选的driver 属性规定实施数据库driver的类的名称,同时需要时user 和 password 属性提供接入数据库的登录证书。
对于<sql:setDataSource>的任何一种格式而言,可选的var 和 scope 属性向scoped变量分配特定的datasource。如果var属性不存在,那么 <sql:setDataSource> 操作设置供sql 标记使用的缺省 datasource,它们没有规定明确的datasource。
您还可以使用javax.servlet.jsp.jstl.sql.dataSource 参数来配置sql 库的缺省datasource。在实践中,在应用程序的Web.xml文件中添加与清单6中显示的类似的程序代码是规定缺省datasource最方便的方式。使用<sql:setDataSource> 来完成这一操作要求使用JSP页面来初始化该应用程序,因此可以以某种方式自动运行这一页面。
清单6:使用JNDI名来设置JSTL在web.xml部署描述符中的缺省datasource
|
在建立了datasource接入之后,您可以使用<sql:query> 操作来执行查询,同时使用<sql:update> 操作来执行数据库更新。查询和更新使用SQL语句来规定,它可以是使用基于JDBC的java.sql.PreparedStatement 接口的方法来实现参数化。参数值使用嵌套的<sql:param> 和 <sql:dateParam> 标记来规定。
支持以下三种<sql:query> 操作:
|
前两种格式只要求sql 和 var 属性,第三种格式只要求var 属性。
var 和 scope 属性规定存储查询结果的scoped 变量。maxRows 属性可以用于限制查询返回的行数,startRow 属性允许忽略一些最开始的行数(如当结果集(Result set)由数据库来构建时忽略)。
在执行了查询之后,结果集被分配给scoped变量,作为javax.servlet.jsp.jstl.sql.Result 接口的一个实例。这一对象提供接入行、列名称和查询的结果集大小的属性,如表1所示:
表1:javax.servlet.jsp.jstl.sql.Result 接口定义的属性
属性 | 说明 |
rows | 一排SortedMap 对象,每个对象对映列名和结果集中的单行 |
rowsByIndex | 一排数组,每个对应于结果集中的单行 |
columnNames | 一排对结果集中的列命名的字符串,采用与rowsByIndex属性相同的顺序 |
rowCount | 查询结果中总行数 |
limitedByMaxRows | 如果查询受限于maxRows 属性值为真 |
在这些属性中,rows 尤其方便,因为您可以使用它来在整个结果集中进行迭代和根据名字访问列数据。我们在 清单7 中阐述了这一操作,查询的结果被分配到名为queryResults的scoped变量中,然后使用core 库中的<c:forEach>标记来迭代那些行。嵌套的<c:out> 标记利用EL内置的Map 收集支持来查找与列名称相对应的行数据。(记得在 第1部分 ,${row.title}和${row["title"]} 是相等的表达式。)
清单7还展示了使用<sql:setDataSource> 来关联datasource 和scoped变量,它由<sql:query> 操作通过其dataSource 属性随后接入。
清单7:使用<sql:query>来查询数据库,使用<c:forEach>来迭代整个结果集
|
图2显示了清单7中JSTL程序代码的实例页面输出结果。注意:清单7中<sql:query>操作主体中出现的SQL语句为参数化语句。
在<sql:query> 操作中,SQL语句根据主体内容来规定,或者使用?字符,通过sql 属性实现参数化。对于SQL语句中每个这样的参数来说,应有相应的<sql:param> 或 <sql:dateParam> 操作嵌套到<sql:query> 标记的主体中。<sql:param> 标记只采用一种属性 -- value --来规定参数值。此外,当参数值为字符串时,您可以忽略value 属性并根据<sql:param> 标记的主体内容来提供参数值。
表示日期、时间或时间戳的参数值使用<sql:dateParam> 标记来规定,使用以下语法:
|
对于<sql:dateParam>来说,value 属性的表达式必须求 java.util.Date 类实例的值,同时type 属性值必须是date、time或timestamp,由SQL语句需要那类与时间相关的值来决定。
与<sql:query> 一样,<sql:update> 操作支持三种格式:
|
sql 和dataSource 属性有与<sql:query>相同的<sql:update> 语义。同样,var 和 scope 属性可以用于规定scoped变量,但在这种情况下,分配给scoped变量的值将是java.lang.Integer 的一个实例,显示作为数据库更新结果而更改的行数。
事务处理用于保护作为一个组必须成功或失败的一系列数据库操作。事务处理支持已经嵌入到JSTL的sql 库中,通过将相应的<sql:query>和<sql:update>操作嵌套到<sql:transaction>标记的主体内容中,从而将一系列查询和更新操作打包到一个事务处理中也就显得微不足道了。
<sql:transaction> 语法如下:
|
<sql:transaction> 操作没有必需的属性。如果您忽略了dataSource 属性,那么使用JSTL的缺省datasource。isolation 属性用于规定事务处理的隔离级别,它可以是read_committed、read_uncommitted、repeatable_read或serializable。如果您未规定这一属性,事务处理将使用datasource的缺省隔离级别。
您可能希望所有嵌套的查询和更新必须使用与事务处理相同的datasource。实际上,嵌套到<sql:transaction>操作中的<sql:query> 或 <sql:update> 不能用于规定dataSource 属性。它将自动使用与周围的<sql:transaction>标记相关的datasource (显性或隐性)。
清单8显示了如何使用<sql:transaction> 的一个实例:
清单:使用<sql:transaction>来将数据库更新联合到事务处理中
|
|
|
JSTL的xml 和 sql 库使用定制标记,从而能够在JSP页面上实施复杂的功能,但是在您的表示层实施这类功能可能不是最好的方法。
对于多位开发人员长期编写的大型应用程序来说,实践证明,用户接口、基本的业务逻辑和数据仓库之间的严格分离能够长期简化软件维护。广泛流行的模式/视图/控制器( Model-View-Controller,MVC)设计模板是这一“最佳实践”的公式化。在J2EE Web应用程序领域中,模式是应用程序的业务逻辑,视图是包含表示层的JSP页面。(控制器是form handlers和其它服务器端机制,使浏览器操作能够开始更改模式并随后更新视图。) MVC规定应用程序的三个主要elements--模式、视图和控制器 --相互之间有最小的相关性,从而限制相互之间的交互到统一、精心定义的接口。
应用程序依赖XML文件来进行数据交换以及关系数据库来提供数据永久性都是应用程序业务逻辑(也就是其模式)的特征。因此,使用MVC设计模板建议无需在应用程序的表示层(也就是其视图)反映这些实施细节。如果JSP用于实施表示层,那么使用 xml 和 sql 库将违反MVC,因为它们的使用将意味着在表示层内暴露基本业务逻辑的elements。
鉴于这一原因,xml 和 sql 库最适用于小型项目和原型工作。应用程序服务器对JSP页面的动态编译也使得这些库中的定制标记可以用于作为调试工具。
|
|
在本系列中,我们讨论了4个JSTL定制标记库的功能及它们的使用。在 第1部分 和 第2部分 ,我们讨论通过El和core 库标记的使用,您如何在许多常见情况下避免JSP脚本程序。 第3部分 关注使用fmt 库来本地化Web内容。
在最后一部分,我们讨论了xml 和 sql 库的功能。如果您愿意接受将业务逻辑包含到表示层的结果,这两个库中的标记都使其能够非常轻松地将XML文件和关系数据库中的内容结合到JSP页面。这两个库还展示了当集成<sql:query> 和<c:forEach>时,JSTL库如何构建和集成,以及xml 库利用<c:import> 操作的能力。