JavaBeans

作者:Reuven M. Lerner

在前两期“At the Forge”专栏中,我们开始使用 Jakarta-Tomcat,这是一个由 Apache 软件基金会赞助的开源 servlet 和 Java 服务器页面 (JSP) 引擎。正如我们所见,创建 servlet 或 JSP 既不困难也不耗时。适应服务器端 Java 范例可能是使用它们的最大障碍。

虽然 servlet 为我们提供了 Java 程序中可用的全部功能和表达能力,但它们迫使我们在相对较低的级别上思考。每次我们想要向用户的浏览器发送 HTML 格式的文本时——这通常是很频繁的——我们都必须使用与 HTTP 响应对象关联的 PrintWriter 对象

PrintWriter out = response.getWriter();
out.println("<HTML><Body>This is illegal HTML</Body></HTML>");

JSP 的出现是为了解决这个问题,它假设所有未显式标记为可执行代码部分的内容都应原封不动地发送到用户的浏览器。但这又产生了一个新问题,即想要连接到关系数据库的 JSP 必须添加数十甚至数百行 Java 代码。

解决方案是创建驻留在 JSP 之外的代码包,可以使用类似于 HTML 而不是 Java 的语法调用这些代码包的方法。这些代码包被称为 JavaBeans,它们可以使 JSP 的使用变得更加容易,无论是对于想要在更高层次上工作的经验丰富的程序员,还是对于想要利用功能的经验不足的程序员。

本月,我们将快速浏览 JavaBeans。我们将编写一些我们自己的 bean,将它们集成到 JSP 中,并讨论与它们相关的一些问题和陷阱。

什么是 Bean?

从实现 bean 的人的角度来看,JavaBeans 无非是遵循几个约定的 Java 类。(我们很快将讨论这些约定是什么。)

但是对于编写 JSP 的人来说,bean 是一种特殊的容器,我们可以在其中存储和检索某些类型的信息。每条信息都称为属性,可以单独设置或检索。并非所有属性都可以设置,也并非所有属性都可以检索,但是从 JSP 到 bean 的接口是统一且易于理解的。

由于 bean 理解一组受限的操作,因此有一些特殊的 JSP 标签允许我们使用它们。使用这些标签可以减少直接放置在 JSP 中的 Java 代码量。这不仅减少了混乱,使我们的 JSP 更易于维护和阅读,而且意味着非程序员也可以利用 bean 的强大功能,而无需学习 Java 编程。

JSP 同时使用多个 bean 并根据需要存储和检索不同的属性是很常见的。因此,在线商店购物车的 JSP 可能会使用一个 bean 来管理商店的库存,另一个 bean 来管理用户的购物车,还有一个 bean 来跟踪用户的语言、付款和送货偏好。这些 bean 中的每一个都由一个单独的 Java 类实现,但使用特殊的 JSP 标签进行操作,这些标签隐藏了 JSP 作者的大部分复杂性。

当然,bean 可以用于多个网站(或单个网站的多个部分)。如果您开发了一个有用的 JavaBean,它封装了有趣的功能,其他用户可以将该 bean 放入他们的 Java classpath 中,从而利用 JSP 中的功能。

实现 Bean

要编写 bean,我们必须编写一个实现 java.io.Serializable 接口的 Java 类。简而言之,这意味着 bean 必须能够保存到磁盘并恢复自身。如果您的类的字段是常见的 Java 类型,例如整数和字符串,那么实现 Serializable 不应该让您太担心。

清单 1 包含一个简单的 bean 实现。此 bean 包含一个实例变量 (userID) 和两个方法。getUserID 方法返回 userID 的当前值,而 setUserID 方法设置该字段的值。由于这些方法的返回值和参数列表与 bean 属性方法的签名匹配,因此我们可以从 JSP 中使用它们。

清单 1. SimpleBean.java

在我的系统上,它运行的是 Apache 项目的 Jakarta-Tomcat servlets/JSP 系统 3.2 版本,我将我的 Java 类放在目录 $TOMCAT_HOME/classes 下($TOMCAT_HOME 是一个环境变量,指向 Tomcat 安装的根目录;在我的系统上,它的值是 /usr/java/jakarta-tomcat-3.2.1/)。如果存在此“classes”目录,则将其添加到 Tomcat CLASSPATH 环境变量中,使其成为放置新类的便捷位置。

该类本身非常简单,演示了您可以创建的不同类型的方法:1) 一个不带任何参数并且可以设置一个或多个字段的 bean 构造函数。在我们的特定示例中,SimpleBean 构造函数将 userID 初始化为 0。2) 一个 get 属性方法,它向调用者返回值。与 bean 构造函数一样,get 属性方法不带任何参数。3) 一个 set 属性方法,它接受单个参数(新值),但不向其调用者返回任何值。

请记住,bean 像大多数其他 Java 类一样是类,这意味着它们必须在重用之前重新编译。此外,Tomcat servlet 容器不会自动重新加载已编译的类。您最好的选择是在每次重新编译 bean 类时重新启动 Tomcat。

使用 Bean

现在我们已经创建了简单的 bean 类,我们如何在 JSP 中使用它?JSP 识别三个特殊标签,所有标签都以“jsp:”开头。

在我们继续之前,先提醒一下:允许我们在 JSP 中使用 JavaBeans 的特殊标签是用 XML 而不是 HTML 编写的。虽然 HTML 是一个定义松散的规范,并不总是要求我们关闭标签,但 XML 要严格得多。每个开始标签 <tag> 都必须由匹配的结束标签 </tag> 关闭。如果没有结束标签 </tag>,XML 解析器将退出并显示错误。标签可以通过在标签末尾放置斜杠来关闭自身,如 <tag/> 中所示。

这意味着在使用 JavaBeans 的 HTML 生成 JSP 中,您将不得不跟踪两种略有不同的语法。一段时间后,您会发现在这两种语法之间切换几乎是第二天性。此外,JSP 解析器会生成错误消息,使您相对容易地确定何时忘记在 JavaBean 标签上放置尾部斜杠。但是,这种语法的混合在开始时可能会让人感到困惑,并且您可以预期在学习理解它时会经历一些困难。

要使用 JavaBean 类,我们使用特殊的 <jsp:useBean/> 标签。此标签告诉 JSP 查找和加载特定的 bean,并在我们的 JSP 中创建 bean 的实例。<jsp:useBean/> 标签还允许我们为 bean 实例命名,我们稍后将使用该名称。以下是如何从 JSP 中加载我们的 SimpleBean 类的示例

<jsp:useBean id="simple"
 class="il.co.lerner.SimpleBean"/>

如您所见,该标签以斜杠 (/) 结尾,遵循 XML 语法。实际上,在某些情况下,您可能更喜欢将 <jsp:useBean/> 标签分成一个开始标签 <jsp:useBean> 和一个结束标签 </jsp:useBean>;这两个标签之间的任何内容仅在 bean 首次加载到内存时执行。但是,更简单的形式并不少见。

<jsp:useBean/> 标签接受两个强制参数。class 参数命名我们的 bean 所在的包和类。id 参数在 JSP 中为我们的 bean 提供唯一的名称。与变量名一样,为 bean 选择清晰的标识符是一个好主意。名称越明显,以后调试我们的 JSP 就越容易。

我们可以使用 <jsp:setProperty/> 和 <jsp:getProperty/> 标签在我们的 bean 实例中设置和检索属性值。这两个标签都采用 name 参数,其值应与我们之前在 <jsp:useBean/> 中给 bean 的 id 相同(我确信我们为什么在 <jsp:useBean/> 中使用 id 属性而在 <jsp:setProperty/> 中使用 name 属性有充分的理由,但我发现这很令人困惑)。因此,我们可以使用以下标签检索 userID 属性的值

<jsp:getProperty name="simple" property="userID"/>

这会从 simple bean 中检索 userID 属性,并将其放置在 JSP 内部。请注意,这不仅仅是检索值,它还使其对用户可见。还要注意,我们的属性名称的大小写略有改变。为了访问 bean 中的 getUserID 方法,我们对属性 userID 使用 <jsp:getProperty/>。但是,不要被误导地认为属性名称不区分大小写;这种转换仅仅是为了保持可读性。

要修改属性的值,我们使用 <jsp:setProperty/> 标签。此标签不返回任何结果,但它确实采用 value 属性,其值随后会传递给 bean 类中的相应方法

<jsp:setProperty name="simple" property="userID"
 value="300"/>

清单 2 包含一个完整的 JSP,演示了我们如何从 JSP 中使用我们的 SimpleBean 类。它显示 userID 属性的默认值,然后将该属性设置为新值并显示新值。

清单 2. use-simple.jsp

参数和属性

属性存储在 bean 的实例变量中当然是很常见的,就像在我们的 SimpleBean 类中一样。在这种情况下,调用 <jsp:setProperty/> 有效地设置了字段的值,而调用 <jsp:getProperty/> 则检索其当前值。当然,没有理由说属性必须反映字段。属性可以轻松地存储到关系数据库中并从中检索。当您检索属性时,返回的值可能是实时计算的,而不是从实例变量返回的。

例如,考虑一下我们如何创建一个执行简单数学运算的 bean。我们可以设置两个读/写属性(称它们为 arg1 和 arg2),然后设置一些只读属性,我们可以使用这些属性对这些参数执行计算。清单 3(可在 ftp.linuxjournal.com/pub/lj/listings/issue86 获取)包含一个简单的 bean,Calculate.java,它演示了我们如何完成此操作。

由于没有 setSum 属性,JSP 引擎将不允许我们在 sum 属性上调用 <jsp:setProperty/>。但是,它将允许我们设置 arg1 和 arg2,并检索我们可能想要的每个单独的属性。

现在我们有了一个可用的 bean,我们可以在 JSP 中使用它。清单 4 包含 calculator.jsp 的清单,它使用我们刚刚创建的 JavaBean 执行一些基本计算。

清单 4. calculator.jsp

calculator.jsp 的第一个也是最有趣的部分是它设置属性的方式

<jsp:setProperty name="calculator" property="*"/>

通常,我们可以获取通过 GET 或 POST 传递的参数之一,并使用以下表示法将其值分配给特定属性

<jsp:setProperty name="calculator"
 parameter="foo" property="arg1"/>
换句话说,上面的标签将获取 foo 参数,并在 calculator bean 上调用 setArg1 时使用其值。但是,当我们使用星号时,如清单 4 所示,我们向 JSP 引擎指示,我们想要获取我们收到的每个参数,并将每个值分配给同名属性。因此,arg1 参数将传递给 arg1 属性,依此类推。

在我的系统上,我已将 calculator.jsp 安装在 examples/jsp URL 下,我可以使用以下 URL 将 arg1 的值分配为 5,将 arg2 的值分配为 20:https://127.0.0.1/examples/jsp/calculator.jsp?arg1=5&arg2=20。

当然,这不是一个万无一失的系统。我可以传递以下 URL 来引发运行时异常:https://127.0.0.1/examples/jsp/calculator.jsp?arg1=5&arg2=20.0。

JSP 引擎尝试将 20.0(浮点值)分配给 arg2(整数),这会失败。

我们可以用 scriptlet 标签包围我们的 <jsp:setProperty/> 和 <jsp:getProperty/> 标签,使用标准的 Java try-and-catch 机制来尝试避免运行时错误。例如,以下代码确保我们在使用 getQuotient 时永远不必处理除零异常

<P>Quotient:
    <% try { %>
        <jsp:getProperty name="calculator"
        property="quotient"/>
    <% } catch (Exception e) { %>
        <B>Error! division by zero</B>
    <% } %>
</P>

与此同时,这种代码将更多的 Java 引入到 JSP 中,这正是我们开始使用 bean 的原因。您的员工中的非程序员是否能够处理这种代码,很大程度上取决于您的环境以及您创建的 bean 的种类。

Bean 作用域

正如我们在前几个月中讨论的那样,servlet 容器在给定时间仅将每个 servlet 的单个副本加载到内存中。它可以做到这一点,因为 Java 是多线程的,允许特定的 servlet 为多个 HTTP 请求同时执行。由于 JSP 实际上是伪装的 servlet,因此它们也受多线程问题的影响。

这就提出了我们的 JavaBeans 会发生什么的问题:它们加载了多少次?它们的作用域是什么?如果 JSP 设置了一个恰好修改类字段的属性,这是否会影响同一 bean 的所有其他实例?

答案是:视情况而定。有四种不同类型的 bean 作用域,您选择的作用域类型将深刻影响您的 bean 的使用方式。应用程序作用域意味着 servlet 容器中运行的所有 JSP 的 bean 的单个副本。会话作用域适用于单个用户,从他们进入站点到他们退出站点的时间。如果用户打开两个浏览器窗口访问您的系统,他们将拥有相同的会话。请求作用域延伸到 HTTP 请求结束。如果您想在一个 JSP 中设置 bean 的属性,使用 <jsp:forward/> 标签执行到第二个 JSP 的内部重定向,然后在第二个 JSP 中继续使用同一个 bean,这将非常有用。页面作用域适用于单个 JSP 页面。当页面退出时,作用域也随之退出。

默认情况下,bean 放置在会话作用域中。您可以通过向 <jsp:useBean/> 添加 scope 参数来更改此设置

<jsp:useBean id="simple" scope="application"
 class="il.co.lerner.SimpleBean"/>

一旦我们完成上述操作,一个 JSP 设置的属性将对其他 JSP 可见。当然,由于应用程序和服务器作用域中的 bean 可能会被多个 JSP 同时执行,因此它们必须是线程安全的。考虑您的 bean 旨在使用的作用域,并根据需要使其线程安全。如果 bean 不是线程安全的,请务必在其文档中注明,以免其他人错误地以错误的方式使用它。

Web 日志

上个月,我们继续对 Web 日志进行简单调查,使用 JSP 直接访问关系数据库以获取 Web 日志的最新内容。虽然这样的 JSP 当然是合法的,但它看起来很笨拙,难以调试,并且未能像我们希望的那样优雅地将代码与逻辑分离。

通过将数据库逻辑放入 JavaBean 中,我们可以实现我们的几个目标:代码是可重用的,即使对于非程序员也可用,并且允许我们更改信息来源或应用程序逻辑,而无需重写我们的 JSP。清单 5(可在 ftp.linuxjournal.com/pub/lj/listings/issue86 获取)包含我们的 bean 的源代码,我已经使其线程安全(在 getBlog 方法中使用“synchronized”关键字),因此我们可以创建应用程序作用域的单个实例。此 JavaBean 连接到 Web 日志数据库并检索最新信息。清单 6 包含一个小 JSP,它使用此 JavaBean 显示 Web 日志。

清单 6. viewblog.jsp

清单 5 中的 bean 实际上没有任何特别之处,但它确实汇集了我们在过去几个月中讨论的许多内容。我们现在有了一种让非程序员访问我们的 Web 日志中的信息的方法,而无需自己编写一行代码!使用一些简单的 JSP 标签,我们可以快速轻松地将我们当前的博客内容放置在 HTML 页面中。

结论

JavaBeans 是减少我们在 JSP 中放置的代码量,同时使其更易于使用的一种绝妙方法。但是,复杂的 JSP 仍然会包含一些代码,例如当它们必须迭代循环或使用复杂数据时。

下个月,我们将了解如何使用 JSP 的自定义操作工具编写我们自己的类似于本月看到的 jsp: 标签的标签。因此,我们可以创建我们自己的新标签,将它们与我们希望的任何代码相关联。通过这种方式,JSP 允许我们创建自己的新格式化语言,以及使用已提供的标签。

资源

JavaBeans
Reuven M. Lerner 拥有一家小型咨询公司,专门从事 Web 和互联网技术。当您阅读本文时,他应该(终于)完成了《Core Perl》的编写,该书将于今年晚些时候由 Prentice-Hall 出版。您可以通过 reuven@lerner.co.il 或 ATF 主页 http://www.lerner.co.il/atf/ 与他联系。
加载 Disqus 评论