Java Servlet 技术简介
Java Servlet 技术简介
Java 开发人员兼培训师 Roy Miller 将我们现有的 servlet 介绍资料修改成了这篇易于学习的实用教程。Roy 将介绍并解释 servlet 是什么,它们是如何工作的,如何使用它们来创建您能够想像到的任意复杂度的 Web 应用程序,以及作为一名专业编程人员,您如何才能最有效地使用 servlet。
4 评论:
开始之前
关于本教程
在您最喜欢的 Web 浏览器中,您所阅读的页面是如何出现的呢?当登录到您最喜欢的 Web 站点时,该 Web 站点如何知道登录的用户是您?而 Web 零售商又如何接受您的在线订购呢?这些功能都是可能的,因为在这些场景的背后,运行于服务器上的代码将在 Web 会话中与您进行交互,通过该过程访问已存储的信息,并经常在一个或多个 Web 页面中展示动态信息。在 Java 语言世界中,这些功能的核心部分是由 servlet 提供的。本教程的目的就是向您介绍 servlet。文中将描述 servlet 是什么,它们是如何工作的,您可以如何使用它们来创建您能够想像到的任意复杂度的 Web 应用程序,以及作为一名专业编程人员,您如何才能最有效地使用 servlet。
本教程的内容是为不熟悉,或者只是略微熟悉 servlet 的 Java 编程人员准备的。本教程假定您对于下载和安装软件以及 Java 语言(创建类、导入类等)有一般性的了解,但并不假定您已经了解 servlet。本教程包括一个说明 servlet 基本概念的简单例子,以及一个涉及更多内容的例子,它说明如何在小型的合同管理程序中更复杂地使用 servlet。
本教程的适用对象
如果您已编写 Web 应用程序多年,那么本教程可能不适合您。如果您不知道 servlet 是什么,或者只是略懂一二,那么请您继续读下去。虽然本教程所包含的只是 servlet 的部分内容,但它是一篇很好的入门介绍。
不过,您应该非常了解 Java 编程的基础知识。但是,如果您还没有完全达到这些要求,那么请从阅读我撰写的 Java 编程简介 教程开始。
工具和代码
为了运行本教程中的例子或示例代码,至少需要在机器上安装 JDK 1.4.2 或更高版本,以及 Eclipse IDE。我们将介绍安装用于 Eclipse 的 Tomcat 插件的整个过程,这将允许您很容易地开发 servlet 应用程序。
本教程中的所有代码示例都在 Windows XP 平台上用 J2SE 1.4.2 进行了测试,但必须使用 J2SE 1.4.1 或者甚至是 5.0 版本,并且不对它们进行修改,它们才会工作。
要安装 Tomcat,则需要进入 Jakarta 的 Web 站点(请参阅 参考资料),并下载二进制版本的 Tomcat 5.0.28(编写本教程时,这是匹配 J2SE 1.4.2 的最新版本)。随 Windows 安装程序一起提供的包会使该平台上的安装轻而易举地完成。按照 readme 文件中的说明进行安装,您将顺利完成这项操作。
为了安装用于 Eclipse 的 Tomcat 插件,需要进入 Sysdeo 的 Web 站点(请参阅 参考资料 ),并下载该插件的 zip 文件(编写本教程时,该文件是 tomcatPluginV3.zip)。然后只要将之解压至 plugins 目录,并按照下载页面底部的说明安装该插件即可。为了确保这个插件正常工作,请阅读极为简单的 HelloWorld servlet 设置“教程”,Sysdeo 页面底部有其链接(至于直接链接,请参阅 参考资料)。
一旦安装了 Tomcat 及其插件,就可以准备开始本教程了。
servlet 简介
servlet 的作用
当使用交互式 Web 站点时,您所看到的所有内容都是在浏览器中显示的。在这些场景背后,有一个 Web 服务器接收会话 中来自于您的请求,可能要切换到其他代码(可能位于其他服务器上)来处理该请求和访问数据,并生成在浏览器中显示的结果。
servlet 就是用于该过程的网守(gatekeeper)。它驻留在 Web 服务器上,处理新来的请求和输出的响应。它与表示无关,实际上也不它应该与表示有关。您可以使用 servlet 编写一个流,将内容添加到 Web 页面中,但那通常也不是一个好办法,因为它有鼓励表示与业务逻辑的混合的倾向。
servlet 的替代品
servlet 不是服务于 Web 页面的惟一方式。满足该目的的最早技术之一是公共网关接口(CGI),但那样就要为每个请求派生不同的进程,因而会影响效率。还有专用服务器扩展,如 Netscape Server API(NSAPI),但那些都是完全专用的。在 Microsoft 的世界里,有活动服务器页面(ASP)标准。servlet 为所有这些提供了一个替代品,并提供了一些好处:
- 它们与 Java 语言一样是与平台无关的。
- 它们允许您完全访问整个 Java 语言 API,包括数据访问库(如 JDBC)。
- 大多数情况下,它们内在地比 CGI 更高效,因为 servlet 为请求派生新的线程,而非不同的进程。
- 对于 servelet 有一个广泛的行业支持,包括用于最流行的 Web 和应用程序服务器的容器。
servlet 是对专业编程人员工具箱的强大补充。
但什么是 servlet?
作为一名专业编程人员,您碰到的大多数 Java servlet 都是为响应 Web 应用程序上下文中的 HTTP 请求而设计的。因此,javax.servlet
和javax.servlet.http
包中特定于 HTTP 的类是您应该关心的。
在创建一个 Java servlet 时,一般需要子类 HttpServlet
。该类中的方法允许您访问请求和响应包装器(wrapper),您可以用这个包装器来处理请求和创建响应。
当然,HTTP 协议不是特定于 Java 的。它只是一个规范,定义服务请求和响应的大致式样。Java servlet 类将那些低层的结构包装在 Java 类中,这些类所包含的便利方法使其在 Java 语言环境中更易于处理。正如您正使用的特定 servlet 容器的配置文件中所定义的,当用户通过 URL 发出一个请求时,这些 Java servlet 类就将之转换成一个 HttpServletRequest
,并发送给 URL 所指向的目标。当服务器端完成其工作时,Java 运行时环境(Java Runtime Environment)就将结果包装在一个 HttpServletResponse
中,然后将原 HTTP 响应送回给发出该请求的客户机。在与 Web 应用程序进行交互时,通常会发出多个请求并获得多个响应。所有这些都是在一个会话语境中,Java 语言将之包装在一个HttpSession
对象中。在处理响应时,您可以访问该对象,并在创建响应时向其添加事件。它提供了一些跨请求的语境。
容器(如 Tomcat)将为 servlet 管理运行时环境。您可以配置该容器,定制 J2EE 服务器的工作方式,而且您必须 配置它,以便将 servlet 暴露给外部世界。正如我们将看到的,通过该容器中的各种配置文件,您在 URL(由用户在浏览器中输入)与服务器端组件之间搭建了一座桥梁,这些组件将处理您需要该 URL 转换的请求。在运行应用程序时,该容器将加载并初始化 servlet,管理其生命周期。
当我们说 servlet 具有生命周期时,只是指在调用 servlet 时,事情是以一种可预见的方式发生的。换言之,在任何 servlet 上创建的方法总是按相同的次序被调用的。下面是一个典型场景:
- 用户在浏览器中输入一个 URL。Web 服务器配置文件确定该 URL 是否指向一个由运行于服务器上的 servlet 容器所管理的 servlet。
- 如果还没有创建该 servlet 的一个实例(一个应用程序只有一个 servlet 实例),那么该容器就加载该类,并将之实例化。
- 该容器调用 servlet 上的
init()
。 - 该容器调用 servlet 上的
service()
,并在包装的HttpServletRequest
和HttpServletResponse
中进行传递。 - 该 servlet 通常访问请求中的元素,代表其他服务器端类来执行所请求的服务并访问诸如数据库之类的资源,然后使用该信息填充响应。
- 如果有必要,在 servlet 的有用生命结束时,该容器会调用 servlet 上的
destroy()
来清除它。
如何“运行”servlet
“运行”servlet 就像运行 Java 程序一样。一旦配置了容器,使容器了解 servlet,并知道某些 URL 会致使容器调用该 servlet,该容器就将按照预定的次序调用生命周期方法。因此,运行 servlet 主要是指正确配置它,然后将浏览器指向正确的 URL。当然,servlet 中的代码正是发现有趣的业务逻辑的地方。您不必担心低层事件的进展,除非发生某种错误。
不幸的是,经常会发生 一些令人沮丧的错误,尤其是在设置 servlet 时。致使 servlet 应用程序令人头痛的最大原因就是配置文件。您无法有效地调试它们。您只能通过试错法弄清楚这些错误,比如尽力破译可能会或不会在浏览器中看到的错误消息。
一个简单的 servlet
这个简单的 servlet 要完成的任务
第一个 servlet 将完成极少量的工作,但是它将暴露编写 servlet 的所有基本要求。它将在浏览器窗口中输出一些简单的无格式文本:
Hello, World!
在创建该 servlet 时,我们将可以证实 Tomcat 应起的作用,并证实我们可以按照计划使用 Eclipse 创建 Web 项目。我们还将遍历在 Tomcat servlet 容器中配置 Web 应用程序的整个过程,如果您碰巧在 XML 文件中犯了一个小错误,那么您可能会对这个过程感兴趣。不要担心:至少在本教程中,Tomcat 会一直发挥起作用。
在这第一个例子中,我们会将输出直接从 servlet 写入浏览器中。在本教程中,这将是我们最后一次使用该方法。
设置 Eclipse
我们需要执行少量工作,确保可以在 Eclipse 中创建并管理 Tomcat 项目。
如果已经安装了该插件(仅仅通过将 Sysdeo zip 文件解压至 eclipse/plugins 目录),那么您应该可以在工具栏上获得一些附加的菜单项和工具。如图 1 中所示。
图 1. Tomcat 插件功能
工具栏按钮允许您启动、停止和重启 Tomcat,当需要运行 servlet 时,必须进行这些工作。
为了允许我们创建 Tomcat 项目,这些项目具有合适的布局,有助于 Tomcat 的部署,我们必须告诉 Eclipse 一些事情。如果单击Window>Preferences,那么您就将看到标准的 Eclipse 偏好设定对话框,其列表底部有一个名为 Tomcat 的新类别。单击它将向您展示 Tomcat 偏好设定的主页(参见图 2)。
图 2. Tomcat 的偏好设定
选择 Version 5.x,并指定 Tomcat home 的位置。(我的系统上,该位置是 C:\Program Files\Apache Software Foundation\Tomcat 5.0,但您的可能会不同)。选择 Context files 为内容声明模式。然后,单击 JVM Settings 对子类别进行偏好设定,并确保在该页顶部的下拉菜单中选择一个有效的 JRE。您可以使用默认的 JRE,也可以指向您的 JDK,并在 Java>Installed JREs 偏好设定页面中告诉 Eclipse 这个 JDK。
完成这些操作后,请单击 OK。现在,我们准备创建 Tomcat 项目。
创建 Tomcat 项目
Tomcat 插件使 Web 开发人员更易于使用 Tomcat。如果单击 File>New>Project,并展开对话框(参见图 3)中的 Java 向导类别,那么您会在该对话框中看到一类新的项目向导:Tomcat 项目。
图 3. 新建 Tomcat 项目
单击 Next,将该项目命名为“HelloWorld”,然后单击 Finish。如果在 Eclipse 中切换至 Java 透视图,那么就可以看到这个新项目。它所具有的结构将有助于部署 Tomcat(参见图 4)。
图 4. Tomcat 项目结构
正如我们稍后将看到的,work、WEB-INF 和 WEB-INF/src 目录特别重要。
测试 Tomcat
单击 Start Tomcat 工具栏按钮。当 Tomcat 尝试启动时,Eclipse 将用信息语句更新控制台。如果它启动了,并且没有显示任何堆栈跟踪,那么您已作好准备。如果看到堆栈跟踪,则事情会麻烦一些。不幸的是,试错法(通过您的好朋友 Google)是跟踪所发生错误的惟一方法。好消息是:可以进行刷新,新的项目(如我们刚才创建的那个)将消除发生严重错误的可能性。
当 Tomcat 启动时,您不会看到任何东西(除了控制台内容)。您必须加以测试以确保它能工作。如果您需要一个快速指示,就设法打开浏览器并输入下列 URL:
http://localhost:8080/
如果一切正常,您就将看到一个精致的 Tomcat 欢迎页面,或一个列举了 Tomcat“启动内容”的目录。不必关注第二个。当我们运行第一个 servlet 时,我们将证明 Tomcat 在工作。
声明类
servlet 是一个类,因此,让我们创建一个基本的。在 Eclipse 中,要在 HelloWorld 项目中创建一个名为 HelloWorldServlet
的类。该类如下所示:
public class HelloWorldServlet extends HttpServlet { public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter writer = response.getWriter(); writer.println("Hello, World!"); writer.close(); } }
输入这些代码,然后按 Ctrl+Shift+O 组织一下导入语句。Eclipse 将允许您导入下列类:
java.io.IOException
java.io.PrintWriter
javax.servlet.ServletException
javax.servlet.HttpServlet
javax.servlet.HttpServletRequest
javax.servlet.HttpServletResponse
请注意,我们将 HttpServlet
作为子类,并重载了 service()
方法。service()
方法是 servlet 引擎将在 servlet 生命周期中调用的最基本的处理方法。它接收一个请求包装器和一个响应包装器,而我们可以在我们的方法中访问它们。但在这里,我们不需要这样做,因为我们只是进行一些基本操作来使 servlet 工作。我们可能重载了 doGet()
,但是 service()
将提供我们所需要的东西。
在我们的 service()
方法中,我们在 response
包装器上调用 getWriter()
,以便能够将一串文字输出到输出流中。然后,我们关闭输出流。这在生成输出的 servlet 中是很典型的:您执行需要执行的逻辑,然后写入输出流。
配置 Web 应用程序
Java 编程工作就完成了,但是现在,我们还必须对配置文件进行必要的工作。在我看来,这是 Web 开发的最大难点。幸好,该 Tomcat 插件分担了部分重担。
右击 HelloWorld 项目,并选择 Properties。选择属性的 Tomcat 类别。您将看到该项目的环境,如下所示:
/HelloWorld
现在,去查看您 Tomcat 原目录中的文件系统。转至 conf/Catalina/localhost 子目录。在那里,您将看到一组 XML 文件。具体地说,您将看到一个 HelloWorld.xml 文件。打开它。该文件为 Tomcat 定义了一个 Web 应用程序语境。
<Context path="/HelloWorld" reloadable="true" docBase="path to your project\HelloWorld" workDir="path to your project\HelloWorld\work" />
当 Tomcat 启动时,它读取这些上下文文件,告诉 servlet 容器在哪里找您的类(包括 servlet)。如果回顾加载 Tomcat 时向控制台发出的信息(INFO)语句,就会看到该列表中与 Web 应用程序上下文有关的信息。
在 Tomcat 中配置 Web 应用程序的最后一步是创建 web.xml 文件,需要将该文件放在项目的 WEB-INF 目录中。(注意:不要 将其放在 WEB-INF/src 目录中 —— 该目录将包含其他东西。)对于这个简单例子,该文件将如下所示:
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'> <web-app> <servlet> <servlet-name>hello</servlet-name> <servlet-class>HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
该文件向 Tomcat 定义了 Web 应用程序。该文件中的 servlet-name
元素命名了所使用的 servlet。servlet-class
元素将该名称映射到一个特定的类,该类定义了 servlet,即 HelloWorldServlet
(本示例中)。servlet-mapping
元素告诉 Tomcat /hello
(本例中)形式的 URL 映射我们的 servlet,这是由映射的 servlet 类定义的。
一旦设置好该文件,就可以启动 Tomcat 并能看到 servlet 加载。
运行 servlet
正如前面提到的,“运行 servlet”仅仅包括启动 Tomcat,以及将 Web 浏览器指向将调用它的 URL。通过适当的工具栏按钮启动 Tomcat(如果它已经运行,则需要停止并重新启动它)。一旦 Tomcat 结束启动过程,它会打开浏览器并输出下列 URL:
http://localhost:8080/HelloWorld/hello
您将在浏览器窗口中看到一条好消息。
动作 servlet
简介
在 Web 开发初期,许多专业编程人员都不得不弄清当他们继续时,如何较好地使用 servlet。最普遍的结果之一就是在服务器上暴露 servlet。每种类型的请求都有一个。
这很快就变得令人头痛,因此,编程人员开始在其 servlet 中包含条件逻辑使之更具适应性,以便处理多种类型的请求。一段时间后,这也产生了一些糟糕的代码。有一种更好的方式,称作动作 servlet(action servlet),它实现了名为模型 2 的概念。据我了解,该思想是由 David M. Geary(关于他的更多信息,请参阅 参考资料)首次写到的,但是它已经较好的用于流行的 servlet 库中了,例如 Jakarta Struts 项目。
在动作 servlet 中,并没有指示 servlet 行为的条件逻辑,而是具有动作(编程人员定义的类),servlet 授权这些类来处理不同类型的请求。大多数情况下,这个面向对象(OO)的方法要优于拥有多个 servlet,或在一个 servlet 中有多个 if
条件。
我们的示例动作 servlet 执行的操作
我们的示例动作 servlet 将是一个极简单的、基于浏览器的应用程序的网守(gatekeeper),该应用程序将允许我们创建、存储、查看以及删除合同列表项。这些记录项的格式都非常良好。最后,为了使用该应用程序,用户将必须登录它,但是,我们稍后将在 用户和数据 中添加这项功能。
设置该项目
在 Eclipse 中创建一个新的 Tomcat 项目,就像您为 HelloWorld 所做的一样。请注意,项目名称就是 servlet 默认的上下文值,因此,当输入访问 servlet 的 URL 时,将使用它。如果配置 Tomcat 使用上下文文件,那么它将为该项目自动创建一个上下文值。
Eclipse 还应创建一个具有正确结构的项目,并带有下列重要目录:
- WEB-INF
- WEB-INF/src
- work
第一个目录(WEB-INF)存储重要的配置文件,具体地说就是 web.xml 文件,我们稍后将讨论它。它还在 classes 目录中包含了编译的代码。第二个目录(WEB-INF/src)存储 Java 类的源代码。第三个目录(work)包含 JavaServer Pages(JSP)文件的编译代码,代码发生更改之后,每当我们第一次点击 JSP 页面时,Tomcat 就会为我们自动创建这些编译代码(我们将在下一面板上谈论更多 JSP 技术)。该项目的根目录包含所有的 JSP 源文件,以及数据库文件。
请注意,您可以在 Eclipse 的 Resource 视图中看到该结构的所有东西,但是在 Java Browsing 视图中只能看到 WEB-INF/src 和 work 目录。
所有这些文件都包含在本教程所包括的 contacts.jar 文件中(有关链接,请参阅 参考资料)。为了导入它们,只要创建一个新的 Tomcat 项目,然后导入 contacts.jar(使用 Import>Zip file 选项)即可。这将会在正确的位置中产生除源代码之外的所有文件。源代码最终会在项目根目录的 src 目录中产生。将该文件夹的内容移至 WEB-INF/src 中,您就完成了所有的准备工作。
表示
这毕竟是一篇关于 servlet 的教程,几乎与表示无关。然而,若不在屏幕某处看到一些结果,我们实际上就只告知了事情的部分内容。您当然可以编写根本不涉及表示的 servlet,但是大多数 Web 应用程序在浏览器中显示信息,这意味着您必须选择使用一种表示机制。JavaServer Pages 技术就是一种典型的备选方案,并得到了广泛采用。
通过 JSP 技术,您可以创建动态 Web 页面。它们支持静态 HTML(或其他标记,如 XML)和动态代码元素,而正如名字所隐含的,动态代码元素可以动态创建内容。在幕后,可以通过诸如 Tomcat 之类的容器将 JSP 页面编译成 servlet(即转换成 Java 代码)。然而,您几乎永远不必关心这一点。只需要知道发生了下列流程即可:
- 用户在浏览器中输入 URL,J2EE servlet 容器将该浏览器指向一个 servlet。
- servlet 完成其工作,并在会话中输入信息,或者在 bean 中,再发送给 JSP 页面。
- JSP 代码转换 bean 和/或会话中的信息,并将响应发送给浏览器。
您可以很容易地创建简单的 JSP 页面,只需要在 Web 应用程序中进行微小的修改即可,并且无需下载额外的代码库,就可以在 Tomcat 中运行它们,因此,我们将在这里使用它们(关于 JSP 技术的更多详细信息,请参阅 参考资料)。
我们的 Contacts 应用程序会有一个主要的 JSP 页面,列举现有的合同并添加新的合同。稍后,我们将添加用于登录和退出页面。
重要的是记得 JSP 技术只是一种表示选择。还有其他方法。受到极大欢迎的一种方法是 Jakarta Velocity 模板包(请参阅 参考资料)。JSP 技术存在一个主要的不足:复杂的、功能丰富的应用程序倾向于需要极其复杂的 JSP 页面,如果想使逻辑与表示分开,那么还需要进行额外的服务器工作来创建定制标签。另一个不足就是 JSP 技术经常带来了无法抑制的诱惑,将业务逻辑和表示混合,这容易导致需要繁重的维护工作的脆弱系统。
据我看来,JSP 技术常常是一个错误的选择,而 Velocity(或者其他某种模板化方法)通常是正确的。但对于我们这个简单例子,JSP 技术将起作用,可以说明我们需要介绍的概念。在这样的简单情况下,将一点点逻辑和一点点表示混合是可以接受的。但从专业的角度来说,多数情况下,这种做法是不明智的,即使许多编程人员都这样做。
web.xml 文件
为了让我们能够使用将要创建的 JSP 页面,我们必须告诉 Tomcat 如何处理该页面。因此,我们必须在 WEB-INF 目录中创建一个 web.xml 文件。如下所示:
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'> <web-app> <servlet> <servlet-name>contacts</servlet-name> <servlet-class>com.roywmiller.contacts.model2.ContactsServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>contacts</servlet-name> <url-pattern>/index.htm</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>contacts</servlet-name> <url-pattern>*.perform</url-pattern> </servlet-mapping> <servlet> <servlet-name>jspAssign</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>logVerbosityLevel</param-name> <param-value>WARNING</param-value> </init-param> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jspAssign</servlet-name> <url-pattern>/*.jsp</url-pattern> </servlet-mapping> </web-app>
我们为 HelloWorldServlet
创建了一个基本的 web.xml 文件,但是它非常小。随着应用程序变得更加复杂,web.xml 文件也不得不变得更智能。让我们快速分析该文件。
<servlet>
标签为 servlet 指定一个别名,我们将在该文件的别处使用它。它还告诉 Tomcat 实例化哪个类,以便在内存中创建 servlet。在我的 Eclipse 工作区中,我创建了 com.roywmiller.contacts.model2
包来保存该 servlet 类。无论需要什么,都可以调用我们的包,但是到 servlet 的路径必须匹配 <servlet-class>
元素中的内容。我们定义的第二个 servlet 是下载 Tomcat 时附带的,您不必修改它。它只是 JSP 正在处理的 servlet。
<servlet-mapping>
告诉 Tomcat 当某个 URL 到达服务器时,执行哪个 servlet。我们这里有三个映射。第一个将 Web 服务器查找的默认页面(<index.htm>
)映射到 servlet。第二个告诉 Tomcat 将以 .perform
结尾的 URL 映射到 servlet。该形式的 URL 将告诉 servlet 实现哪个动作(稍后,我们将更详细地讨论其工作方式)。第三个映射告诉 Tomcat 使用 JSP servlet 来处理 JSP 页面。
JSP 页面的用户视图
在我们的简单例子中,我们不会花太多时间谈论 JSP 技术。JSP 技术可以使事情简单,不会陷入一般表示的细节中,特别是不会陷入 JSP 技术细节中。(有关的更多信息,请再次参阅 参考资料。)我们还会将所有事情放置在一个页面上,即使这样做有些不太现实。这将最大程度地减少仅仅为了说明如何使用 servlet 的重要概念而必须创建的页面数。
我们的最初页面将显示合同列表,这将来自于一个包含了该列表的对象。它还将包含一个用于添加新合同的表单。该页将如图 5 所示。
图 5. 合同列表页面
虽然并非一件艺术作品,但该页在顶部按照良好的格式显示了所有合同。每一个页面都有 Delete 链接,用户可以单击它来删除特定的合同。该表单包含名称和地址值字段,以及关于合同类型(我们的简单示例中是 family 或 acquaintance)的单选按钮。这个简单页面将允许我们探索如何在 servlet 应用程序使用简单的动作框架。它还将让我们探索如何在用户会话期间使用请求,以及对 servlet 从浏览器接收的内容进行响应。
现在,我们准备创建该页面。
JSP 页面编码
下面是我们的 JSP 页面的代码:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <%@ page import="java.util.*" %> <%@ page import="com.roywmiller.contacts.model.*" %> <html> <head> <title>Contacts List 1.0</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <style type="text/css"> body, table, hr { color: black; background: silver; font-family: Verdana, sans-serif; font-size: x-small; } </style> </head> <body> <jsp:useBean id="contacts" scope="session" class="com.roywmiller.contacts.model.ContactList"/> <h2>Contact List 1.0</h2> <hr size="2"/> <table frame="below" width="100%"> <tr> <th align="left"></th> <th align="left">Name</th> <th align="left">Street</th> <th align="left">City</th> <th align="left">State</th> <th align="left">Zip</th> <th align="left">Type</th> </tr> <% List list = contacts.getContacts(); for (Iterator i = list.iterator(); i.hasNext();) { Contact contact = (Contact)i.next(); %> <tr> <td width="100"><a href="removeContactAction.perform?id=<%= contact.getId()%>" >Delete</a></td> <td width="200"><%=contact.getFirstname()%> <%=contact.getLastname()%></td> <td width="150"><%=contact.getStreet()%></td> <td width="100"><%=contact.getCity()%></td> <td width="100"><%=contact.getState()%></td> <td width="100"><%=contact.getZip()%></td> <td width="100"><%=contact.getType()%></td> </tr> <% } %> </table> <br/> <br/> <br/> <fieldset> <legend><b>Add Contact</b></legend> <form method="post" action="addContactAction.perform"> <table> <tr> <td>First Name:<td> <td><input type="text" size="30" name="firstname"></td> </tr> <tr> <td>Last Name:<td> <td><input type="text" size="30" name="lastname"></td> </tr> <tr> <td>Street:<td> <td><input type="text" size="30" name="street"></td> </tr> <tr> <td>City:<td> <td><input type="text" size="30" name="city"></td> </tr> <tr> <td>State:<td> <td><input type="text" size="30" name="state"></td> </tr> <tr> <td>Zip:<td> <td><input type="text" size="30" name="zip"></td> </tr> <tr> <td>Type:<td> <td><input type="radio" size="30" name="type" value="family"> Family <input type="radio" size="30" name="type" value="acquaintance" checked> Acquaintance</td> </tr> </table> <br/> <input type="submit" name="addContact" value=" Add "> </form> </fieldset> </body> </html>
此时,在该页面上看到的大多数内容可能是希腊文。我们不会对所有内容都进行详细讨论,但是在接下来的几屏中,我们将指出其中要点,便于您理解 servlet 将如何与该页面进行交互。
简单 JSP 页面的剖析
在 JSP 页面中,HTML 就是 HTML。Java 代码嵌在该页面中,如下所示:
<% Java code %>
为了在该页面中嵌入 Java 代码,必须告诉 JSP 页面这些类位于何处,就像您在 Java 类中所做的那样。可以用语句完成这项工作,如下所示:
<%@ page import="java.util.*" %>
我们的页面显示一个合同列表,该列表来自于一个 ContactList
实例,JSP 页面通过下列代码行可以了解该实例:
<jsp:useBean id="contacts" scope="session" class="com.roywmiller.contacts.model.ContactList"/>
这行代码告诉 JSP 页面在该页面的别处使用一个名为 contacts
的 bean。它是 com.roywmiller.contacts.model.ContactList
的实例,并拥有 session
作用域。
请注意,该页面的主体中有一个 Java for
循环:
List list = contacts.getContacts(); for (Iterator i = list.iterator(); i.hasNext();) { Contact contact = (Contact)i.next(); %> <tr> <td width="100"> <a href="removeContactAction.perform?id=<%= contact.getId()%>" >Delete</a> </td> <td width="200"><%=contact.getFirstname()%> <%=contact.getLastname()%></td> <td width="150"><%=contact.getStreet()%></td> <td width="100"><%=contact.getCity()%></td> <td width="100"><%=contact.getState()%></td> <td width="100"><%=contact.getZip()%></td> <td width="100"><%=contact.getType()%></td> </tr> <% }
这说明了 JSP 技术是如何混合 HTML 和 Java 语句的。在这里,我们将遍历 contact
对象的合同列表。每进行一次循环,就要向 HTML 表添加一个 <tr>
元素。在该表中,每个合同为一行,我们调用 Contact
实例的 getter 来填充表单元。对于第一个单元,需要为每一行创建一个 Delete 链接。我们将 href
属性设置为下列字符串:
removeContactAction.perform?id=<%= contact.getId()%>
当用户单击该链接时,先添加一个斜线(/
),然后,该字符串将追加到发送至服务器的 URL 的末尾处。问号是请求参数的定界符(delimiter),紧跟在 name=value
对之后。本例中,我们发送每个合同的 ID。
该页面别处也是发生相同的事情,例如,在表单中添加新合同。请注意 <form>
标签:
<form method="post" action="addContactAction.perform">
当用户单击 Add 按钮(表单底部的提交按钮)时,addContactAction.perform
被追加到 URL 中。
这就是全部工作!一些精妙的语法正是许多专业编程人员要么吝啬于使用 JSP 技术,要么创建各种 helper 类(例如定制的 JSP 标签)来易化页面的创建、读取和维护的部分原因。但既然我们有了该页面,就可以开始编写一些代码了。
创建 servlet
我们的 servlet 类似于 HelloWorldServlet
,并添加了动作处理功能:
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.roywmiller.contacts.actions.Action; public class ContactsServlet extends HttpServlet { protected ActionFactory factory = new ActionFactory(); public ContactsServlet() { super(); } protected String getActionName(HttpServletRequest request) { String path = request.getServletPath(); return path.substring(1, path.lastIndexOf(".")); } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Action action = factory.create(getActionName(request)); String url = action.perform(request, response); if (url != null) getServletContext().getRequestDispatcher(url).forward(request, response); } }
就像以前一样,我们扩展 HttpServlet
并重载 service()
方法。在该方法中,我们:
- 从导致调用 servlet 的 URL 中派生动作名。
- 基于该名称实例化正确的动作。
- 告诉该动作开始执行。
- 将响应发送给动作所指向的 URL。
我们从导致调用 servlet 的 URL 中派生动作名,而该 servlet 是从 request.servletPath()
获得的。请记住,导致我们调用动作的所有 URL 都具有 *.perform
的形式。我们将解析该形式来获得圆点左边的字符串,该字符串就是动作名,然后将该动作名传递给 ActionFactory
,以实例化正确的动作。现在,您看到我们为何告诉 Web 应用程序如何处理该形式的 URL,以及为何在 JSP 页面中使用这些“神奇”的字符串。正是因为这样,我们才可以在这里对它们进行解码,并采取对我们有利的动作。有什么替代方案?大量的 if
语句和大量的附加代码。正如我们将看到的,通过动作,需要执行的每个动作都已完全封装。
这样做很好,但是我们需要一些附加类来完成该任务。这就是动作框架要做的事。
简单的动作框架
我们的简单动作框架有 4 个主要组件:
ActionFactory
。该工厂将请求中的动作名转换成 servlet 可以用来完成其工作的动作类。Action
接口。该接口定义所有动作的极其简单的公共接口。- 名为
ContactsAction
的抽象类。该类实现了所有动作共用的一个方法,并强制子类实现另一个方法(perform()
)。 ContactsAction
的三个子类。这些子类使 servlet 能够进行自我引导、添加新合同和删除合同。
在 servlet 的 service()
方法中,该过程以 ActionFactory
开始。
ActionFactory
下面是我们的 ActionFactory
:
import java.util.HashMap; import java.util.Map; import com.roywmiller.contacts.actions.Action; import com.roywmiller.contacts.actions.AddContactAction; import com.roywmiller.contacts.actions.BootstrapAction; import com.roywmiller.contacts.actions.RemoveContactAction; public class ActionFactory { protected Map map = defaultMap(); public ActionFactory() { super(); } public Action create(String actionName) { Class klass = (Class) map.get(actionName); if (klass == null) throw new RuntimeException(getClass() + " was unable to find an action named '" + actionName + "'."); Action actionInstance = null; try { actionInstance = (Action) klass.newInstance(); } catch (Exception e) { e.printStackTrace(); } return actionInstance; } protected Map defaultMap() { Map map = new HashMap(); map.put("index", BootstrapAction.class); map.put("addContactAction", AddContactAction.class); map.put("removeContactAction", RemoveContactAction.class); return map; } }
ActionFactory
极其简单。它有一个 Map
动作类及其名称。我们在页面中使用该名称告诉 servlet 执行哪个动作。本例中,我们有三个动作:
BootstrapAction
AddContactAction
RemoveContactAction
记得要分别通过 Add 表单和 Delete 链接,将添加和删除合同的动作作为 URL 发送给 servlet。BootstrapAction
仅仅适用于将 /index.htm
调用至我们的动作框架中。
当告诉该工厂创建 Action
时,它将实例化该类,并把它送回实例。向该工厂添加新动作其实就是简单地为该动作创建一个类,然后在工厂的动作 Map
中添加新的条目。
Action
Action
接口如下所示:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface Action { public String perform(HttpServletRequest request, HttpServletResponse response); public void writeToResponseStream(HttpServletResponse response, String output); }
现在,我们将广泛使用的方法是 perform()
。而另一方法,writeToReponseStream()
允许动作直接写入响应的输出流,以传递给 JSP 页面。写入的任何内容(文本、HTML 等)都将在该页面上显示。我们暂时不需要使用该方法,但是,您可以在 ContactsAction
上获得它,以查看它如何工作。记得我们在 HelloWorldServlet
里使用了该方法体中的代码,因此,您不会对它感到陌生。
用 BootstrapAction 启动
我们拥有的 ContactsAction
的最简单的子类是 BootstrapAction
,它是其他子类的一个好模型:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class BootstrapAction extends ContactsAction { public String perform(HttpServletRequest request, HttpServletResponse response) { return "/" + "contactList.jsp"; } }
我们仅实现 perform()
来完成需要的工作。本例中,我们只要返回一个指向 contactList.jsp 的 URL(开头的斜线很重要,因此不要忘了它)。现在来回顾 ContactsServlet
上的 service()
方法:
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Action action = factory.create(getActionName(request)); String url = action.perform(request, response); if (url != null) getServletContext().getRequestDispatcher(url).forward(request, response); }
该动作要么返回一个 URL 字符串,要么写入输出流,在 JSP 页面上显示。如果动作返回 URL 字符串,那么这是 BootstrapAction
的功劳,我们可以获取 ServletContext
,在 URL 上向它请求 RequestDispatcher
,最后将请求和响应发送给 JSP servlet,以便构造该页面。在这之后,只要动作 servlet 没有写入 JSP 页面的 PrintStream
(现在已经关闭),它就会取回控制权,并完成剩下的工作。
如果需要,可以在 response
上调用 sendRedirect()
,而不是使用 RequestDispatcher
:
response.sendRedirect("http://...");
但是,这样做要付出代价。当使用调度程序时,我们将 request
和 response
授权给 JSP servlet,它还将转发现有的 HttpSession
。那样就保存了该会话的内容。转发给另一个 URL 就不会如此。此刻,当我们开始显示该页时,我们所关心的会话中没有任何东西,因此,效果是一样的。但是很快,保存会话内容就会很重要。
为了告诉动作框架这个新的可用动作,我们要向工厂的动作 Map
添加以下代码行:
map.put("index", BootstrapAction.class);
添加合同
一个仅显示页面却不允许您做任何事情的应用程序不是很有用。我们需要能够添加合同的页面。
为此,必须执行以下操作:
- 创建一个名为
AddContactAction
的类。 - 实现
perform()
,向 Web 会话中维护的ContactList
添加新的Contact
实例。 - 将新动作告诉工厂。
将动作告诉工厂就是在工厂的 map 中添加另一条目,正如我们用 BootstrapAction
所做的那样。
AddContactAction
类及其已实现的 perform()
方法如下所示:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.roywmiller.contacts.model.Contact; import com.roywmiller.contacts.model.ContactList; public class AddContactAction extends ContactsAction { public String perform(HttpServletRequest request, HttpServletResponse response) { Contact newContact = createContact(request); HttpSession session = request.getSession(); ContactList contacts = (ContactList) session.getAttribute("contacts"); contacts.addContact(newContact); session.setAttribute("contacts", contacts); return "/contactList.jsp"; } protected Contact createContact(HttpServletRequest request) { Contact contact = new Contact(); contact.setFirstname(request.getParameter(RequestParameters.FIRSTNAME)); contact.setLastname(request.getParameter(RequestParameters.LASTNAME)); contact.setStreet(request.getParameter(RequestParameters.STREET)); contact.setCity(request.getParameter(RequestParameters.CITY)); contact.setState(request.getParameter(RequestParameters.STATE)); contact.setZip(request.getParameter(RequestParameters.ZIP)); contact.setType(request.getParameter(RequestParameters.TYPE)); return contact; } }
在这里,我们要做的所有操作就是调用 createContact()
创建一个新的 Contact
,并将其实例变量设置为包含请求参数的相应值。然后,我们在 HttpSession
中向 ContactList
添加新的 Contact
。最后,我们告诉 servlet 返回 /contactList.jsp。
记住每当我们创建一个 Contact
时,构造函数就给它分配一个惟一的 ID。回顾一下 JSP 代码。您将看到与我们在该动作中做什么有关的两项重要事情。首先要注意的是,通过添加下列这个行,我们保证在会话中总是有一个 ContactList
实例:
<jsp:useBean id="contacts" scope="session" class="com.roywmiller.contacts.model.ContactList"/>
在第一次编译并显示(BootstrapAction
执行后发送至 JSP 页面的结果)JSP 页面时,它将实例化 ContactList
。该对象没有包含任何内容,因此,当我们启动应用程序时,合同列表显示为空。在 AddContactAction
中,我们修改该对象,添加新的合同信息,然后将它重新插入会话中。以后,当显示该页面时,它将读取 Contact
实例的 ContactList
列表,并显示它们。
第二,注意用来添加合同的表单,如下所示:
<form method="post" action="addContactAction.perform"> table with labels and text input fields <input type="submit" name="addContact" value=" Add "> </form>
该表单的动作致使在请求中将 addContactAction.perform
发送给 servlet。然后,它提取 addContactAction
的一部分作为动作名,在工厂中查找它,并创建 AddContactsAction
的一个实例。
删除合同
添加合同很重要,但是能够删除它们也同样重要。能够对合同进行编辑是很不错,但本教程只能做到这些。此外,添加编辑功能将像添加另一动作一样简单。因此,我们暂时只是添加删除合同的功能,然后转向更有意思的事情。
与前面一样,我们只要添加了一个新的动作类,并实现其 perform()
方法,告诉工厂添加了这个方法。我们还必须确保 JSP 代码在合适的时候告诉 servlet 调用该动作。
可以在 JSP 页面中查看合同表中每一行的 Delete
链接:
<a href="removeContactAction.perform?id=<%= contact.getId()%>" >Delete</a>
该链接告诉 servlet 请求工厂为名称 removeContactAction
提供正确的动作类。它还传递了该请求中一个名为 id
的参数,该请求中有一个为当前合同 ID 设置的值。
我们的类如下所示:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.roywmiller.contacts.model.ContactList; public class RemoveContactAction extends ContactsAction { public String perform(HttpServletRequest request, HttpServletResponse response) { int contactId = Integer.parseInt(request.getParameter("id")); HttpSession session = request.getSession(); ContactList contacts = (ContactList) session.getAttribute("contacts"); contacts.removeContact(contactId); session.setAttribute("contacts", contacts); return "/contactList.jsp"; } }
这里,我们要做的就是从会话中提取 id
参数和 ContactList
,告诉该列表删除具有该 id
的 Contact
,然后替换会话上的列表。最后但也是很重要的,我们要告诉 servlet 返回 contactList.jsp。
运行应用程序
如果好奇心还没有占上风,那么您现在就应该运行这个应用程序,看看它是如何工作的。
启动浏览器,并输入下列 URL:
http://localhost:8080/contacts/
如果 Tomcat 运行正确,您就应查看 contactList.jsp,其列表中没有合同。在 add 表单上的文本字段中输入一些值,然后并单击 Add 按钮。您将在该列表中看到新的合同,该合同名称的左边有一个 Delete 链接。除非您修改它,否则其类型将设为 Acquaintance(单选钮的默认类型选择)。为了简便起见,我们没有对该表单进行任何验证,因此,您可以输入所有字段值完全相同的多个合同。每个合同都有一个惟一的 ID,因此,每个合同将分开显示,您可以逐个删除它们。
说得简单点 —— 我们有了一个实用的 Web 应用程序!但我们无法保存合同列表,因此,每当启动该应用程序时,我们都必须重新输入它们。更糟的是,该应用程序的每位用户都有相同的合同列表。我们是可以通过添加对于惟一用户的支持,以及通过在文件中存储数据(可以工作的最简单的数据库),来解决这些问题。在下一小节中,我们将完成这两项工作。
用户和数据
增强应用程序
在这一小节中,我们将对代码和现有的 JSP 页面进行少量重构(refactor),以便能为惟一的用户处理持久存储的合同数据。简言之,我们进行下列工作:
- 创建一个
ContactsUser
对象。 - 为每个
ContactsUser
提供用户名、密码和合同列表。 - 修改 JSP 页面的
<jsp:useBean/>
标签以使用ContactsUser
。 - 添加 login.jsp 作为该应用程序的第一页。
- 修改 contactList.jsp,为登录用户提供友好的欢迎消息。
- 向 contactList.jsp 添加 Logout 链接,然后调用
LogoutAction
。 - 添加 goodbye.jsp 来显示个性化的再见消息。
- 添加
LoginAction
和LogoutAction
。 - 添加
UsersDatabase
来处理 usersDatabase.txt 中Contacts
的存储和检索。 - 通过重载 servlet 上的
init()
初始化ContactsDatabase
。 - 重载 servlet 上的
destroy()
,告诉UsersDatabase
关闭 usersDatabase.txt。
实际上并非如此糟糕。惟一真正较新的概念是使用文件(只是更多标准 Java 语言工作)以及指向新页面。所有动作处理机制都是相同的。这说明动作框架的功能强大,而创建该框架只需要花费一点点宝贵的时间。它完全不像 Jakarta 的 Struts 框架那样复杂(请参阅 参考资料),将 Struts 框架用于应用程序中所进行的工作可能有点小题大做。
ContactsUser
删除导入语句和存取程序之后,ContactsUser
对象将如下所示(您可以在 contacts.jar 中找到完整的源代码):
public class ContactsUser { protected String username = ""; protected String password = ""; protected List contactList = new ArrayList(); public ContactsUser() { } public ContactsUser(String username, String password, List contactList) { this.username = username; this.password = password; this.contactList.addAll(contactList); } public boolean hasContacts() { return !contactList.isEmpty(); } public void addContact(Contact aContact) { contactList.add(aContact); } public void removeContact(Contact aContact) { contactList.remove(aContact); } public void removeContact(int id) { Contact toRemove = findContact(id); contactList.remove(toRemove); } protected Contact findContact(int id) { Contact found = null; Iterator iterator = contactList.iterator(); while (iterator.hasNext()) { Contact current = (Contact) iterator.next(); if (current.getId() == id) found = current; } return found; } accessors... }
该类保存应用程序用户的有关信息。这通常就是它所要做的所有工作。它保存用户的用户名和密码,并维护该用户的合同列表。它允许动作框架中的各种动作为该用户添加和删除 Contact
。在这里,不带参数的构造函数被用于单元测试。另一个接收三个参数的构造函数才是应用程序用户所使用的。
您可能会问自己,“该类为何没有一个 ContactList
实例变量呢?”毕竟,我们早先花功夫创建了这样一个实例。我们为何不使用它呢?答案很简单,我们实际上不再需要该类。它包装了一个 ArrayList
,并为我们提供了一些辅助方法。这些辅助方法实际在 ContactUser
上更有意义。如果我们使用了 ContactList
,则需要通过拥有相同名称以及需要完成相同事情的 ContactUser
来调用它上面的方法。例如,如果ContactUser
拥有一个 ContactList
,并且将该实例变量命名为 contactList
,那么 addContact()
将如下所示:
public void addContact(Contact aContact) { contactList.addContact(aContact); }
在这里对其他对象进行授权有些愚蠢。因此,我们删除了 ContactList
类。那正是重构要做的全部工作。我们简化了代码,并减少了系统中类的数目,但仍能完成相同的任务。拥有 ContactList
是创建系统时的中间步骤。它允许我们启动并运行系统,并帮助我们创建动作框架。然后,它的有效寿命就结束了,我们将删除它。编写一些代码并不代表您必须永远维护它们。
修改 contactList.jsp
修改 JSP 页面来使用新的 ContactUser
十分简单。我们需要进行三处修改。
第一处就是修改 <jsp:useBean>
标签,如下所示:
<jsp:useBean id="user" scope="session" class="com.roywmiller.contacts.model.ContactsUser"/>
现在,页面将实例化 ContactsUser
,而非 ContactList
。
第二处修改就是更新页面中的表行构建逻辑,以使用新的 user
变量:
<% List list = user.getContacts(); for (Iterator i = list.iterator(); i.hasNext();) { Contact contact = (Contact)i.next(); %>
第三处修改就是为用户添加一个退出链接:
<a href="logoutAction.perform">Logout</a>
我们将该链接置于“Contacts 1.0”头旁边。当用户单击该链接时,servlet 将执行 LogoutAction
。
添加登录/退出页面
与其他页面相比,支持登录和退出的页面都十分简单。惟一的差别存在于 <body>
标签中。下面是 login.jsp:
<body> <h2>Contact List 1.0</h2> <hr size="2"/> <fieldset> <legend><b>Please Login</b></legend> <form method="post" action="loginAction.perform"> <table> <tr> <td>Username:<td> <td><input type="text" size="30" name="username"></td> </tr> <tr> <td>Password:<td> <td><input type="text" size="30" name="password"></td> </tr> </table> <br/> <input type="submit" name="login" value=" Login "> </form> </fieldset> </body>
该页面有一个表单,其中带有两个文本字段和一个提交按钮。当用户单击 Login 时,servlet 将执行 LoginAction
。
下面是 goodbye.jsp:
<body> <jsp:useBean id="user" scope="session" class="com.roywmiller.contacts.model.ContactsUser"/> <h2>Contact List 1.0</h2> <hr size="2"/> Goodbye <%= user.getUsername() %>! </body>
该页面调用 ContactsUser
bean 上的 getUsername()
来显示个性化的再见消息。
当用户用一个数据库中没有的用户名尝试登录时,应用程序将放弃登录,并将用户指向一个错误页面,如下所示:
<body> <h2>Contact List 1.0</h2> <hr size="2"/> <fieldset> <legend><b>Error</b></legend> There was an error: <%= session.getAttribute("errorMessage") %> </fieldset> </body>
这是我们拥有的最简单的页面。它使用可从所有 JSP 页面获得的默认 session
变量来显示出错消息。
添加 LoginAction
LoginAction
类如下所示:
public class LoginAction implements Action { public String perform(HttpServletRequest request, HttpServletResponse response) { String username = request.getParameter(USERNAME); String password = request.getParameter(PASSWORD); ContactsUser user = UserDatabase.getSingleton().get(username, password); if (user != null) { ContactsUser contactsUser = (ContactsUser) user; request.getSession().setAttribute("user", contactsUser); return "/contactList.jsp"; } else request.getSession().setAttribute("errorMessage", "Invalid username/password."); return "/error.jsp"; } }
该动作从请求中提取 username 和 password 参数,然后用 username/password 组合查看数据库中是否包含该用户。如果存在该用户,那么就将该用户置于会话中,并直接进入 contactList.jsp。如果数据库中没有该用户,那么就在会话上设置一条出错消息,并转至 error.jsp。
现在,添加动作对于我们而言应该很容易了。我们向动作工厂添加一个条目,如下所示:
map.put("loginAction", LoginAction.class);
在设置好页面之后,工厂会感知新动作,添加操作也就完成了。您应该能够运行该应用程序,并看到登录页面。当输入用户名和密码时,不管输入的是什么,您都会看到出错页面。等一会儿之后,您就可以通过有效的用户名和密码登录,并看到包含空合同列表的 contactList.jsp。
添加 LogoutAction
LogoutAction
类如下所示:
public class LogoutAction implements Action { public String perform(HttpServletRequest request, HttpServletResponse response) { UserDatabase.getSingleton().shutDown(); return "/goodbye.jsp"; } }
在这里,我们将告诉数据库执行 shutDown()
操作。UserDatabase
上的方法如下所示:
public void shutDown() { writeUsers(); } protected void writeUsers() { StringBuffer buffer = new StringBuffer(); Collection allUsers = users.values(); Iterator iterator = allUsers.iterator(); while (iterator.hasNext()) { ContactsUser each = (ContactsUser) iterator.next(); UserRecord record = new UserRecord(each); buffer.append(record.getFullRecord()); } writeText(buffer.toString()); } protected synchronized void writeText(String text) { Writer writer = null; try { writer = new FileWriter(usersFile.getAbsolutePath()); writer.write(text); } catch (Exception e) { throw new RuntimeException("Unable to append to file.", e); } finally { closeWriter(writer); } }
shutDown()
调用 writeUsers()
,该方法将迭代内存中保存的所有用户(当 servlet 对自身进行初始化时,将从我们读入该文件的地方开始),为每个用户创建一个 UserRecord
,然后将完整的字符串传递给 writeText()
。writeText() 将该字符串写入文件中,重写现有的内容。UserRecord
类是一个极好的辅助类,封装了文件中每条用户记录的所有烦杂的标记工作。您可以自己检查代码(关于完整的源代码清单,请参阅 contacts.jar)。
一旦关闭数据库,就可以告诉 servlet 发送 goodbye.jsp,显示个性化的再见。
userDatabase.txt 文件
大多数 Web 应用程序从某种“数据库”中访问数据。许多都使用行业级(industrial-strength)的 RDBMS,但文本文件也可以是数据库。它是可以工作的最简单的数据库。如果您将它包装得很好,并将访问细节隐藏在一个接口之后,而该接口使得应用程序中的其他类极易于访问这些数据,那么底层数据采用什么样的存储形式实际上就没什么关系。
在这个应用程序中,我们将使用一个文本文件。该文件将按照下列形式,为每位用户保存一行:
username password comma-delimited contact1 info|comma-delimited contactN info|...
该文件中的用户名将是明文,但出于安全考虑,密码将是 Base64 编码(绝对最简单)。合同条目将用逗号分隔。而合同本身将通过 |
字符分隔。这种格式没有什么特别。它只是执行我们需要它完成的工作,以允许我们易于解析该文件。
为了方便,我们将该文件放置在本项目的根目录中,以便该文件的路径简单直接。
为了使事情简单,该应用程序不支持用户维护功能,这意味着无法在应用程序中添加或删除用户。这就表示您必须手工将用户添加到 userDatabase.txt 中。例如,要添加一个名为 testuser
以及密码为 password
的用户,就要向该文件添加下列一行:
testuser cGFzc3dvcmQ=
每个条目中的密码都是通过 Base64 编码进行编码的。您可以在 contacts.jar 中使用 EncoderDecoder
类来计算您密码的编码版本。它的main()
方法允许您输入明文字符串,然后运行该类,在控制台上输出已编码的密码。
UserDatabase
UserDatabase
包装了与文本文件的交互。这个类的清单看上去很大,但是并不复杂(大部分让人感觉很复杂的东西是那些额外的 Java 编码内容,处理读写文件操作需要它们)。我们将在本面板上讨论一些要点(关于完整的代码清单,请参阅 contacts.jar)。
该类实现了 Singleton 模式,并且维护了一个实例,而所有用户则通过调用 getSingleton()
共享这个实例。
该类维护了 ContactsUser
的一个 Map
,该 Map 将用户名与密码的组合作为每个条目的密钥。任何东西都可以充当每个条目的键,但这个比较方便。
在 servlet 的 init()
方法中,我们将告诉 UserDatabase
数据库文件位于何处(基于 ServletContext
),然后告诉它通过调用initialize()
初始化它本身。该方法如下所示:
public void initialize() { usersFile = new File(databaseFilePathname); String allUsers = retrieveText(); StringTokenizer tokenizer = new StringTokenizer(allUsers, "\n"); while (tokenizer.hasMoreTokens()) { String userEntry = tokenizer.nextToken(); UserRecord record = new UserRecord(userEntry); put(new ContactsUser(record.getName(), record.getPassword(), record.getContactList())); } }
该方法通过调用 retrieveText
读入完整的文件,标记较大的字符串,为每个用户创建 UserRecord
,然后调用 put()
来在该 map 中放置新的 ContactsUser
。该方法的真正作用体现在调用 retrieveText()
和 put()
中:
protected synchronized String retrieveText() { BufferedReader bufferedReader = null; try { bufferedReader = new BufferedReader(new FileReader(usersFile.getAbsolutePath())); char charBuff[] = new char[(int) new File(usersFile.getAbsolutePath()).length()]; bufferedReader.read(charBuff); return new String(charBuff); } catch (Exception e) { throw new RuntimeException("Unable to read in the file.", e); } finally { closeReader(bufferedReader); } } protected void closeReader(BufferedReader bufferedReader) { try { if (bufferedReader != null) bufferedReader.close(); } catch (Exception ex) {} } public void put(ContactsUser user) { String userKey = user.getUsername() + user.getPassword(); users.put(userKey, user); }
retrieveText()
方法负责完成文件读取工作。它创建了一个 BufferedReader
,将整个文件内容读入到字符缓冲区中,然后将这些内容转换成一个 String
。在其 finally
子句中,它只调用 closeReader()
来完成该工作。writeText()
方法将输出写入文件,重写现有的内容。该方法隐藏了同一类型的文件交互细节。
put()
方法为用户创建密钥(用户名加上密码),并将该密钥插入用户的 map 中。
有效地使用 servlet
简介
本教程中,我们只涉及了用 servlet 可完成的浅层功能。Web 应用程序可以与您所能想像的一样复杂。尽管所有 Web 应用程序的底层机制基本上是相同的,如果用 Java 语言编写代码,servlet 将是核心部分。创建更为复杂的应用程序实质上就是使用更复杂的工具和库。
然而,许多编程人员会在这个地方犯错,从而导致创建出糟糕的 Web 应用程序。这一小节包含一些关于如何避免这些错误的建议。大多数具有 Web 开发经验的 Java 编程人员都赞同其中的一些建议。还有一些更具争议性的建议。无论在哪种情况下,它们都将帮助您较好地了解 servlet。
使用一个 servlet
如果您不能只使用一个 servlet,就使用尽可能少的数目。实际上,我建议您只使用一个,直到肯定无法再这样下去。应该不需很多 servlet,您肯定无需为每种类型的请求提供一个 servlet。
不要在 servlet 上耗费太长时间
在 servlet 中尽可能少花时间。
servlet 不是为业务逻辑提供的场所。只有糟糕的 OOP 设计才那样。将 servlet 考虑成以下两种事物之一:
- UI 后面的一个附加层,帮助服务器了解“事件”。
- 服务器前面的一个附加层,允许您将浏览器用作 UI。
无论将 servlet 视为哪一种事物,它都是您快速将问题分派到应用程序的其他部分、然后退出的一个地方。
使用动作
动作框架,即使是像本教程中所使用的一样简单,是一种功能强大的方法。它允许您采纳前面的意见:在 servlet 中花费尽可能少的时间。它还是很好的 OOP 设计。每个动作类完成一件事,或一组联系紧密的事情。
某些人认为这会分裂代码,使之更难以理解。我认为这种反对源于以下两件事:
- 人们不习惯于查看面向对象的代码,当您第一次开始查看它时,肯定会感觉它们支离破碎的。
- 人们更喜欢程序代码,即使该代码在对象中。
考虑其他选择。若没有动作(假设您不是简单地授权给其他对象,不调用“动作”),则必须使用大量的 if
语句。如果只有两三个 if 语句,代码可能还很容易阅读。但是如果您有 10 个 if 语句,则很可能令人感到目眩。要求您滚动多个屏幕的方法比较令人讨厌。它通常意味着该方法要进行太多工作。至少,您要将 service()
(或 doGet()
等)中完成的类似于动作的事情提取到其他方法中,要对这些方法起个好的名称,这样您才知道它们是做什么的。
使用动作。在需要添加功能时,尽管添加动作好了。
使用 service(),除非您不能使用它
servlet 世界中有许多人们声称,您不应重载 service()
。并且说您应该重载 doGet()
和/或 doPost()
,可以让它们相互调用,使代码类似于下面所示:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { statements } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); }
我不知道这些思想源于何处。Bruce Eckle 指出该思想是从 CGI 时期遗留下来的,当人们开始习惯注意进来的是 GET
还是 POST
时。(关于对他的理论的更详细版本的链接,请参阅 参考资料。)我从未听到不要使用 service()
的好理由。任何情况下,如果您使用 service(),然后确定它将比使用任何一种 doX()
风格都要好,那么请重构代码!在这个时候,就使用 service()
,因为它更简单。
不要混合表示和业务逻辑
对于简单的应用程序而言,生成复杂的 HTML 字符串来输出 JSP 页面的输出流很不错,但是创建功能更丰富的应用程序时,该方法要困难得多。
将表示放置在它所属的地方是一种比较明智的做法:在页面中。JSP 技术允许您完成该工作,但正如我前面所说的,它需要做大量工作才能使业务逻辑与表示分离。诸如 Velocity 之类的模板化引擎通常是更好的选择。无论您选择哪种方法,都要尽可能少地混合业务逻辑和表示。
很好地处理异常
我们没有在本教程中过多谈论这一点,但是异常处理在创建 Web 应用程序时变得很重要。没有比在服务器端发生一些意想不到的事情,然后在浏览器中看到隐藏的堆栈跟踪更让用户沮丧的了。其中一些跟踪可能极其迟钝、晦涩。追查它们可能令人很苦恼。
许多打包的 Web 应用程序开发库,如 Struts(请参阅 参考资料),都附带了用来处理动态消息(包括错误)显示的内置框架。您可以使用这些功能。
不要使用每种功能
您是否需要使用您正采用的 Web 应用程序开发框架或库中的每项功能呢?很可能不需要,每一种功能都使用将使代码比您所需要的要复杂得多。实际上,除了可以工作的最简单框架,我建议您根本不要使用别的框架。有时候您知道,对于您手头的问题而言,将要使用或需要使用的功能或框架可能有些小题大做。
当使用框架是可取的方法时,就使用它。不要假定您需要它,等待系统告诉您需要使用时再使用它。一些编程人员认为那是“糟糕的设计”。并非如此。假设您将需要一个特定的框架,甚至是需要该框架中的某个特定功能,那么这样的设计可能是过分设计。您应针对所需要的进行设计;设计通常会随着系统扩展而更改。在开发开始之前选择框架是无益的,如果该框架不支持或不允许完成您需要完成的一些工作,那会让您急得撞墙。
结束语
本教程中,您知道了 Java servlet,以及如何专业地使用它们。当然,本教程中的例子十分简单,但是它们说明了创建 Web 应用程序将用到的大部分 servlet 概念。还有更多可用的功能(配置等),但是几乎用 Java 语言编写的每个 Web 应用程序的核心都是一个或多个 servlet,它们在幕后的一台或多台服务器上充当业务逻辑的网守。
更重要的是,您了解了很好地使用 servlet 的一些技术。Web 应用程序常常会变成一大堆凌乱的代码。通过使用由基本 OOP 原则驱动的简单技术,您可以避免这种混乱,创建易于增强和维护的应用程序。
参考资料
-
- 下载本教程所附带的 contacts.jar。
- 从 Apache Jakarta Project 下载 Tomcat。
- 您还需要来自于 Sysdeo 的用于 Eclipse 的 Tomcat 插件。您一旦安装了该插件,就需要检查 Sysdeo 的 HelloWorld servlet 设置“教程”。
- David Geary 撰写的 Advanced JavaServer Pages(Pearson Higher Education,2001)是您必读的一本参考书。
- 如果您的 Web 应用程序对于简单的 JSP 组件来说太复杂,那么您可能希望研究 Velocity Template Engine 或 Struts Web Application Framework。这两个项目都由 Apache Foundation 主持。
- Java Technology 主页是“官方的”Java 语言资源。该站点的 Java Servlet 技术专区提供了关于用 servlet 进行编程的丰富信息。
- Sun 的 The Java tutorial 是一本优秀的参考资料。其中有对 servlet 的描述。
- developerWorks New to Java technology page是为刚入门 Java 开发人员提供的 developerWorks 资源交换中心,其中包括了教程和认证资源的链接。
- developerWorks Technology 文档库包含了到 developerWorks Java 学习资料的链接,包括诸如这篇和 Java 文章等附加教程。
- Bruce Eckle 思考了许多 Web 编程人员不愿重载
service()
的历史原因。 - 在 developerWorks Java 技术专区中,可以找到关于 Java 编程的各个方面的文章。
- 请访问 Developer Bookstore,获得技术书籍的完整清单,其中包括大量 Java 相关书籍。
- 关于 developerWorks 上免费的 Java 相关教程的完整清单,还请查看 Java 技术专区教程页面。