自定义 JSP 动作
在过去的几个月里,我们从多个角度研究了服务器端 Java。我们从 Servlet 开始,Servlet 是从 Servlet 容器内部执行的 Java 类。虽然程序员们对 Servlet 并不感到特别畏惧,但图形设计师可能会有不同的感受。
解决这个问题的是 JavaServer Pages (JSP),它结合了 Java 和 HTML,使用类似于微软的 Active Server Pages (ASP) 或用于 mod_perl 的开源 HTML::Mason 系统的语法。每个 JSP 实际上都是伪装的 Servlet;JSP 引擎将页面翻译成 Servlet,然后将 Servlet 编译成 Java .class 文件。
JSP 可以包含直接的 Java 代码,这可以使执行复杂操作变得更容易。但在某些时候,这些代码会淹没 HTML,使其无法维护 JSP。非程序员也会对 JSP 中大量代码感到反感,这在很大程度上违背了使用 JSP 而不是直接 Servlet 的目的。
上个月,我们研究了一种使用 JavaBean 避免在 JSP 中放置代码的方法。通过使用简单的基于 XML 的标签,非程序员可以组合出展现复杂行为的 JSP,而无需编写一行代码。实际上,JavaBean 的真正魔力不在于 bean 本身,而在于使我们能够如此轻松地使用它们的特殊标签。
本月,我们将学习如何编写我们自己的“自定义动作”,正如它们所知——基于 XML 的标签,使我们能够在不使用 Java 本身的情况下使用 Java 类和方法。我们的示例旨在与 Servlet 和 JSP 的开源 Jakarta-Tomcat 实现一起使用。但是,它们应该适用于任何使用自定义动作的 JSP 实现。
使用自定义标签有很多原因。首先,它们减少了我们必须放在 JSP 中的 Java 代码量,使其更易于阅读、理解和维护。此外,自定义标签比 Java 代码更简单,使其比 Java 代码用户更适合更广泛的受众。最后,每个自定义标签库都指向一个集中编写和维护的 Java 类。因此,使用自定义动作,站点可以创建适合其特定需求的标签库。一位 Java 程序员可以为一个或多个图形设计师和 JSP 作者创建和发布标签库。正如我们将看到的,自定义标签并非万能药,但它们可能非常有用,我认为它们是使用 JSP 而不是竞争技术的最大理由之一。
自定义动作为我们在 JSP 中复杂的 Java 代码提供了简写。您可以使用自定义动作完成的任何操作也可以使用 scriptlet (<% %>) 标签内的 Java 代码来完成。毕竟,JSP 在为最终用户编译和执行之前会被转换为 Servlet。
正如我们上个月在 JavaBean 标签中看到的那样,自定义动作是用 XML 而不是 HTML 定义的。这起初可能会令人困惑和沮丧,尤其是对于我们这些在编写 HTML 时养成了坏习惯的人来说。以下内容可能看起来是合法的
<P><jsp:getProperty name="simple" property="userID"></P>
但实际上,上面的行将不起作用,并且会在 JSP 中导致异常和堆栈跟踪。这是因为 XML 中的所有标签都必须以某种方式关闭。如果 <tag> 没有匹配的 </tag>,那么它必须使用 <tag/> 来指示它自身关闭。因此,上面的行实际上必须写成
<P><jsp:getProperty name="simple" property="userID"/></P>自定义动作只是 Java 方法的语法糖。每个标签库定义一组动作。例如,jsp 标签库定义了三个动作:“getProperty”、“setProperty”和“useBean”。每个动作都由一个 Java 类定义,称为标签处理器。
为了定义标签库,我们创建一个 XML 文件,称为标签库描述符或 TLD。TLD 将每个动作连接到其相应的标签处理器类,列出可选和强制属性,以及有关标签的其他信息。
为了在 JSP 中使用我们的自定义动作,我们使用一个特殊的指令来加载我们的 TLD。这有助于 JSP 引擎验证 JSP 中的自定义标签,并找到与这些动作关联的标签处理器类。
我们现在将定义一个简单的自定义动作,以便了解使用标签处理器类、TLD 和 JSP 的基本机制。
我们的自定义动作将是一个“hello”标签,它接受一个可选的“firstname”参数。如果存在该参数,我们的标签将向指定用户生成一个简单的“hello”消息。如果缺少该参数,我们的标签将生成一个通用的“hello”消息。
第一步是编写一个简单的标签处理器来实现此功能。清单 1 显示了这样一个标签处理器,它定义了 HelloTag 类。我将 HelloTag.java 源代码文件以及所有 JSP 和 Servlet 相关类放在 $TOMCAT_HOME/classes 目录下。由于 HelloTag.java 在 il.co.lerner 包中,并且我机器上的 $TOMCAT_HOME 是 /usr/java/jakarta-tomcat-3.2.1,这意味着我将我的 Java 源代码文件放在
/usr/java/jakarta-tomcat-3.2.1/classes/il/co/lerner/HelloTag.java
将 HelloTag.java 编译成 HelloTag.class 后,此标签处理器可以合并到一个或多个标签库中。
每个标签处理器类都必须实现两个不同的标准接口之一,Tag 或 BodyTag。(后者用于在开始和结束标签之间有主体的自定义动作,而不是我们本月将讨论的那些没有主体的自定义动作。)
实际上,没有理由实现这些接口。从 TagSupport 和 BodyTagSupport 类继承更容易也更实用,这两个类为接口提供了默认实现。通过子类化 TagSupport,我们可以节省一些工作,仅覆盖那些我们不希望使用默认行为的方法。最后,我们对 HelloTag 的实现只需要三个方法:setFirstname、doEndTag 和 release。
第一个方法 setFirstname,看起来和行为都像 JavaBean 属性设置方法,它接受一个参数并返回 void。当 JSP 引擎遇到带有“firstname”参数的自定义动作时,会自动调用 setFirstname。参数值设置为标签中传递的值。与 JavaBean 一样,设置 firstname 的方法必须命名为 setFirstname,其中“F”大写。
我们的第二个方法 doEndTag,在 JSP 引擎遇到自定义动作的结束标签时被调用。doEndTag 方法不接受任何参数并返回一个整数。但是,我们不会返回整数,而是返回为我们提供的符号常量之一。通常,我们将返回 EVAL_PAGE,它告诉 JSP 引擎它应该继续评估调用我们的自定义动作的 JSP 的其余部分。如果我们希望阻止 JSP 引擎进一步评估文件,可能是因为我们遇到了错误,或者因为我们想将用户转发到另一个 URL,我们可以返回 SKIP_PAGE。
在 doEndTag 内部,我们可以放置我们可能喜欢的任何 Java 代码。除了我们创建的任何实例变量之外,我们还可以访问有关 JSP 本身的信息,包括其 HTTP 请求和响应。这就是我们如何将信息写入用户的浏览器,用 HTML、XML 或纯文本替换自定义标签。(自定义动作通常返回纯文本,允许 JSP 作者选择如何格式化该文本。)使用由我们的 TagSupport 超类定义的 PageContext 对象,我们可以检索输出流并将数据发送到它
pageContext.getOut().println("Hi there!");
最后,我们定义 release 方法,它不接受任何参数并返回 void。release() 在自定义动作完成执行后被调用,它使标签处理器类有机会在自身之后进行清理。一般来说,这意味着将每个实例变量设置为 null,但也可能涉及关闭与关系数据库的连接或将信息发送到错误日志。在 HelloTag.java 中,我们只是将 firstname 赋值为 null 值,然后要求我们的超类将它自己的每个值都置为空。
现在我们了解了 HelloTag 中的每个单独方法,它们是如何协同工作的?当 JSP 包含映射到我们类的自定义动作(通过 TLD,如下所述)时,每个动作的参数都会调用我们类中的“set”方法。例如,有人传递参数 firstname=“foo” 将有效地调用 setFirstname(“foo”)。
由于我们希望使 firstname 成为可选参数,因此我们在首次创建它时为其指定默认值 (null)。当 JSP 引擎完成评估我们的自定义动作时,它会调用 doEndTag 并查看 firstname 的值。如果 firstname 为 null,它会向最终用户发送通用(“Hi there!”)消息。但是,如果 firstname 不为 null,则 doEndTag 使用其值向最终用户发送更个性化的消息。
当自定义动作完成执行后,JSP 引擎会调用 release(),重置 firstname 和许多其他对象。
一旦我们编写了我们的类,我们就可以编写一个 TLD,向 JSP 引擎描述它。许多人可能更喜欢以相反的方向工作,使用 TLD 作为 JSP 作者和标签处理器在并行工作时可以使用的规范。我更喜欢先编写自定义动作,然后根据我的进度修改 TLD,即使这显然不是最安全也不是最优雅的工作方式。
正如您从清单 2 中看到的那样,TLD 可以是一个相对较短的 XML 文件。TLD 将动作名称映射到实现这些动作的类。TLD 可以将单个动作映射到单个类,或者它可以将数百个动作映射到数百个不同的类。并且由于每个类都单独存在,因此甚至有可能(尽管绝不是一个好主意)在一个类中同时使用多个 TLD。
TLD 在首次被引用时被加载到我们的 Servlet 容器中。不幸的是,这意味着在自定义动作已被调用后更改 TLD 需要重新启动 Tomcat(以及 Apache,如果您将 Apache 的 mod_jk 与 Tomcat 服务器一起使用)。它告诉 JSP 引擎您的标签库支持的版本和规范,使 JSP 引擎可以知道何时需要升级特定库才能与当前标准兼容。
TLD 由顶层 <taglib> 标签组成,该标签至少包含四个部分:<tlibversion> 指示此库支持的标签库规范的版本;<jspversion> 指示编写标签库所针对的 JSP 规范的版本;<shortname> 为此标签库提供一个名称,某些 JSP 引擎会使用该名称;<tag> 对于我们要包含在库中的每个标签处理器类出现一次。每个标签都有自己的名称,即被调用的动作的名称。因此,如果我们导入一个前缀为“abc”的标签库,则名为“hello”的标签将作为“abc:hello”被调用。<tagclass> 部分将标签的名称映射到实际执行动作的标签处理器类;此类别显然必须在您的服务器的 CLASSPATH 中。<info> 部分允许我们提供有关此特定标签的一些基本信息和内联文档。
最后,我们命名此自定义动作接受的每个属性。每个属性都有自己的 <name> 标签,以及属性是否为必需的指示。
现在我们有了 TLD 和标签处理器类,我们可以在我们的任何 JSP 中一起使用它们。我们使用特殊的 JSP taglib 指令导入标签库
<%@ taglib uri="/WEB-INF/hello.tld" prefix="hello" %>
请注意 taglib 指令如何接受两个参数“uri”和“prefix”。uri 部分包含我们刚刚创建的 TLD 的文件名。如果您想将 TLD 直接放在您的 WEB-INF 目录中,那么上面的语法是完全有效的。prefix 参数是一种命名空间声明,它告诉 JSP 引擎我们将为标签库导入的每个动作附加什么前缀。让 JSP 可以选择命名前缀,而不是将其构建到标签库本身中,这使我们能够导入多个标签库,而无需担心命名空间冲突。
由于我们的 TLD 定义了一个“hello”标签,并且由于我们使用“hello”前缀导入了标签库,因此我们可以使用以下语法调用我们的 HelloTag 方法:<hello:hello/>。清单 3 包含一个完整的 JSP (test-tag.jsp),演示了我们如何使用此标签。
请记住在调用自定义动作时包含尾部斜杠。如果您忘记包含它,Tomcat 的 JSP 引擎(称为 Jasper)将生成类似于以下的错误消息
Unterminated user-defined tag: ending tag </hello:hello> not found or incorrectly nested
我们的 TLD 指示 firstname 属性是可选的。如果我们不传递 firstname 参数,那么我们在 Web 浏览器中得到以下输出
This is a test of our custom action. Hi there!我们还可以传递一个可选的 firstname 参数
<hello:hello firstname="Reuven"/>如果我们将以上内容放在我们的 JSP 中,则以下输出将发送到浏览器
This is a test of our custom action. Hello, Reuven
以上只是一个关于自定义动作如何工作的简单示例。自定义动作标签可以做的不仅仅是打印名称。例如,对象可以连接到关系数据库,检索(或存储)信息,而无需在我们的 JSP 中显式 Java 代码。自定义动作也可以充当迭代器或为我们提供条件执行。
为了执行这些更高级的动作,我们将利用标签处理器类可以查看自定义动作的主体这一事实;也就是说,可能碰巧位于动作的开始和结束标签之间的任何文本。我们可以使用此文本执行各种操作,从迭代和条件执行到要求 JSP 引擎评估其内容,然后再将其传递给标签处理器。甚至可以将一个标签嵌套在另一个标签内部,从而有效地将值从一个动作传递到另一个动作。
有许多开源标签库,包括 Jakarta 项目本身提供的一个标签库,它们使用这些功能在许多标签中提供大量功能。
自定义动作是一个非常强大的工具。与在 JSP 中放置直接 Java 代码相比,它们提供了许多优势,将复杂行为封装在易于记忆的标签中,使非程序员相对容易地使用数据库和其他重要的系统。
但是,自定义动作存在一个问题,可以追溯到“自定义”一词。在 JSP 中定义您自己的标签的能力是一个聪明而精巧的工具,并为参与 Web 站点开发的每个人提供了许多好处。但是,Web 的部分美妙之处在于它是相对标准化的。
此外,自定义动作可以用于创建一种全新的语言,该语言用 Java 编写并在标签处理器类中实现。Hans Bergsten,他的著作 JavaServer Pages 在 JSP 中提供了出色的信息和指导,他将这个想法推向了极限,有效地消除了 JSP 中对 Java 的需求。但是,用一种新的、鲜为人知且未经实战检验的语言(他的自定义标签库)来代替一种相对稳定且众所周知的语言 (Java) 让我感到不安。
如果我在一家大型公司工作,该公司已决定对 Java、Servlet 和 JSP 进行重大投资,我会非常放心地使用自定义动作。这样的公司有能力创建自己的标签库,该标签库可以在 Web 站点的生命周期内使用,定义自己的工作方式标准。
但是,对于我们这些在大型公司之外工作或与许多不同客户合作的人来说,互操作性是首要考虑因素。如果我的每个客户都为他们的站点定义了一组不同的自定义动作,我将发现自己难以记住我需要使用哪些标签和属性来进行循环、数据库访问和条件执行。正如我在上面指出的,我担心与已经难以理解将 Java 嵌入到他们的 HTML 页面中的想法的非程序员合作——教他们两种不同类型的循环(一种在 Java 中,另一种使用自定义动作)无疑会导致一些混乱。
一个好的折衷解决方案可能是包含一组大型的标准自定义动作,这些动作将成为 JSP 规范的一部分,就像 JavaBean 相关标签所做的那样。Bergsten 的 Java Server Pages 中介绍的标签库是一个良好的开端,但只是众多可用库之一。很高兴看到 JSP 社区在这个问题上团结起来,在我们发现自己面临数十个相似但不兼容的库(其中一些无疑是专有的)之前。
JSP 是一种强大而快速的方式来使用服务器端 Java,特别是对于不想学习语言的非程序员而言。自定义动作,特别是与 JavaBean 组件结合使用时,可以最大限度地减少代码来执行复杂任务。通过一些预见,站点可以避免在他们的 JSP 中插入几乎任何 Java 代码,而是依赖自定义动作和标签库。
但是,站点(以及使用自定义动作的顾问)应在标签库的便利性和强大功能与它们实际上正在创建一种新的编程语言这一事实之间取得平衡。如果我们不小心,自定义标签将导致服务器端 Java 社区分裂,将其分裂成使用不同、不兼容库的子社区。
