使用 JSP 2.0 开发类似 JSTL 的标记
学习如何使用简单标记 API 和构建用于求解 JSP 表达式的定制标记,如何控制 JSP 页面中的流以及如何创建 Java 集合。
本文相关下载:
· 示例代码 · OC4J 10g 开发人员预览版 2 · JSTL 1.1 |
JavaServer Pages (JSP) 和 JSP 标准标记库 (JSTL) 为 Web 开发人员提供了许多有用的标记(也称作操作)。此外,JSP 2.0 还提供两个 API,即标准标记 API 和简单标记 API,用于构建定制标记/操作。前一个 API 继承自 JSP 1.x,并由于历史原因而由 JSTL 使用。(由于 JSTL 1.0 的开发在 JSP 2.0 之前,因此新 API 不包含 JSTL 1.1。)此外,JSTL 也不使用 JSP 片段和动态属性等 JSP 新特性。本文使用 JSP 2.0 的新 API 和特性构建定制标记扩展 JSTL。本文提供 API 概述并演示如何开发
- 导出变量的标记
- 条件标记
- 迭代标记
- 具有动态属性的标记
- 协调标记
简单标记 API 概述
在 JSP 页面中使用定制标记时,应用服务器的 JSP 容器将 <prefix:customTag> ...</prefix:customTag> 转换为调用称为标记处理类的方法的 Java 代码。因此,如果要开发定制标记,必须提供一个标记处理类,此类必须使用 JSP 1.x 标准标记 API 或 JSP 2.0 简单标记 API。比较一下这两个 API,就会发现新 API 更易于使用。简单标记 API 只有一个接口 (javax.servlet.jsp.tagext.SimpleTag),它定义了处理定制标记的方法。通常从 JSP 容器从 JSP 页面中自动生成的 Java Servlet 中调用这些方法。
javax.servlet.jsp.tagext.SimpleTagSupport 类实现了 SimpleTag 接口,因此当标记处理类扩展 SimpleTagSupport 时只须编写 doTag() 方法即可。以下步骤介绍了如何开发一个简单的标记处理类:
第 1 步:设计定制标记
首先,必须为标记选择一个名称并设置它的属性。然后,创建一个标记库描述符 (TLD) 文件(采用由 JSP 规范定义的 XML 格式),以告知 JSP 容器如何处理和验证定制标记。文本提供了一个名为 util.tld 的示例 TLD 文件。
第 2 步:创建标记处理类
必须提供一个用于实现 SimpleTag 接口的 Java 类。最简单的方法是扩展 SimpleTagSupport 或它的某个子类。本文中的 VarTagSupport、IfTag 和 WhileTag 类用于扩展 SimpleTagSupport。其他标记处理类示例扩展 VarTagSupport。
如果要使用未在 TLD 文件中指定的属性,则标记处理类必须实现 javax.servlet.jsp.tagext.DynamicAttributes 接口(如“具有动态属性的标记”部分中介绍的 MapTag 示例所示)。
第 3 步:初始化标记处理类实例
每个标记处理类都必须包含一个不带参数的公共构造函数,用于放置初始化代码。本文中的某些标记处理类(EvalTag、ListTag 和 MapTag)包含一个无参数的公共构造函数,它使用默认值初始化实例变量。其他类(IfTag、WhileTag 和 ItemTag)没有构造函数。请注意,Java 编译器在类不包含任何构造函数的情况下自动生成一个无参数的公共构造函数,该函数不执行任何操作。
第 4 步:提供属性设置方法
JSP 页面中的标记属性值通过 setAttribute() 方法传递给标记处理类。例如,本文中的 <u:eval> 标记包含四个属性:var、scope、expr 和 type。EvalTag 处理类实现 setExpr() 和 setType() 方法,并从 VarTagSupport 继承 setVar() 和 setScope()。
动态属性通过 DynamicAttributes 接口定义的 setDynamicAttribute() 方法传递。
第 5 步:实现 doTag() 方法
该方法用于实现定制标记的逻辑。doTag() 方法由 JSP 容器继所有属性设置方法之后调用。此处可以使用 getJspContext() 获得一个 javax.servlet.jsp.JspContext 对象来访问 JSP 环境。可以调用 getJspBody(),它返回 javax.servlet.jsp.tagext.JspFragment 的实例,该实例表示位于 <prefix:customTag> 和 </prefix:customTag> 之间的 JSP 主体。如果要开发协同工作的标记,如 <u:list> 和 <u:item>(本文的最后一部分将对其进行介绍),则还可以使用 getParent() 和 findAncestorWithClass() 方法。
第 6 步:测试定制标记
使用定制标记的 JSP 页面必须使用 <%@taglib%> 指令导入该标记的标记库。当定制标记出现在 JSP 页面中时,JSP 容器将生成创建标记处理类实例、调用属性设置方法和调用 doTag() 方法的代码。因此,在使用定制标记的 JSP 页面的执行过程中将调用标记处理类方法。
限制和变通方法
为简化标记处理 API,JSP 2.0 采取了一个限制:如果定制标记的处理类是基于简单标记 API 的,则页面作者不得在 <prefix:customTag> 和 </prefix:customTag> 之间使用 JSP 1.x 声明 (<%!...%>)、JSP 1.x 表达式 (<%=...%>) 和 scriptlet (<%...%>)。大多数情况下,您可以将 JSP 页面中的 Java 代码移动到标记处理类中,或在 JSP 2.0 表达式 (${...})(可以在定制标记的主体中使用)中使用 JSTL。请注意,JSP 2.0 允许您在基于标准标记 API 的定制标记主体中使用 scriptlet。然而,由于不使用脚本的 JSP 页面更易于维护,因此最好避免在 Web 页中使用 Java 代码。
我的上一篇 Oracle 技术网 (OTN) 文章“使用 JSP 2.0 EL API”介绍了简单标记 API 的另一个限制并提供了变通方法。JspContext 类未提供对 JSP 隐式对象(如application、session、request 和 response)的访问。大多数应用服务器(包括 Oracle Application Server Containers for J2EE (OC4J) 10g)允许将 JSP 上下文转换为 PageContext
标记处理类不适用于使用 println() 语句生成大量可重用的 HTML 代码。JSP 2.0 为此工作提供了一个更好的方法。所谓的标记文件使用 JSP 语法并由 JSP 容器自动转换为基于简单标记 API 的标记处理类。我的另一篇 OTN 文章“创建 JSP 2.0 标记文件”介绍了这个 JSP 新特性。
导出变量的标记
许多 JSTL 标记实现某个逻辑并导出 JSP 变量以报告结果。例如,<sql:query> 包含一个 var 属性,该属性必须指定用于保存 SQL 结果集的 JSP 变量的名称。var 属性对其他 JSTL 标记(如 <fmt:formatNumber> 和 <fmt:formatDate>)来说是可选的。如果 var 属性不存在,则这些标记将输出它们的结果。所有包含 var 属性的标记还包含一个 scope 属性,该属性可用于指示以下 JSP 变量的作用域:page、request、session 或 application。
VarTagSupport 类(它是为本文开发的一个示例)扩展 SimpleTagSupport 并为 var 和 scope 属性提供设置方法。VarTagSupport 包含用于导出 JSP 变量、获取主体内容和输出内容的实用方法,而不是实现 doTag() 方法。这些方法由 VarTagSupport 的子类在 doTag() 中使用。本文包含四个用于扩展 VarTagSupport 的标记处理类(EvalTag、MapTag、ListTag 和 ItemTag)。
请注意,JSP 变量在 JSTL 规范中称作范围变量,而在 JSP 规范中称作具名变量或范围属性。这些变量通过 JspContext 类的 setAttribute() 方法创建/导出。您可以在 JSP 页面中使用 ${varName},以及在 Java 代码中使用 JspContext 的 getAttribute() 或 findAttribute() 方法取得它们的值。不要混淆 JSP 变量与标记属性。
实现属性设置方法
JSP 容器调用属性设置方法,将标记属性的值传递给定制标记处理类。VarTagSupport 的 setVar() 方法将 var 属性的值存储在受保护的实例变量 (varName) 中。setScope() 方法将它的参数转换为整数常数。如果该参数包含有效值(page、request、session 或 application),则将此整数常数存储在另一受保护的实例变量 (varScope) 中。否则,setScope() 将抛出 JspException:
package jsputils.tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.tagext.SimpleTagSupport; ... public class VarTagSupport extends SimpleTagSupport { protected String varName; protected int varScope; protected VarTagSupport() { varScope = PageContext.PAGE_SCOPE; } public void setVar(String name) throws JspException { varName = name; } public void setScope(String scope) throws JspException { if (scope.equalsIgnoreCase("page")) varScope = PageContext.PAGE_SCOPE; else if (scope.equalsIgnoreCase("request")) varScope = PageContext.REQUEST_SCOPE; else if (scope.equalsIgnoreCase("session")) varScope = PageContext.SESSION_SCOPE; else if (scope.equalsIgnoreCase("application")) varScope = PageContext.APPLICATION_SCOPE; else throw new JspException("Invalid scope:" + scope); } ... }
将变量导出到 JSP 环境
如果 var 属性存在,并具有非 null 值 (varName != null),则 export() 方法使用 getJspContext() 取得 JSP 上下文。随后,如果 value 参数不为 null,则 export() 将使用 JSP 上下文的 setAttribute() 方法设置 JSP 变量。可以在 JSP 页面中使用 ${varName} 取得变量值。如果 value 参数为 null,则 export() 将调用 removeAttribute(),后者从给定的范围中删除任何具有给定名称的现有变量。
如果 var 属性不存在或具有 null 值,则 export() 方法将返回 false。否则,export() 将返回 true:
package jsputils.tags; import javax.servlet.jsp.JspContext; import javax.servlet.jsp.tagext.SimpleTagSupport; ... public class VarTagSupport extends SimpleTagSupport { ... protected boolean export(Object value) { if (varName == null) return false; JspContext jspContext = getJspContext(); if (value != null) jspContext.setAttribute(varName, value, varScope); else jspContext.removeAttribute(varName, varScope); return true; } ... }
取得由标记主体生成的内容
标记处理类可以使用从 SimpleTagSupport 继承的 SimpleTagSupport 方法取得表示所处理 JSP 标记主体的 JspFragment。然后,标记处理类可以使用 invoke() 方法执行 JSP 片段;如果要捕获由 JSP 主体生成的内容,则该方法需要 java.io.Writer 参数。invokeBody() 方法将这些操作分组,并返回 String 类型的主体内容:
package jsputils.tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.JspFragment; import javax.servlet.jsp.tagext.SimpleTagSupport; ... import java.io.StringWriter; import java.io.IOException; public class VarTagSupport extends SimpleTagSupport { ... protected String invokeBody() throws JspException { JspFragment body = getJspBody(); StringWriter buffer = new StringWriter(); try { body.invoke(buffer); } catch (IOException x) { throw new JspException(x); } return buffer.toString(); } ... }
请注意,如果只想输出由 JSP 主体生成的内容,则可以使用 null 参数调用 invoke() 方法。如果不调用 invoke(),则不执行定制标记的 JSP 主体。
在标记执行过程中生成内容
标记处理类可以使用由 JSP 上下文的 getOut() 方法返回的 JspWriter 输出内容:
package jsputils.tags; import javax.servlet.jsp.JspContext; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.SimpleTagSupport; ... import java.io.IOException; public class VarTagSupport extends SimpleTagSupport { ... protected void write(String str) throws JspException { JspContext jspContext = getJspContext(); JspWriter out = jspContext.getOut(); try { out.write(str); } catch (IOException x) { throw new JspException(x); } } }
开发导出变量的定制标记
“使用 JSP 2.0 EL API”介绍了名为 ELUtils 的类的开发,该类的方法在 Java 代码中求解 JSP 表达式。当您要在 JSP 页面外使用 EL时(如在 XML 文件中使用),EL API 将很有帮助。在“使用 JSP 2.0 EL API”中,我们将 ELUtils 的静态方法映射为 EL 函数。这次,我们将构建一个定制标记 (<u:eval>),它调用 ELUtils 的某个 evaluate() 方法。<u:eval> 标记由名为 EvalTag 的类处理,该类为 <u:eval> 的属性实现两个设置方法(setExpr() 和 setType())。属性 expr 和 type 的值被传递给 evaluate() 方法,该方法返回表达式的值。
如果 expr 属性不存在,则 doTag() 方法将调用从 VarTagSupport 类继承的 invokeBody() 方法以取得主体内容(应为表达式)。因此,调用 <u:eval> 标记的 JSP 页面可以将表达式指定为 expr 属性的值或置于 <u:eval> 和 </u:eval>之间。
EvalTag 类扩展了 VarTagSupport,这是因为它需要 export(),以便使用 var 属性指定的名称和由 evaluate() 返回的值创建 JSP 变量。如果 var 属性不存在,则 export() 无法设置变量并返回 false。这种情况下,EvalTag 使用从 VarTagSupport 继承的 write() 方法输出所求解表达式的值。
EvalTag 处理类的源代码如下所示:
package jsputils.tags; import jsputils.el.ELUtils; import javax.servlet.jsp.JspException; public class EvalTag extends VarTagSupport { private String strExpr; private Object varType; public EvalTag() { varType = Object.class; } public void setExpr(String expr) throws JspException { strExpr = expr; } public void setType(Object type) throws JspException { varType = type; } protected Object evaluate(String expression, Object expectedType) throws JspException { return ELUtils.evaluate( expression, expectedType, getJspContext()); } public void doTag() throws JspException { if (strExpr == null) strExpr = invokeBody(); Object value = evaluate(strExpr, varType); boolean exported = export(value); if (!exported && value != null) write(value.toString()); } }
在库描述符中定义定制标记
<u:eval> 标记定义在名为 util.tld 的 XML 文件中。JSP 容器使用此文件将定制标记映射为它的处理类 (EvalTag)。除标记的名称和标记处理类外,该描述符还包含有关标记主体和属性的信息。
主体内容被声明为 scriptless,这意味着不能在 <u:eval> 和 </u:eval> 之间使用 Java 代码 (scriptlet)。如果标记不使用它的主体内容,则应指定 empty 而非 scriptless。请注意,对于标准标记,还可以指定 JSP,它允许在标记的主体中使用 Java scriptlet。基于简单标记 API 开发处理类时,必须将所有 Java 代码置于 Java 类中。
util.tld 描述符为 <u:eval> 标记定义了四个属性:expr、type、var 和 scope。所有属性都被声明为可选(required 为 false)。expr 和 type 的值可以包含 JSP 表达式(rtexprvalue 为 true),但 var 和 scope 属性必须具有固定值(rtexprvalue 为 false)。
定制标记在 <taglib> 元素中描述,该元素包含版本号、短名称(前缀)和统一资源标识符 (URI)(不一定指示现有 Web 资源):
<?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd" version="2.0"> <tlib-version>1.0</tlib-version> <short-name>u</short-name> <uri>http://otn.oracle.com/jsp/taglib/util.tld</uri> <tag> <name>eval</name> <tag-class>jsputils.tags.EvalTag</tag-class> <body-content>scriptless</body-content> <attribute> <name>expr</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>type</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>var</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> ... </taglib>
在 JSP 页面中使用定制标记
示例 Web 应用程序的 web.xml 描述符定义了两个参数:debug_mode 和 tags_db_dataSource。debug_mode 参数指示应用程序是运行在测试环境中还是运行在生产环境中。tags_db_dataSource 参数使用 EL 根据 debug_mode 的值选择数据源名称:
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd" version="2.4"> <context-param> <param-name>debug_mode</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>tags_db_dataSource</param-name> <param-value>jdbc/${ initParam.debug_mode ?"dbtags" :"production" }</param-value> </context-param> </web-app>
使用 <%@taglib%> 导入本文的标记库后,EvalTest.jsp 页面将使用 <u:eval> 标记求解 web.xml 文件中的表达式:
<!-- EvalTest.jsp --> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <u:eval expr="${initParam.tags_db_dataSource}" var="db"/> ${db} <u:eval expr="${initParam.tags_db_dataSource}"/> <u:eval>${initParam.tags_db_dataSource}</u:eval>
该 JSP 页面测试两种指定表达式的方法:使用 expr 属性以及置于 <u:eval> 和 </u:eval> 之间。var 属性用于创建名为 db 的 JSP 变量,它的值使用 ${db} 输出。如果 var 属性不存在,则 <u:eval> 标记输出所求解表达式的值。以下是 EvalTest.jsp 生成的输出:
jdbc/dbtags jdbc/dbtags jdbc/dbtags
条件标记
JSTL 提供了几个条件标记(<c:if>、<c:choose>、<c:when> 和 <c:otherwise>)以及一个用于捕获 JSP 页面中异常的标记 (<c:catch>)。这些标记虽然简单、有用,但并非得益于 JSP 2.0 的片段属性特性,该特性允许单个标记处理多个 JSP 片段。本文的此部分使用片段属性构建一个更复杂的名为 <u:if> 并由 IfTag 类处理的条件标记。IfTag 示例还演示了如何捕获在 JSP 片段执行过程中可能发生的任何异常。
使用片段属性
假设有一个包含两个文本域(unitPrice 和 quantity)的表单,需要计算总价。还需要处理用户未填写表单或提供非数字值(可能生成 NumberFormatException)的情况。在实际应用程序中,可能会使用框架(如 JavaServer Faces (JSF))生成 HTML 表单和验证用户输入。但为了测试本部分中开发的条件标记,假设要创建不使用专用标记库的表单。以下是要使用的代码:
<!-- IfTest.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <html> <body> <form method="post"> <c:set var="paramsProvided" value="${!empty param.unitPrice and !empty param.quantity}"/> ... <p> Unit Price: <input type="text" name="unitPrice" size="10" value="<c:out value='${param.unitPrice}'/>"> <p> Quantity: <input type="text" name="quantity" size="10" value="<c:out value='${param.quantity}'/>"> <p> <input type="submit" value="Calculate Price"> </form> </body> </html>
以下代码演示了如何使用 JSTL 的 <c:if> 和 <c:catch> 标记验证表单数据:
<c:if test="${paramsProvided}"> <c:catch var="error"> <c:set var="price" value="${param.unitPrice * param.quantity}"/> <p> Price:${price} </c:catch> </c:if> <c:if test="${not paramsProvided}"> <p> Please fill out the form </c:if> <c:if test="${error != null}"> <p> Number format error </c:if>
前面的代码段不是 IfTest.jsp 的一部分。该页面使用定制标记 <u:if>(包含 test 属性,如 <c:if>)而不是使用 JSTL 验证表单数据。<u:if> 标记包含三个条件属性,即 TRUE、FALSE 和 ERROR。这些属性的值是由 <jsp:attribute> 标准操作包含的 JSP 片段:
<u:if test="${paramsProvided}"> <jsp:attribute name="TRUE"> <c:set var="price" value="${param.unitPrice * param.quantity}"/> <p> Price:${price} </jsp:attribute> <jsp:attribute name="FALSE"> <p> Please fill out the form </jsp:attribute> <jsp:attribute name="ERROR"> <p> Number format error </jsp:attribute> </u:if>
声明片段属性
必须使用 <fragment>true</fragment> 在描述符文件中声明片段属性:
<taglib ...> ... <tag> <name>if</name> <tag-class>jsputils.tags.IfTag</tag-class> <body-content>empty</body-content> <attribute> <name>test</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>TRUE</name> <required>true</required> <fragment>true</fragment> </attribute> <attribute> <name>FALSE</name> <required>false</required> <fragment>true</fragment> </attribute> <attribute> <name>ERROR</name> <required>false</required> <fragment>true</fragment> </attribute> </tag> ... </taglib>
处理片段属性
IfTag 处理类具有 <u:if> 的常规 test 属性以及 TRUE、FALSE 和 ERROR 片段属性的设置方法。doTag() 方法使用 JspFragment 类的 invoke() 方法执行 JSP 片段:
package jsputils.tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.JspFragment; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; public class IfTag extends SimpleTagSupport { private boolean testAttr; private JspFragment trueAttr; private JspFragment falseAttr; private JspFragment errorAttr; public void setTest(boolean test) { testAttr = test; } public void setTRUE(JspFragment fragment) { trueAttr = fragment; } public void setFALSE(JspFragment fragment) { falseAttr = fragment; } public void setERROR(JspFragment fragment) { errorAttr = fragment; } public void doTag() throws JspException, IOException { try { if (testAttr) { if (trueAttr != null) trueAttr.invoke(null); } else { if (falseAttr != null) falseAttr.invoke(null); } } catch (Exception x) { if (errorAttr != null) errorAttr.invoke(null); else throw new JspException(x); } } }
迭代标记
JSTL 具有三个迭代标记(<c:forEach>、<c:forTokens> 和 <x:forEach>)。本部分演示了如何实现在我的上篇文章中介绍的 <u:while> 标记(在每次迭代前使用 EL API 判断其条件)。
在定制标记中使用 JSTL 函数库
JSTL 1.1 提供了一个函数库来实现许多有用的字符串操作。以下代码尝试使用 JSTL 的 fn:split() 函数将字符串 (a1/a2//b//c1/c2/c3) 拆分为三个标记(a1/a2、b 和 c1/c2/c3):
<!-- SplitTest.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <p>fn:split - <c:set var="str" value="a1/a2//b//c1/c2/c3"/> <c:set var="delim" value="//"/> <c:set var="array" value="${fn:split(str, delim)}"/> <c:forEach var="token" items="${array}"> [<c:out value="${token}"/>] </c:forEach>
上面的代码未生成所要的结果,这是因为 fn:split() 是基于 java.util.StringTokenizer 的,后者将 delim 参数作为一组分隔字符处理。以下是 SplitTest.jsp 生成的输出:
fn:split - [a1] [a2] [b] [c1] [c2] [c3]
JSTL 提供了其他字符串函数(如 fn:contains()、fn:substringBefore() 和 fn:substringAfter()),可以通过循环使用这些函数将 a1/a2//b//c1/c2/c3 拆分为 a1/a2、b 和 c1/c2/c3。该循环由 <u:while> 标记控制:
<!-- WhileTest.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <p>u:while - <c:set var="str" value="a1/a2//b//c1/c2/c3"/> <c:set var="delim" value="//"/> <u:while test="\${fn:contains(str, delim)}"> [<c:out value="${fn:substringBefore(str, delim)}"/>] <c:set var="str" value="${fn:substringAfter(str, delim)}"/> </u:while> [<c:out value="${str}"/>]
请注意,$ 字符在 test 条件中使用反斜杠进行了转义。因此,JSP 容器将 test 属性的值作为文本处理,而不求解 JSP 表达式的值。<u:while> 标记由 WhileTag 类处理,该类在每此迭代前求解 ${fn:contains(str, delim)} 表达式。${fn:substringBefore(str, delim)} 表达式返回字符串的第一个标记,而 ${fn:substringAfter(str, delim)} 返回剩余标记。WhileTest.jsp 页面生成所要的输出:
u:while - [a1/a2] [b] [c1/c2/c3]
多次求解同一 JSP 表达式
标记处理类可以使用 invoke() 方法多次调用定制标记的主体。还可以使用同一 invoke() 方法多次执行片段属性。但如果要对常规属性重新求值,则必须使用 EL API。“使用 JSP 2.0 EL API”中的 PEWrapper 类使您能够通过一次性分析 JSP 表达式来优化此过程。请注意,此示例类只是标准 EL API 的包装类。WhileTag 处理类的 doTag() 方法创建一个 PEWrapper 实例,该实例需要必须求解的表达式 (strTest)、预期的类型 (Boolean)、JSP 上下文和一个函数映射类(使您能够在 JSP 表达式中使用 JSTL 函数)。FNMapper 类(“使用 JSP 2.0 EL API”中提供了该类)将 JSTL 函数映射为一组由 JSTL 的 Apache 实现提供的静态 Java 方法。当 test 表达式为 true 时,doTag() 方法执行 <u:while> 标记的 JSP 主体:
package jsputils.tags; import jsputils.el.PEWrapper; import jsputils.el.FNMapper; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; public class WhileTag extends SimpleTagSupport { private String strTest; public void setTest(String test) throws JspException { strTest = test; } public void doTag() throws JspException, IOException { PEWrapper parsedExpr = PEWrapper.getInstance( strTest, Boolean.class, getJspContext(), FNMapper.getInstance("fn")); while (((Boolean) parsedExpr.evaluate()).booleanValue()) getJspBody().invoke(null); } }
util.tld 文件中定义了 <u:while> 和 WhileTag 类之间的映射:
<taglib ...> ... <tag> <name>while</name> <tag-class>jsputils.tags.WhileTag</tag-class> <body-content>scriptless</body-content> <attribute> <name>test</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> ... </taglib>
具有动态属性的标记
JSTL 提供对 Java 集合的支持。例如,可以使用 <c:set> 为 java.util.Map 添加元素。但无法使用 JSTL 创建 Map 对象。本部分构建了一个定制标记,该标记创建一个用于保存 java.util.LinkedHashMap 的 JSP 变量。您可以按以下示例所示在 JSP 页面中创建 Map 对象:
<u:map var="langMap" scope="application" en="English" de="Deutsch" fr="Fraais" it="Italiano" es="Espol"/>
此 Map 对象使用标记的动态属性(en、de、fr、it 和 es)进行初始化。这些属性之所以称作动态属性是因为它们不在库描述符中声明。
处理动态属性
定制标记可以通过实现 DynamicAttributes 接口(它只有一个方法 setDynamicAttribute())处理动态属性。JSP 容器对未在 TLD 文件中声明的每个属性调用此方法。
MapTag 处理类扩展了 VarTagSupport 并实现了 DynamicAttributes。LinkedHashMap 实例在 MapTag() 构造函数中创建。setDynamicAttribute() 方法通过将属性名称作为属性值的键为 Map 对象中添加元素。doTag() 方法使用从 VarTagSupport 继承的 export() 方法将 Map 对象导出为 JSP 变量:
package jsputils.tags; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.DynamicAttributes; import java.util.LinkedHashMap; public class MapTag extends VarTagSupport implements DynamicAttributes { private LinkedHashMap map; public MapTag() { map = new LinkedHashMap(); } public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { map.put(localName, value); } public void doTag() throws JspException { export(map); } }
请注意,LinkedHashMap 保留其元素的顺序,这意味着由 iterator() 方法返回的 java.util.Iterator 按元素添加到 Map 对象的顺序访问这些元素。
必须在 util.tld 文件中声明对动态属性以及 <u:map> 标记的 var 和 scope 属性的支持:
<taglib ...> ... <tag> <name>map</name> <tag-class>jsputils.tags.MapTag</tag-class> <body-content>empty</body-content> <attribute> <name>var</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <dynamic-attributes>true</dynamic-attributes> </tag> ... </taglib>
在 JSP 页面中使用 Map 对象
MapTest.jsp 页面使用 <u:map> 标记创建两个包含某个虚拟网站所支持的语言和版本的 Map 对象(langMap 和 versionMap)。然后,该页面使用 JSTL 的 <c:redirect> 标记将用户重定向到 MapTest2.jsp:
<!-- MapTest.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <u:map var="langMap" scope="application" en="English" de="Deutsch" fr="Fraais" it="Italiano" es="Espol"/> <c:set var="defaultLang" scope="application" value="en"/> <u:map var="versionMap" scope="application" html="HTML" java="Java" flash="Flash"/> <c:set var="defaultVersion" scope="application" value="html"/> <c:redirect url="MapTest2.jsp"/>
如果 session 范围中尚未存在此 JSP 变量,则 MapTest2.jsp 页面将再创建一个 Map 对象 (prefMap)。这三个 Map 对象(包含语言、版本和用户首选项)用于创建一个简单的 HTML 表单,允许用户更改他们的首选语言和站点版本:
<!-- MapTest2.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <c:if test="${prefMap == null}"> <u:map var="prefMap" scope="session" lang="${defaultLang}" version="${defaultVersion}"/> </c:if> <form method="post" action="MapTest3.jsp"> <p>Language:<br> <select name="lang" size="1"> <c:forEach var="lang" items="${langMap}"> <c:set var="selected" value=""/> <c:if test="${lang.key == prefMap.lang}"> <c:set var="selected" value="selected"/> </c:if> <option value="${lang.key}" ${selected}> ${lang.value} </option> </c:forEach> </select> <p>Version:<br> <c:forEach var="version" items="${versionMap}"> <c:set var="checked" value=""/> <c:if test="${version.key == prefMap.version}"> <c:set var="checked" value="checked"/> </c:if> <input type="radio" name="version" ${checked} value="${version.key}"> ${version.value} <br> </c:forEach> <p> <input type="submit" value="Save"> </form>
以下是由 MapTest2.jsp 生成的 HTML 表单:
<form method="post" action="MapTest3.jsp"> <p>Language:<br> <select name="lang" size="1"> <option value="en" selected> English </option> <option value="de" > Deutsch </option> <option value="fr" > Fraais </option> <option value="it" > Italiano </option> <option value="es" > Espol </option> </select> <p>Version:<br> <input type="radio" name="version" checked value="html"> HTML <br> <input type="radio" name="version" value="java"> Java <br> <input type="radio" name="version" value="flash"> Flash <br> <p> <input type="submit" value="Save"> </form>
MapTest3.jsp 将用户选择的首选项保存到 prefMap 变量中并显示用户首选项:
<!-- MapTest3.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <u:map var="prefMap" scope="session" lang="${param.lang}" version="${param.version}"/> <p>User Preferences: <ul> <li>Language:${prefMap.lang}</li> <li>Version:${prefMap.version}</li> </ul> <form method="post" action="MapTest2.jsp"> <p> <input type="submit" value="Change"> </form>
协调标记
JSTL 的 <sql:param> 标记与 <sql:query> 和 <sql:update> 配合使用,来传递所执行 SQL 语句的参数。本文使用了同一技巧来实现两个定制标记(<u:list> 和 <u:item>),它们协同在 JSP 页面中创建一个 java.util.ArrayList。<u:list> 标记可以是一个或多个包含此列表元素的 <u:item> 标记的父标记,如下例所示:
<u:list var="services"> <u:item> E-Mail </u:item> <u:item> Web Hosting </u:item> <u:item> E-Commerce </u:item> </u:list>
<u:item> 标记不需要直接包含在 <u:list> 中。您可以在 <u:list> 和 <u:item> 之间放置其他标记,如 <c:forEach>。这使您能够在循环中添加元素。如前一个代码片段所示,可以在 <u:item> 和 </u:item> 之间指定这些元素的值,也可以使用 <u:item> 的 value 属性指定:
<u:list var="selectedServices"> <c:forEach var="index" items="${paramValues.selected}"> <c:set var="paramName" value="service${index}"/> <u:item value="${param[paramName]}"/> </c:forEach> </u:list>
这些代码示例是 ListTest.jsp 和 ListTest2.jsp 页面的一部分,位于 ListTag 和 ItemTag 类之后。
实现协同工作的标记处理类
ListTag 处理类在其构造函数中创建 ArrayList 实例,并提供一个公共 add() 方法为列表添加元素。doTag() 方法调用所处理的 <u:list> 标记的主体并将列表导出为 JSP 变量:
package jsputils.tags; import javax.servlet.jsp.JspException; import java.util.ArrayList; import java.io.IOException; public class ListTag extends VarTagSupport { private ArrayList list; public ListTag() { list = new ArrayList(); } public void add(Object item) { list.add(item); } public void doTag() throws JspException, IOException { getJspBody().invoke(null); export(list); } }
<u:item> 标记由 ItemTag 类处理,后者的 doTag() 方法使用 findAncestorWithClass() 定位 ListTag 实例 — 该实例处理最近的 <u:list> 祖先标记。该列表元素被传递给祖先标记的处理类的 add() 方法:
package jsputils.tags; import javax.servlet.jsp.JspException; public class ItemTag extends VarTagSupport { private Object itemValue; public void setValue(Object value) throws JspException { itemValue = value; } public void doTag() throws JspException { ListTag ancestor = (ListTag) findAncestorWithClass( this, ListTag.class); if (ancestor == null) throw new JspException( "Couldn't find 'list' ancestor for 'item'"); if (itemValue == null) itemValue = invokeBody(); ancestor.add(itemValue); } }
同其他常规标记一样,这两个标记在 TLD 文件中声明:
<taglib ...> ... <tag> <name>list</name> <tag-class>jsputils.tags.ListTag</tag-class> <body-content>scriptless</body-content> <attribute> <name>var</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> <tag> <name>item</name> <tag-class>jsputils.tags.ItemTag</tag-class> <body-content>scriptless</body-content> <attribute> <name>value</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> </taglib>
在 JSP 页面中使用 List 对象
ListTest.jsp 页面创建一个包含服务的列表,并使用它生成一个 HTML 表单,该表单允许用户选择一个或多个服务:
使用以下资源测试这些示例并了解有关 JSP 2.0 简单标记 API 的更多信息。 下载源代码。jsptags_src.zip 文件包含本文的示例:Java 类均位于 jsputils 目录中;jspelapi 是一个 Java Web 应用程序。要运行这些示例,需要 J2SE、J2EE 1.4 应用服务器和 JSTL 1.1。 阅读“创建 JSP 2.0 标记文件”。Andrei Cioroianu 展示了如何创建和使用标记文件,以及如何将现有页面片段变换为标记文件。他使用 JSTL 和几个高级 JSP 特性构建用于更新和查询数据库的标记文件。 阅读《使用 JSP 2.0 EL API》。Andrei Cioroianu 演示了如何动态求解 JSP 表达式、如何在 XML 配置文件中使用表达式语言以及如何在显示 SQL 结果集时优化 EL 的使用。 下载 OC4J 10g。OC4J 10g 开发人员预览版 2 完全采用 J2EE 1.4 规范(包括 JSP 2.0)。可以使用 OC4J 10g (10.0.3) 来测试这些示例。 下载 JSTL 1.1。部署 jsptags Web 应用程序之前,下载 JSTL 并将文件 jstl.jar 和 standard.jar 复制到 jsptags/WEB-INF/lib 目录中。 阅读 JSP 2.0 规范。JSP 2.0 规范有整个一章(“第 1 部分:第 JSP.7 章 - 标记扩展”)是专门为要构建定制标记库的开发人员准备的。简单标记 API 和标准标记 API 在另一章(“第 2 部分:第 JSP.13 章 - 标记扩展 API”)中进行了介绍。 JSP 示例和教程。JSP 示例代码 |
<!-- ListTest.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <u:list var="services"> <u:item> E-Mail </u:item> <u:item> Web Hosting </u:item> <u:item> E-Commerce </u:item> </u:list> <form method="post" action="ListTest2.jsp"> <p> Services:<br> <c:forEach var="index" begin="${0}" end="${fn:length(services)-1}"> <input type="checkbox" checked name="selected" value="${index}"> <c:out value="${services[index]}"/> <input type="hidden" name="service${index}" value="${services[index]}"> <br> </c:forEach> <p> <input type="submit" value="Select"> </form>
以下是由 ListTest.jsp 生成的 HTML 表单:
<form method="post" action="ListTest2.jsp"> <p> Services:<br> <input type="checkbox" checked name="selected" value="0"> E-Mail <input type="hidden" name="service0" value=" E-Mail "> <br> <input type="checkbox" checked name="selected" value="1"> Web Hosting <input type="hidden" name="service1" value=" Web Hosting "> <br> <input type="checkbox" checked name="selected" value="2"> E-Commerce <input type="hidden" name="service2" value=" E-Commerce "> <br> <p> <input type="submit" value="Select"> </form>
ListTest2.jsp 页面处理用户提交的表单数据,创建并显示选定服务的列表:
<!-- ListTest2.jsp --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="u" uri="/WEB-INF/util.tld" %> <u:list var="selectedServices"> <c:forEach var="index" items="${paramValues.selected}"> <c:set var="paramName" value="service${index}"/> <u:item value="${param[paramName]}"/> </c:forEach> </u:list> <p> Selected Services:<br> <ul> <c:forEach var="service" items="${selectedServices}"> <li><c:out value="${service}"/></li> </c:forEach> </ul>
总结
无论 JSTL 和其他标记库提供了多少标记,总会有一些情形需要开发自己的标记。本文演示了如何使用简单标记 API 以及在开发定制标记时所需的基本技巧。此外,还提供了几个可以供您在 Web 应用程序中重复使用的标记。
Andrei Cioroianu (devtools@devsphere.com) 是 Devsphere (www.devsphere.com) 的创始人,那是一家 Java 框架、XML 咨询和 Web 开发服务的供应商。Cioroianu 编写了许多 Java 文章,这些文章由 ONJava (www.onjava.com)、JavaWorld (www.javaworld.com) 和 Java Developer's Journal 出版。他还与别人合著了 Java XML Programmer's Reference 和 Professional Java XML 两本书(均由 Wrox Press 出版)。