JSP自定义标签手把手教

1.想要在 JavaServer Pages (JSP) 应用程序中添加自定义标签吗?本教程将为您展示如何用这些标签编写类似于 JSP 技术自带操作 —— 如 jsp:useBean、jsp:getProperty 和 jsp:forward —— 的自定义操作。介绍如何用特定于自已的域的表示逻辑的自定义操作来扩展 JSP 语法。

在 JSP 应用程序中添加 自定义标签 的能力可以使您将工作重点放到以文档为中心的开发方式上。可以使 Java 代码不出现在 JSP 页中,从而使这些页面更容易维护。(我从经验中学到,在 JSP 页中放入过多的 Java 代码时,代码维护就会成为可怕的任务)。本教程将使您可以立即开发出自定义标签。了解了 JSP 自定义标签开发的好处后,您可能会对程序员没有更多地使用它而感到意外。
在本教程中,我将讨论使用自定义标签的基本内容。将介绍如何用自定义标签创建可重用的表示组件并避免在 JSP 页加入 Java scriptlet。 我们将: 定义一个 JSP 自定义标签体系结构。 解释简单标签。 定义嵌套标签。 用 BodyContent 解释标签。 在标签中添加属性。 在标签中添加 scriptlet 变量。 用自定义标签实现控制流程。 用 Struts 简化标签部署。 如果发现自己在 JSP 应用程序中加入了大量 Java scriptlet,那么本教程就是为您准备的。 阅读本教程后,就会掌握将 Java 代码从 JSP 页面中清除出去所需要的信息。
本教程假定读者熟悉 Java 平台、JavaServer Pages (JSP) 技术、MVC 模式、Reflection API、Model 2,最好还有 Struts 框架。此外,要从本教程中得到最大的收获,还需要很好的使用标签库的经验关于作者Rick Hightower 是一位 J2EE 开发人员和顾问,他热衷于使用 J2EE、Ant、Hibernate、Struts、IMB 的 ETTK 和 Xdoclet。 Rick 是 Trivera Technologies 的前任 CTO,这是一家全球培训、指导和咨询公司,其重点是企业开发。他经常在 IBM developerWorks 上发表文章,并编写了 10 多篇 developerWorks 教程,内容从 EJB 技术到 Web 服务到 XDoclet。 Rick 不久前与别人共同开办了另一家名为 ArcMind 的公司,它专门研究 agile 方法,还从事 Struts/JavaServer Faces 开发、咨询和指导。
在为 eBlox 工作时,Rick 和 eBlox 小组远在 1.0 版本之前就已使用 Struts 为电子商务站点构建了两个框架和一个 ASP (应用程序服务提供者)。这个框架目前正在为 2000 多个在线商店店面提供支持。
Rick 最近完成了一本名为 Professional Jakarta Struts 的书。在周游全国对 J2EE 和 Struts 项目提供咨询,或者在大会上发表关于 J2EE 和极端编程 (extreme programing)的讲演之余,Rick 喜欢在通宵咖啡店喝咖啡,写一些有关 Struts、J2EE 和其他内容的文章,并以第三人称描写他自己。
[color=red][/color]标签处理程序
在创建自定义标签之前,需要创建一个 标签处理程序。标签处理程序是一个执行自定义标签操作的 Java 对象。在使用自定义标签时,要导入一个 标签库 —— 即一组标签/标签处理程序对。通过在 Web 部署描述符中声明库导入它,然后用指令 taglib 将它导入 JSP 页。
如果 JSP 容器在转换时遇到了自定义标签,那么它就检查 标签库描述符(tag library descriptor) (TLD) 文件以查询相应的标签处理程序。TLD 文件对于自定义标签处理程序,就像 Web 部署描述符对于 servlet 一样。
在运行时,JSP 页生成的 servlet 得到对应于这一页面所使用的标签的标签处理程序的一个实例。生成的 servlet 用传递给它的属性初始化标签处理程序。
标签处理程序实现了 生存周期 方法。生成的 servlet 用这些方法通知标签处理程序应当启动、停止或者重复自定义标签操作。生成的 servlet 调用这些生存周期方法执行标签的功能。
[color=red][/color]标签的类型
可以定义两种类型的标签:
javax.servlet.jsp.tagext.Tag
javax.servlet.jsp.tagext.BodyTag
对 正文 进行操作 —— 即对在开始和结束标签之间的内容进行操作的 —— 标签必须实现 BodyTag 接口。在这个教程中,我们将称这些标签为 正文标签。我们将不对其正文操作的标签称为 简单标签。简单标签可以实现 Tag 接口,尽管不要求它们这样做。要记住不对其正文操作的标签仍然 有 正文,只不过,它的标签处理程序不能读取这个正文。
[color=red][/color]简单标签的例子
Struts 框架带有几个自定义标签库(有关 Struts 的更多信息的链接请参阅 参考资料 )。这些库中的一个标签可以创建一个支持改写 URL 的链接并用 jsessionid 对改写的连接编码。
不过有一个问题:如果希望传递一组请求参数(如查询字符串),也许必须为此创建一个 Java scriptlet。真是乱!下面的清单 (search_results.jap) 展示了一个 JSP 页,它被迫加入了这样一个 scriptlet。
<%@ taglib uri="struts-html" prefix="html" %> <jsp:usebean id="deleteParams" class="java.util.HashMap"> <% deleteParams.put("id", cd.getId()); deleteParams.put("method","delete"); %> <html:link name="deleteParams" action="/deleteCD">delete </html:link> </jsp:usebean> search_results.jsp 创建一个 hashmap 并向这个 map 传递两个属性。在下面几小节,我们将创建一个不用 Java 代码完成这项工作的自定义标签。我们的标签将定义如下的一个 hashmap:
<map:mapdefine id="deleteParams"> <map:mapentry property="id" name="cd" id="id"> <map:mapentry value="delete" id="method"> </map:mapentry> <html:link name="deleteParams" action="/deleteCD">delete </html:link> </map:mapentry></map:mapdefine> 这将使我们可以容易地创建小型 map。

这个例子将展示几个关键概念,包括使用嵌套标签和定义 scriplet 变量。首先我将解释这个标签是如何工作的。然后在以后的几节中建立这些概念,并介绍如何编写这个标签的不同形式,使它们处理其正文并控制执行流程。
2.构建简单标签的步骤
让我们创建一个定义一个 HashMap scriptlet 变量的标签。为此,需要实现标签处理程序接口 (javax.servlet.jsp.tagext.Tag)。因此,我们要创建的第一个标签将是一个简单标签。这个标签将实例化一个 map。使用这个标签的开发人员可以指定要实例化的 map 的类型 —— HashMap、TreeMap、FastHashMap 或者 FastTreeMap。FastHashMap 和 FastTreeMap 来自 Jakarta Commons Collection library (有关链接请参阅 参考资料)。开发人员还可以指定标签所在的范围 —— 页、请求、会话还是应用程序范围。
要构建这个简单标签,我们需要完成以下步骤:
创建实现了 Tag 接口(准确地说是 javax.servlet.jsp.tagext.Tag)的标签处理程序类。
创建一个 TLD 文件。
在标签处理程序 Java 类中创建属性。
在 TLD 文件中定义与标签处理程序 Java 类中定义的属性对应的属性。
在 TLD 文件中声明 scriptlet 变量。
实现 doStartTag() 方法。在标签处理程序类中,根据属性将值设置到 scriptlet 变量中。
如果您像我一样,可能会提前阅读书的结尾,所以请查看 附录 中标签处理程序类的完整列表以了解这个过程是如何结束的。
在下面几小节中,我们将分析 MapDefineTag 的实现,并分析如何到达这一步。
第 1 步:创建一个实现了 Tag 接口的标签处理程序
为了编写标签处理程序,必须实现 Tag 接口。如前所述,这个接口用于不操纵其标签正文的简单标签处理程序。就像 J2EE API 文档 (有关链接请参阅 参考资料)所说的:Tag 接口定义了标签处理程序和 JSP 页实现类之间的基本协议。它定义了在标签开始和结束时调用的生存周期和方法。
标签处理程序接口有以下方法:
方法 作用
int doStartTag() throws JspException 处理开始标签
int doEndTag() throws JspException 处理结束标签
Tag getParent()/void setParent(Tag t) 获得/设置标签的父标签
void setPageContext(PageContext pc) pageContext 属性的 setter 方法
void release() 释放获得的所有资源
TagSupport
现在,不必直接实现 Tag 接口,相反,用 map 定义的(map-defining)标签将继承 TagSupport 类。这个类以有意义的默认方法实现 Tag 接口,因而使开发自定义标签更容易 (有关 TagSupport 的 API 文档的链接请参阅 参考资料)。 例如,TagSupport 类定义了 get/setParent() 和 setPageContext(),这与所有标签处理程序几乎相同。 get/setParent() 方法允许标签嵌套。TagSupport 类还定义了一个可以被子类使用的 pageContext 实例变量 (protected PageContext pageContext),这个变量是由 setPageContext() 方法设置的。
在默认情况下,TagSupport 实现了 doStartTag() 以使它返回 SKIP_BODY 常量,表示将不对标签正文进行判断。 此外,在默认情况下,doEndTag() 方法返回 EVAL_PAGE,它表示 JSP 运行时引擎应当对页面的其余部分进行判断。 最后,TagSupport 实现了 release(),它设置 pageContext 及其父元素为 null。
TagSupport 类还实现了 IterationTag 接口和 doAfterBody(),这样它就返回 SKIP_BODY。 在后面讨论进行迭代的标签时我将对此加以更详细的解释(请参阅 用自定义标签控制流程)。 好了,现在让我们通过继承 TagSupport 来实现 Tag 接口:
... import javax.servlet.jsp.tagext.TagSupport; ... public class MapDefineTag extends TagSupport { ... 我们已经定义了标签处理程序,现在需要增加从处理程序到 TLD 文件中的标签的映射。我们将在下一小节中对此进行处理。然后,将完成 MapDefineTag 中剩余的代码。
第 2 步:创建一个 TLD 文件
TLD 文件对自定义标签处理程序的作用就像 Web 部署描述符对 servlet 的作用。 TLD 文件列出了从标签名到标签处理程序的映射。 这个文件中的大多数数据都是在 JSP 页转换时使用的。 TLD 文件通常保存在 Web 应用程序的 WEB-INF 目录,并在 web.xml 文件中声明。它们一般用 .tld 扩展名结束。
TLD 文件有一个 导言(preamble),在这里标识 JSP 技术的版本和使用的标签库。这个导言通常看起来像这样:
<taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>map</short-name> </taglib> 让我们更详细地分析一下这些标签:TLD 文件的根元素是 taglib。taglib 描述了一个 标签库 —— 即一组标签/标签处理程序对。
因为我们使用的是 JSP 版本 1.2,所以在这个例子中需要 tlib-version 和 short-name 元素。tlib-version 元素对应于标签库版本。
jsp-version 对应于标签库所依赖的 JSP 技术的版本。
short-name 元素定义了 IDE 和其他开发工具可以使用的标签库的简单名。
taglib 元素包含许多 tag 元素,标签库中每一个标签有一个 tag 元素。
因为我们刚创建了自己的类,所以我们将继续往下进行,在 TLD 文件中声明这个类,如下所示:
<taglib> ... <tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ... </tag></taglib> tag 元素用于将自定义标签映射到它们的自定义标签处理程序。上述清单中的 tag 元素将自定义标签 mapDefine 映射到处理程序 trivera.tags.map.MapDefineTag。 因此,不论在 mapDefine 上运行的是什么转换引擎,都会调用 trivera.tags.map.MapDefineTag。
已经在 TLD 中定义了标签,接下来要在标签处理程序类中定义这个标签的一些属性了
第 3 步:在标签处理程序 Java 类中创建属性
我们希望为 mapDefine 标签指定三个属性,如下所示:
属性说明
id 新 scriptlet 变量的名字。
scope 新 scriptlet 变量所在的范围。
type 新 scriptlet 变量的类型 (HashMap、FastHashMap、TreeMap 或者 FastTreeMap)。 如果 type 设置为 hash,那么就会创建一个 HashMap。如果 type 设置为 fasthash,那么将创建 FastHashMap。
在 JSP 页中使用这个标签时,它看起来将像下面这样:
<map:mapdefine type="hash" scope="session" id="editParams"> ... </map:mapdefine> 这个标签将在会话范围内创建一个名为 editParams 的 HashMap。
为了在标签处理程序中创建属性,需要定义相应的 JavaBean 属性。 因此,每一个属性在标签处理程序中都有对应的 setter 方法,如下所示:
public class MapDefineTag extends TagSupport { ... private String type = FASTTREE; private String id; private String scope; public void setType(String string) { type = string; } public void setId(String string) { id = string; } public void setScope(String string) { scope = string; } 转换引擎将用硬编码的配置数据或者运行时表达式设置这个标签的属性。 我第 4 步:在 TLD 文件中定义属性
就 像上一小节中所做的那样,通过声明 JavaBean 属性定义自定义属性,然后在 TLD 文件中声明这些属性。 每一个 JavaBean 属性都必须与相应的自定义标签属性相匹配。 在 TLD 中定义的属性必须匹配 JavaBean 属性,不过却可以有与标签属性不匹配的 JavaBean 属性。

下面是 MapDefineTag 的属性声明:

<tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ... <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> <description>The id attribute</description> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description>The scope attribute</description> </attribute> <attribute> <name>type</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description> Specifies the type of map valid values are fasttree, fasthash, hash, tree </description> </attribute> </tag> name 元素指定属性的名字。required 元素指定属性是否是必需的(默认值是 false)。rtexprvalue 元素表明属性是硬编码了转换时的值还是允许使用运行时 scriptlet 表达式。

记住,MapDefineTag 类必须为前面描述的每一个属性定义一个 JavaBean 属性,我们在 第 3 步:在标签处理程序 Java 类中创建属性 中完成这个任务。
第 5 步:实现 doStartTag() 方法 中,我们将在标签处理程序的 doStartTag() 方法中使用这些属性。实现 doStartTag() 方法
标签开始时调用 doStartTag() 方法 —— 从开发人员的角度看,这是当引擎遇到 <map:mapdefine></map:mapdefine> 时发生的。如果 doStartTag() 返回 SKIP_BODY,那么将不处理标签正文。 如果它返回一个 EVAL_BODY_INCLUDE,那么将处理正文。
MapDefine 类的 doStartTag() 方法完成以下工作:
根据 type 属性确定要创建的 map 的属性。
根据 scope 属性确定新的 map 对象放在什么范围内。
根据 id 属性确定新 map 对象要放入的范围的名字。
让我们更详细地分析这个过程。MapDefine 类检查 type 属性是设置为 FASTTREE、HASH、TREE 还是 FASTHASH。然后创建相应的 map,如下所示:
/* String constants for the different types of maps we support */ public static final String FASTHASH = "FASTHASH"; public static final String FASTTREE = "FASTTREE"; public static final String HASH = "HASH"; public static final String TREE = "TREE"; /** The map we are going to create */ private Map map = null; /** The member variable that holds the type attribute */ private String type = FASTTREE; ... public int doStartTag() throws JspException { /** Based on the type attribute, determines which type of Map to create */ if (type.equalsIgnoreCase(FASTTREE)) { map = new FastTreeMap(); } else if (type.equalsIgnoreCase(HASH)) { map = new HashMap(); } else if (type.equalsIgnoreCase(TREE)) { map = new TreeMap(); } else if (type.equalsIgnoreCase(FASTHASH)) { map = new FastHashMap(); } 然后,用 id 和 scope 属性将 hashmap 以一个给定的名字设置到一个给定范围中:
private String id; private String scope; public int doStartTag() throws JspException { ... if (scope == null){ pageContext.setAttribute(id, map); }else if("page".equalsIgnoreCase(scope)){ pageContext.setAttribute(id, map); }else if("request".equalsIgnoreCase(scope)){ pageContext.getRequest().setAttribute(id, map); }else if("session".equalsIgnoreCase(scope)){ pageContext.getSession().setAttribute(id, map); }else if("application".equalsIgnoreCase(scope)){ pageContext.getServletContext().setAttribute(id, map); } return EVAL_BODY_INCLUDE; } 如果范围属性是 null,那么 map 将放入页范围。否则,参数将放入通过 scope 属性传递的范围名所对应的范围中。
到目前为止,我们已经有一个非常简单的标签,它有三个属性:id、scope 和 type。 我们将用给定的名字将 map 放到一个范围中。但是,我们还有一件事没做,就是声明 scriptlet 变量。 为了做到这一点,需要向 TLD 再添加一项,这就是我们在下一小节中所要做的事。
第 6 步:声明 scriptlet 变量
要 理解 scriptlet 变量,必须理解 TLD 文件的作用。这个文件基本上是元数据的一个储存库,当 JSP 页转换为 servlet 时,标签会使用这些元数据。 在生成的 servlet 中,scriptlet 变量成了本地变量。 要让 JSP 转换引擎知道这些变量应当声明的类型,需要像下面这样在 TLD 文件中增加项:
<variable> <name-from-attribute>id</name-from-attribute> <variable-class>java.util.Map</variable-class> <scope>AT_BEGIN</scope> </variable> 将上面的代码片断放在 TLD 文件中 body-content 元素之后、attribute 元素之前。 在 variable 元素下面,我们声明了三个子元素:name-from-attribute、variable-class 和 scope。 name-from-attribute 指定 id 属性的值是转换引擎将要定义的 scriptlet 变量的名字。variable-class 是转换将要定义的变量的类类型。scope 指定变量什么时候可用:它可以嵌套到标签的正文中 (NESTED)、在标签结束后 (AT_END)、或者在标签的开始时 (AT_BEGIN)。我们使用 AT_BEGIN 这一范围,它意味着变量的范围将是从标签的开始到当前 JSP 页的结束。
现在了解了如何构建简单自定义标签。在下一小节中,我们将分析标签的生存周期方法,以了解 JSP 页实际运行时所发生的情况。
简单标签的生存周期概述

如果像我第一次使用标签时那样,您可能发现不容易将转换引擎生存周期与运行时实际发生的事情对应起来。 像我一样,您可能看到很多试图说明这些概念的图表,这些图表看起来可能像下面这样。

这里是已最小化的图像

这个图表对我没什么意义,但是生成的代码有意义,所以我们将在这一小节中分析这些代码。(现在我觉得这些图表有意义了,但是我认为这些代码更能说明问题,在我们完成这一小节时,您可能想再次分析这个图表。)

您可能还记得,JSP 页实际上是伪装的 servlet。JSP 文件在使用之前转换为 servlet。 下面是一个名为 testMapDefine.jsp 的小型 JSP 页,我们将用它来展示并测试在前面一节中开发的 MapDefineTag 标签:

<%@taglib uri="map" prefix="map"%> <map:mapdefine id="employee"> The employee is <%=employee%> <% employee.put("firstName", "Kiley"); employee.put("lastName", "Hightower"); employee.put("age", new Integer(33)); employee.put("salary", new Float(22.22)); %> The employee is <%=employee%> </map:mapdefine> 注意这一页用 map 的 URI 导入了一个自定义标签。之所以能这样是因为我们在 web.xml 文件中声明了这个 TLD,如下所示:

<web-app> ... <taglib> <taglib-uri>map</taglib-uri> <taglib-location>/WEB-INF/tlds/map.tld</taglib-location> </taglib> ... </web-app> 有另一种导入标签的方法。我认为这个方法更有用,因为它允许为 URI 指定一个短名,而短名容易记忆。 注意 taglib 是在其 TLD 文件上下文中描述的。在 WEB-INF 目录中的 map.tld 文件看起来像下面这样:

<taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>map</short-name> <tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ... </tag></taglib> 当 JSP 转换器遇到自定义标签 mapDefine 时,它将根据 taglib 指令和在 web.xml 文件中指定的 TLD 文件查询 TLD 文件。然后在生成的 servlet 中加入下面的代码:

public class _testmapdefine__jsp extends ...JavaPage{ public void _jspService(HttpServletRequest request, HttpServletResponse response) throws...{ trivera.tags.map.MapDefineTag tag0 = null; java.util.Map employee = null; try { ... if (tag0 == null) { tag0 = new trivera.tags.map.MapDefineTag(); tag0.setPageContext(pageContext); tag0.setParent((javax.servlet.jsp.tagext.Tag) null); tag0.setId("employee"); } int includeBody = tag0.doStartTag(); if (includeBody != javax.servlet.jsp.tagext.Tag.SKIP_BODY) { employee = (java.util.Map)pageContext.findAttribute("employee"); out.print(" \n The employee is "+ (employee) +" \n"); employee.put("firstName", "Kiley"); employee.put("lastName", "Hightower"); employee.put("age", new Integer(33)); employee.put("salary", new Float(22.22)); out.print(" \n The employee is "+ (employee) +" \n"); } employee = (java.util.Map)pageContext.findAttribute("employee"); ... } catch (java.lang.Throwable _jsp_e) { pageContext.handlePageException(_jsp_e); ... } ... } 生成的 JSP servlet 声明了名为 tag0、类型为 trivera.tags.map.MapDefineTag 的一个本地变量。 然后它创建标签的一个实例、设置页上下文、设置父标签为 null,并设置 ID 为 employee。然后,生成的 servlet 调用 doStartTag() 方法,它检查返回类型是否设置为 Tag.SKIP_BODY。如果是,那么容器就不对标签的正文(在这里就是 if 块)进行判断。如果它返回 EVAL_BODY_INCLUDE,就像我们的标签那样,容器就将处理正文。 可以用这种技术有条件地加入标签的正文 —— 即控制流程。

注意生成的 servlet 有一个名为 employee 的本地变量, 根据标签的 id 属性将它设置为 employee。因此,转换引擎在 转换 时而不是运行时定义了一个名为 employee 的本地变量。这个概念使很多新人感到迷惑。

理解嵌套标签

前面的 JSP 页示例使用 JSP scriptlet 向 employee map 添加项。如果可以用另一个标签来完成就好了。 让我们定义一个名为 MapEntryTag 的嵌套标签,它通过调用 getParent() 并将它转换为 MapDefineTag 而得到其父标签。

MapDefineTag 定义了 getMap() 方法,它返回新创建的 map。 doEndTag() 中的嵌套 MapEntryTag 使用 MapDefineTag 的 getMap() 方法向 map 中增加值,如下所示:

public class MapEntryTag extends TagSupport { String type = "java.lang.String"; String id; String value; String name; String property; String scope; public int doEndTag() throws JspException { /* Grab the MapDefineTag using the getParent method and cast it to a MapDefineTag.*/ MapDefineTag mapDef = (MapDefineTag) this.getParent(); Object objectValue = null; ... /* Instantiate a new String, Integer or Float based on the type. */ if (type.equals("java.lang.String")) { objectValue = value; } else if (type.equals("java.lang.Integer")) { Integer intValue = Integer.valueOf(value); objectValue = intValue; } else if (type.equals("java.lang.Float")) { Float floatValue = Float.valueOf(value); objectValue = floatValue; } /* Put the new entry into the map. */ mapDef.getMap().put(id,objectValue); return EVAL_PAGE; 在上述代码的基础上,并假定在 TLD 文件中加入了必要的项,则可以像这样使用我们的标签:

<%@taglib uri="map" prefix="map"%> <map:mapdefine id="employee"> <map:mapentry value="Jennifer" id="firstName"> <map:mapentry value="Wirth" id="lastName"> <map:mapentry type="java.lang.Integer" value="33" id="age"> <map:mapentry type="java.lang.Float" value="22.22" id="salary"> </map:mapentry> The employee is set as <%=employee%> </map:mapentry></map:mapentry></map:mapentry></map:mapdefine> 上述清单定义一个 employee map,其中带有针对 firstName、lastName、age 和 salary 各三项,它们的类型分别为 String、String、Integer 和 Float。
用 Reflection 将 beann 属性提取为值 <nobr>第 2 页(共3 页)</nobr>


开发人员通常使用 Java Reflection 改进自定义标签代码。 在这一小节中,我们将用 Reflection 改写 MapEntryTag,使它可以使用所有范围内的所有 bean 属性定义 map 中的项。 例如,假设有像这样的一个 bean:

public class Test { String test="Jenny"; public String getTest() { return test; } public void setTest(String string) { test = string; } } 改写后的标签可以将 bean 属性作为 map 中的项使用,像下面这样:

<jsp:usebean id="bean" class="trivera.tags.map.Test"></jsp:usebean> <map:mapdefine id="employee2"> <map:mapentry property="test" name="bean" id="firstName"></map:mapentry> <map:mapentry value="33" type="java.lang.Integer" id="age"></map:mapentry> <map:mapentry value="22.22" type="java.lang.Float" id="salary"></map:mapentry> <map:mapentry name="properties" id="properties"></map:mapentry> </map:mapdefine> 注意 firstName 项现在是用 bean 的 test 属性定义的。

为了做到这一点,我们需要在自定义标签中加入 Relection,像这样(参看代码中的注释以了解改变的过程):

public class MapEntryTag extends TagSupport {... /* All of these have corresponding getters and setter method */ String type = "java.lang.String"; //Holder for the type attribute String id; //Holder for the id attribute String value; //Holder for value attribute String name; //Holder for name attribute String property; //Holder for property attribute String scope; //Holder for scope attribute public int doEndTag() throws JspException { MapDefineTag mapDef = (MapDefineTag) this.getParent(); Object objectValue = null; /* Check to see if the value property is set, if it is then this is a simple entry */ if (value !=null){ if (type.equals("java.lang.String")) { objectValue = value; } else if (type.equals("java.lang.Integer")) { Integer intValue = Integer.valueOf(value); objectValue = intValue; } else if (type.equals("java.lang.Float")) { Float floatValue = Float.valueOf(value); objectValue = floatValue; } /* If it is not a simple entry, then use reflection to get the property from the bean */ }else { Object bean =null; if (scope == null){ bean = pageContext.findAttribute(name); }else if("page".equalsIgnoreCase(scope)){ bean = pageContext.getAttribute(name); }else if("request".equalsIgnoreCase(scope)){ bean = pageContext.getRequest().getAttribute(name); }else if("session".equalsIgnoreCase(scope)){ bean = pageContext.getSession().getAttribute(name); }else if("application".equalsIgnoreCase(scope)){ bean = pageContext.getServletContext().getAttribute(name); } /* If the property attribute is null, then just use the bean as the entry*/ if (property==null){ objectValue = bean; mapDef.getMap().put(id,bean); /* If the property attribute is set, then use reflection to read the property */ }else { try{ String propertyMethod = "get" + property.substring(0,1).toUpperCase() + property.substring(1, property.length()); Method prop = bean.getClass() .getMethod(propertyMethod,new Class[]{}); objectValue = prop.invoke(bean, new Object[]{}); }catch(Exception e){ throw new RuntimeException(e); } } } mapDef.getMap().put(id,objectValue); return EVAL_PAGE; } 看起来仅仅是为了实现一些很多标签都要有的功能就需要做大量的工作。幸运的是,有一个库使这种开发变得容易了。在下一小节分析这个库 —— Struts —— 是如何发挥作用的。


用 Struts 简化自定义标签开发 <nobr>第 3 页(共3 页)</nobr>


对 Struts 的深入讨论超出了本教程的范围(有关这个框架的更多信息请参阅 参考资料)。不过,如果熟悉这个框架,那么这些知识会对开发自定义标签有帮助。

也可以不使用 Reflection,而使用 Struts 的 RequestUtils 实现在上一小节看到的结果,如下所示:

public class MapEntryTag extends TagSupport {... private String type = "java.lang.String"; ... public int doEndTag() throws JspException { MapDefineTag mapDef = (MapDefineTag) this.getParent(); Object objectValue = null; if (value !=null){ if (type.equals("java.lang.String")) { objectValue = value; } else if (type.equals("java.lang.Integer")) { Integer intValue = Integer.valueOf(value); objectValue = intValue; } else if (type.equals("java.lang.Float")) { Float floatValue = Float.valueOf(value); objectValue = floatValue; } }else { /** THIS USED TO BE 30 LINES OF CODE */ objectValue = RequestUtils.lookup(pageContext, name, property, scope); } mapDef.getMap().put(id,objectValue); return EVAL_PAGE; } 可以看到,objectValue = RequestUtils.lookup(pageContext, name, property, scope); 这一行代替了使用 Reflection 的代码中的 30 行! Struts 自带了许多实用程序,使自定义标签的开发更容易了。


介绍 <nobr>第 1 页(共3 页)</nobr>


可以编写标签处理程序对其 正文内容 进行操作。记住,标签的正文内容是 JSP 页中出现在自定义标签的开始和结束标签之间的数据。操纵其正文的标签称为 正文标签。编写正文标签处理程序比简单标签处理程序要复杂。

注意: 记住,简单标签也可以有正文。惟一的不同是简单标签不能读取或者操纵其正文。

要编写正文标签处理程序,必须实现 BodyTag 接口。BodyTag 实现了 Tag 实现的所有方法 (详见 第 1 步:创建一个实现了 Tag 接口的标签处理程序 ),而且还实现了另外两个处理正文内容的方法:


方法 目的
void setBodyContent(BodyContent b) bodyContent 属性的 Setter 方法。
void doInitBody() 准备对正文进行判断。每次调用标签时,在获得新的 BodyContent 并通过 setBodyContent() 对其进行设置之后调用一次。 如果没有请求正文内容就不调用,因为 doStartTag() 返回 EVAL_BODY_BUFFERED。


就像 Tag 接口有 TagSupport 类一样,BodyTag 接口有 BodyTagSupport 类。 因此,正文标签处理程序只需要覆盖它们要使用的方法。BodyTagSupport 类继承了 TagSupport 并实现了 BodyTag 接口。这使得编写正文标签处理程序更容易了。BodyTagSupport 定义了 get/setBodyContent() 和一个 protected bodyContent 实例变量。

BodyTagSupport 类重新定义了 doStartTag() 生存周期方法以返回 EVAL_BODY_BUFFERED。 通过返回 EVAL_BODY_BUFFERED,doStartTag() 请求创建一个新的缓冲区 —— 即一个 BodyContent。

BodyContent 是一个包含运行时正文判断结果的缓冲区。 BodyContent 继承了 JspWriter 并作为标签正文的隐式 out。因此,JSP 容器创建 BodyContent 的一个实例,并且在处理标签的正文内容时,它写入这个实例而不是根 JspWriter 中。 因此,在标签中使用隐式对象 out 时,实际上使用的是 BodyContent 对象而非 JspWriter (JspWriter 是页的隐式 out )。

可以从 BodyContent 得到判断后的正文,它是一个 String。BodyContent 是在运行时由容器调用页的 pageContext 的 pushBody() 和 popBody() 方法创建的 (只有在 doStartTag() 返回 EVAL_BODY_BUFFERED 时才调用pushBody())。 因此,BodyContent 是在 JspWriter 和 BodyContent 的一个嵌套结构中的。 (外面的 out 可以是另一个 BodyContent 对象,因为 BodyContent 是一个 JspWriter。) 通过 setBodyContent() 方法将 BodyContent 提供给正文标签处理程序。 向正文标签处理程序传递一个 BodyContent 实例 (通过 setBodyContent()) 并可以决定如何处理它。 可以对它做进行一步处理、放弃它、将它发送给浏览器等。

已经介绍了足够的背景知识了,该分析代码了!在下面几小节中我们将分析一个简单正文标签的例子。


例子:map 标签 <nobr>第 2 页(共3 页)</nobr>


自定义标签的开发人员可以决定如何处理标签的正文。 例如,可以编写一个执行 SQL 语句的标签,标签的正文是要执行的 SQL 语句。

回到 hashmap 主题,我们将编写一个像下面这样解析字符串的标签:

{ firstName=Jennifer, lastName=Wirth, age=25 } 实际上,这个标签将读取这种字符串并将它转换为一个 Map(java.util.Map)。我们将调用新标签 map。下面是使用新标签的例子:

<%@taglib uri="map" prefix="map"%> <map:map id="employee"> { firstName=Jennifer, lastName=Jones, age=25 } </map:map> The employee map is <%=employee%> 上面的代码创建一个名为 employee 的 map,它有三项:firstName、lastName 和 age。

实现标签处理程序

MapParseTag 是一个 map 标签的标签处理程序。它根据传递给正文的字符串定义一个 map。在 doAfterBody() 方法中,MapParseTag 用 body.getString() 将正文内容抓取为字符串。然后用 body.clearBody() 清除正文内容。

public class MapParseTag extends BodyTagSupport { private String id; private Map map; public int doStartTag() throws JspException { map=new FastTreeMap(); return EVAL_BODY_BUFFERED; } public int doAfterBody() throws JspException { /* Grab the body content */ BodyContent body = this.getBodyContent(); /* Get the body content as a String */ String content = body.getString(); /* Clear the body */ body.clearBody(); /* Parse the map */ int start = content.indexOf("{"); int end = content.lastIndexOf("}"); content = content.substring(start+1, end); /* Parse the entries in the map */ StringTokenizer token = new StringTokenizer(content,"=;, \t\r\n"); while(token.hasMoreTokens()){ String key = token.nextToken(); String value = token.nextToken(); map.put(key,value); } this.pageContext.setAttribute(id,map); return SKIP_BODY; } 多简单!我们已经讨论了对其正文进行处理的标签,现在要详细讨论实现了执行流程的标签了。

概述 <nobr>第 1 页(共4 页)</nobr>


有时,可能希望根据一些特定于应用程序的显示逻辑有条件地调用方法的正文。可以通过从 doStartTag() 返回一个值做到这一点:SKIP_BODY 跳过标签的正文,而 EVAL_BODY 对它进行判断。

迭代标签需要实现 IterationTag 接口。容器调用 doAfterBody() 方法以确定是否要重新判断正文。这个方法返回 EVAL_BODY_AGAIN 时表明容器应当继续对正文进行判断。doAfterBody() 方法返回 SKIP_BODY 时表明迭代操作已经结束。TagSupport 类和 BodyTagSupport 类都实现了 IterationTag 接口。

回到 map 主题,在下面几小节我们将创建一个自定义标签处理程序,它迭代一组 map 并打印出它们的值。


控制流程序示例 <nobr>第 2 页(共4 页)</nobr>


在开始实现自定义标签处理程序之前,让我们看看示例标签是如何使用的。首先,用上面的例子中的 map 标签定义两个 map:

<%@taglib uri="map" prefix="map"%> <map:map id="employee1"> { firstName=Jennifer, lastName=Wirth, age=33 } </map:map> <map:map id="employee2"> { firstName=Kiley, lastName=McKeon, age=27 } </map:map> 然后,创建一个名为 list 的集合,加入上面定义的两个 map (employee1 和 employee2)。

<jsp:usebean id="list" class="java.util.ArrayList"></jsp:usebean> <% list.add(employee1); list.add(employee2); %> 下面展示了如何用自定义标签迭代这个集合:

<map:printmaps name="list"> </map:printmaps>First Name ${firstName} Last Name ${lastName} Age ${age} map:printMaps 标签迭代 list 集合。 在每一次迭代时,它使用其正文,通过搜索以 ${key} 开始的子字符,在 map 中搜索当前迭代的键。它从字符串 ${} 中解析出键,并用从 map 得到的这个键的值(即 map.get(key))替换它。


实现 doStartTag() 方法 <nobr>第 3 页(共4 页)</nobr>


doStartTag() 方法从范围中抓取集合,然后从集合中抓取迭代器。 如果迭代器中没有任何项(iter.hasNext()),那么 doStartTag() 方法就返回 SKIP_BODY,从而实现了逻辑 if。在这种情况下,这个 if 等同于“如果集合为空,则跳过正文判断”。 然后迭代器从集合中抓取第一个 map。

public class MapPrintMapsTag extends BodyTagSupport { private String name; private Iterator iter; private Map map; private String scope; public int doStartTag() throws JspException { Collection collection = null; /* Grab the collection out of scope using the scope attribute. */ if (scope == null){ collection = (Collection) pageContext.findAttribute(name); }else if("page".equalsIgnoreCase(scope)){ collection = (Collection) pageContext.getAttribute(name); }else if("request".equalsIgnoreCase(scope)){ collection = (Collection) pageContext.getRequest().getAttribute(name); }else if("session".equalsIgnoreCase(scope)){ collection = (Collection) pageContext.getSession().getAttribute(name); }else if("application".equalsIgnoreCase(scope)){ collection = (Collection) pageContext.getServletContext().getAttribute(name); } /* Get the iterator from the collection. */ iter = collection.iterator(); /* If the collection is empty skip the body evaluation. */ if (iter.hasNext()==false) return SKIP_BODY; /* Grab the first map out of the collection. */ map = (Map)iter.next(); return EVAL_BODY_BUFFERED; } 实现 doAfterBody() 方法 <nobr>第 4 页(共4 页)</nobr> doAfterBody() 通过在没有可迭代的项时返回 SKIP_BODY 实现了迭代。如果还有项,那么它就返回 EVAL_BODY_AGAIN。只要 doAfterBody() 返回 EVAL_BODY_AGAIN,容器就会继续对正文进行判断,如下所示: public int doAfterBody() throws JspException { /** Process body */ ... /** Write the processed buffer to the previous out */ ... if (iter.hasNext() == false) { return SKIP_BODY; } else { map = (Map) iter.next(); return EVAL_BODY_AGAIN; } }
doAfterBody() 方法用 getBodyContent() 抓取正文,然后用正文内容的 getString() 方法得到字符串形式的内容。 下一步它清除正文内容缓冲区,用 StringTokenizer 查找以 ${ 开始的字符串。 它还创建一个名为 buffer 的 StringBuffer。 当它迭代 tokenizer 中的字符串时,将它们附加到缓冲区中。
如果 tokenizer 中的字符串以 ${, doAfterBody() 开始,那么它就从字符串中提取键,并用这个键在 map 中查找值。它调用值对象的 toString() 方法将 map 中的值转换为字符串,并将结果附加到缓冲区中。下面的清单展示了这个过程是如何进行的。
/** Process body */ /* Get and clear the body */ BodyContent body = this.getBodyContent(); String content = body.getString(); body.clearBody(); /* Process the body with a String tokenizer */ StringTokenizer token = new StringTokenizer(content); /* Create an output buffer of the processed body */ StringBuffer buffer = new StringBuffer(content.length() * 2); /* Iterate over the strings from the tokenizer and put them into the output buffer. */ while (token.hasMoreTokens()) { String tok = token.nextToken(); /* See if the String contains the special substring "${" */ if (tok.startsWith("${")) { /* Parse the key out of the string */ String key = tok.substring(2, tok.length() - 1); /* Use the key to look up the object in the map */ Object value = (String) map.get(key); String svalue = tok; /* If the value is not null, get the value's string representation */ if (value != null) { svalue = value.toString(); } /* Add the string representation of the value to the output buffer */ buffer.append(svalue + " "); } else { buffer.append(tok + " "); } }
doAfterBody() 方法在构建了输出缓冲区后,就将它输出到前面的 out 中,如下所示:
try { this.getPreviousOut().print(buffer.toString()); } catch (IOException e) { throw new JspException(e); }
这样,每一次迭代都处理正文并将它输出到前面的 out 中。如果前面的 out 是根 JspWriter,那么它就会写到浏览器中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值