开始之前关于本教程在您最喜欢的 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 为所有这些提供了一个替代品,并提供了一些好处:
servlet 是对专业编程人员工具箱的强大补充。 但什么是 servlet?作为一名专业编程人员,您碰到的大多数 Java servlet 都是为响应 Web 应用程序上下文中的 HTTP 请求而设计的。因此, 在创建一个 Java servlet 时,一般需要子类 当然,HTTP 协议不是特定于 Java 的。它只是一个规范,定义服务请求和响应的大致式样。Java servlet 类将那些低层的结构包装在 Java 类中,这些类所包含的便利方法使其在 Java 语言环境中更易于处理。正如您正使用的特定 servlet 容器的配置文件中所定义的,当用户通过 URL 发出一个请求时,这些 Java servlet 类就将之转换成一个 容器(如 Tomcat)将为 servlet 管理运行时环境。您可以配置该容器,定制 J2EE 服务器的工作方式,而且您必须 配置它,以便将 servlet 暴露给外部世界。正如我们将看到的,通过该容器中的各种配置文件,您在 URL(由用户在浏览器中输入)与服务器端组件之间搭建了一座桥梁,这些组件将处理您需要该 URL 转换的请求。在运行应用程序时,该容器将加载并初始化 servlet,管理其生命周期。 当我们说 servlet 具有生命周期时,只是指在调用 servlet 时,事情是以一种可预见的方式发生的。换言之,在任何 servlet 上创建的方法总是按相同的次序被调用的。下面是一个典型场景:
如何“运行”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 项目中创建一个名为 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 将允许您导入下列类:
请注意,我们将 在我们的 配置 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./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 应用程序。该文件中的 一旦设置好该文件,就可以启动 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 中有多个 我们的示例动作 servlet 执行的操作我们的示例动作 servlet 将是一个极简单的、基于浏览器的应用程序的网守(gatekeeper),该应用程序将允许我们创建、存储、查看以及删除合同列表项。这些记录项的格式都非常良好。最后,为了使用该应用程序,用户将必须登录它,但是,我们稍后将在 用户和数据 中添加这项功能。 设置该项目在 Eclipse 中创建一个新的 Tomcat 项目,就像您为 HelloWorld 所做的一样。请注意,项目名称就是 servlet 默认的上下文值,因此,当输入访问 servlet 的 URL 时,将使用它。如果配置 Tomcat 使用上下文文件,那么它将为该项目自动创建一个上下文值。 Eclipse 还应创建一个具有正确结构的项目,并带有下列重要目录:
第一个目录(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 代码)。然而,您几乎永远不必关心这一点。只需要知道发生了下列流程即可:
您可以很容易地创建简单的 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./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> 我们为
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.*" %> 我们的页面显示一个合同列表,该列表来自于一个 <jsp:useBean id="contacts" scope="session" class="com.roywmiller.contacts.model.ContactList"/> 这行代码告诉 JSP 页面在该页面的别处使用一个名为 请注意,该页面的主体中有一个 Java 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 语句的。在这里,我们将遍历 removeContactAction.perform?id=<%= contact.getId()%> 当用户单击该链接时,先添加一个斜线( 该页面别处也是发生相同的事情,例如,在表单中添加新合同。请注意 <form method="post" action="addContactAction.perform"> 当用户单击 Add 按钮(表单底部的提交按钮)时, 这就是全部工作!一些精妙的语法正是许多专业编程人员要么吝啬于使用 JSP 技术,要么创建各种 helper 类(例如定制的 JSP 标签)来易化页面的创建、读取和维护的部分原因。但既然我们有了该页面,就可以开始编写一些代码了。 创建 servlet我们的 servlet 类似于 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); } } 就像以前一样,我们扩展
我们从导致调用 servlet 的 URL 中派生动作名,而该 servlet 是从 这样做很好,但是我们需要一些附加类来完成该任务。这就是动作框架要做的事。 简单的动作框架我们的简单动作框架有 4 个主要组件:
在 servlet 的 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; } }
记得要分别通过 Add 表单和 Delete 链接,将添加和删除合同的动作作为 URL 发送给 servlet。 当告诉该工厂创建 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); } 现在,我们将广泛使用的方法是 用 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"; } } 我们仅实现 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 字符串,那么这是 如果需要,可以在 response.sendRedirect("http://..."); 但是,这样做要付出代价。当使用调度程序时,我们将 为了告诉动作框架这个新的可用动作,我们要向工厂的动作 map.put("index", BootstrapAction.class); 添加合同一个仅显示页面却不允许您做任何事情的应用程序不是很有用。我们需要能够添加合同的页面。 为此,必须执行以下操作:
将动作告诉工厂就是在工厂的 map 中添加另一条目,正如我们用
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; } } 在这里,我们要做的所有操作就是调用 记住每当我们创建一个 <jsp:useBean id="contacts" scope="session" class="com.roywmiller.contacts.model.ContactList"/> 在第一次编译并显示( 第二,注意用来添加合同的表单,如下所示: <form method="post" action="addContactAction.perform"> table with labels and text input fields <input type="submit" name="addContact" value=" Add "> </form> 该表单的动作致使在请求中将 删除合同添加合同很重要,但是能够删除它们也同样重要。能够对合同进行编辑是很不错,但本教程只能做到这些。此外,添加编辑功能将像添加另一动作一样简单。因此,我们暂时只是添加删除合同的功能,然后转向更有意思的事情。 与前面一样,我们只要添加了一个新的动作类,并实现其 可以在 JSP 页面中查看合同表中每一行的 <a href="removeContactAction.perform?id=<%= contact.getId()%>" >Delete</a> 该链接告诉 servlet 请求工厂为名称 我们的类如下所示: 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"; } } 这里,我们要做的就是从会话中提取 运行应用程序如果好奇心还没有占上风,那么您现在就应该运行这个应用程序,看看它是如何工作的。 启动浏览器,并输入下列 URL: http://localhost:8080/contacts/ 如果 Tomcat 运行正确,您就应查看 contactList.jsp,其列表中没有合同。在 add 表单上的文本字段中输入一些值,然后并单击 Add 按钮。您将在该列表中看到新的合同,该合同名称的左边有一个 Delete 链接。除非您修改它,否则其类型将设为 Acquaintance(单选钮的默认类型选择)。为了简便起见,我们没有对该表单进行任何验证,因此,您可以输入所有字段值完全相同的多个合同。每个合同都有一个惟一的 ID,因此,每个合同将分开显示,您可以逐个删除它们。 说得简单点 —— 我们有了一个实用的 Web 应用程序!但我们无法保存合同列表,因此,每当启动该应用程序时,我们都必须重新输入它们。更糟的是,该应用程序的每位用户都有相同的合同列表。我们是可以通过添加对于惟一用户的支持,以及通过在文件中存储数据(可以工作的最简单的数据库),来解决这些问题。在下一小节中,我们将完成这两项工作。 用户和数据增强应用程序在这一小节中,我们将对代码和现有的 JSP 页面进行少量重构(refactor),以便能为惟一的用户处理持久存储的合同数据。简言之,我们进行下列工作:
实际上并非如此糟糕。惟一真正较新的概念是使用文件(只是更多标准 Java 语言工作)以及指向新页面。所有动作处理机制都是相同的。这说明动作框架的功能强大,而创建该框架只需要花费一点点宝贵的时间。它完全不像 Jakarta 的 Struts 框架那样复杂(请参阅 参考资料),将 Struts 框架用于应用程序中所进行的工作可能有点小题大做。 ContactsUser删除导入语句和存取程序之后, 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... } 该类保存应用程序用户的有关信息。这通常就是它所要做的所有工作。它保存用户的用户名和密码,并维护该用户的合同列表。它允许动作框架中的各种动作为该用户添加和删除 您可能会问自己,“该类为何没有一个 public void addContact(Contact aContact) { contactList.addContact(aContact); } 在这里对其他对象进行授权有些愚蠢。因此,我们删除了 修改 contactList.jsp修改 JSP 页面来使用新的 第一处就是修改 <jsp:useBean id="user" scope="session" class="com.roywmiller.contacts.model.ContactsUser"/> 现在,页面将实例化 第二处修改就是更新页面中的表行构建逻辑,以使用新的 <% 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 将执行 添加登录/退出页面与其他页面相比,支持登录和退出的页面都十分简单。惟一的差别存在于 <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 将执行 下面是 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> 该页面调用 当用户用一个数据库中没有的用户名尝试登录时,应用程序将放弃登录,并将用户指向一个错误页面,如下所示: <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 页面获得的默认 添加 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
public class LogoutAction implements Action { public String perform(HttpServletRequest request, HttpServletResponse response) { UserDatabase.getSingleton().shutDown(); return "/goodbye.jsp"; } } 在这里,我们将告诉数据库执行 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); } }
一旦关闭数据库,就可以告诉 servlet 发送 goodbye.jsp,显示个性化的再见。 userDatabase.txt 文件大多数 Web 应用程序从某种“数据库”中访问数据。许多都使用行业级(industrial-strength)的 RDBMS,但文本文件也可以是数据库。它是可以工作的最简单的数据库。如果您将它包装得很好,并将访问细节隐藏在一个接口之后,而该接口使得应用程序中的其他类极易于访问这些数据,那么底层数据采用什么样的存储形式实际上就没什么关系。 在这个应用程序中,我们将使用一个文本文件。该文件将按照下列形式,为每位用户保存一行: username password comma-delimited contact1 info|comma-delimited contactN info|... 该文件中的用户名将是明文,但出于安全考虑,密码将是 Base64 编码(绝对最简单)。合同条目将用逗号分隔。而合同本身将通过 为了方便,我们将该文件放置在本项目的根目录中,以便该文件的路径简单直接。 为了使事情简单,该应用程序不支持用户维护功能,这意味着无法在应用程序中添加或删除用户。这就表示您必须手工将用户添加到 userDatabase.txt 中。例如,要添加一个名为 testuser cGFzc3dvcmQ= 每个条目中的密码都是通过 Base64 编码进行编码的。您可以在 contacts.jar 中使用 UserDatabase
该类实现了 Singleton 模式,并且维护了一个实例,而所有用户则通过调用 该类维护了 在 servlet 的 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())); } } 该方法通过调用 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); }
有效地使用 servlet简介本教程中,我们只涉及了用 servlet 可完成的浅层功能。Web 应用程序可以与您所能想像的一样复杂。尽管所有 Web 应用程序的底层机制基本上是相同的,如果用 Java 语言编写代码,servlet 将是核心部分。创建更为复杂的应用程序实质上就是使用更复杂的工具和库。 然而,许多编程人员会在这个地方犯错,从而导致创建出糟糕的 Web 应用程序。这一小节包含一些关于如何避免这些错误的建议。大多数具有 Web 开发经验的 Java 编程人员都赞同其中的一些建议。还有一些更具争议性的建议。无论在哪种情况下,它们都将帮助您较好地了解 servlet。 使用一个 servlet如果您不能只使用一个 servlet,就使用尽可能少的数目。实际上,我建议您只使用一个,直到肯定无法再这样下去。应该不需很多 servlet,您肯定无需为每种类型的请求提供一个 servlet。 不要在 servlet 上耗费太长时间在 servlet 中尽可能少花时间。 servlet 不是为业务逻辑提供的场所。只有糟糕的 OOP 设计才那样。将 servlet 考虑成以下两种事物之一:
无论将 servlet 视为哪一种事物,它都是您快速将问题分派到应用程序的其他部分、然后退出的一个地方。 使用动作动作框架,即使是像本教程中所使用的一样简单,是一种功能强大的方法。它允许您采纳前面的意见:在 servlet 中花费尽可能少的时间。它还是很好的 OOP 设计。每个动作类完成一件事,或一组联系紧密的事情。 某些人认为这会分裂代码,使之更难以理解。我认为这种反对源于以下两件事:
考虑其他选择。若没有动作(假设您不是简单地授权给其他对象,不调用“动作”),则必须使用大量的 使用动作。在需要添加功能时,尽管添加动作好了。 使用 service(),除非您不能使用它servlet 世界中有许多人们声称,您不应重载 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 时期遗留下来的,当人们开始习惯注意进来的是 不要混合表示和业务逻辑对于简单的应用程序而言,生成复杂的 HTML 字符串来输出 JSP 页面的输出流很不错,但是创建功能更丰富的应用程序时,该方法要困难得多。 将表示放置在它所属的地方是一种比较明智的做法:在页面中。JSP 技术允许您完成该工作,但正如我前面所说的,它需要做大量工作才能使业务逻辑与表示分离。诸如 Velocity 之类的模板化引擎通常是更好的选择。无论您选择哪种方法,都要尽可能少地混合业务逻辑和表示。 很好地处理异常我们没有在本教程中过多谈论这一点,但是异常处理在创建 Web 应用程序时变得很重要。没有比在服务器端发生一些意想不到的事情,然后在浏览器中看到隐藏的堆栈跟踪更让用户沮丧的了。其中一些跟踪可能极其迟钝、晦涩。追查它们可能令人很苦恼。 许多打包的 Web 应用程序开发库,如 Struts(请参阅 参考资料),都附带了用来处理动态消息(包括错误)显示的内置框架。您可以使用这些功能。 不要使用每种功能您是否需要使用您正采用的 Web 应用程序开发框架或库中的每项功能呢?很可能不需要,每一种功能都使用将使代码比您所需要的要复杂得多。实际上,除了可以工作的最简单框架,我建议您根本不要使用别的框架。有时候您知道,对于您手头的问题而言,将要使用或需要使用的功能或框架可能有些小题大做。 当使用框架是可取的方法时,就使用它。不要假定您需要它,等待系统告诉您需要使用时再使用它。一些编程人员认为那是“糟糕的设计”。并非如此。假设您将需要一个特定的框架,甚至是需要该框架中的某个特定功能,那么这样的设计可能是过分设计。您应针对所需要的进行设计;设计通常会随着系统扩展而更改。在开发开始之前选择框架是无益的,如果该框架不支持或不允许完成您需要完成的一些工作,那会让您急得撞墙。 结束语本教程中,您知道了 Java servlet,以及如何专业地使用它们。当然,本教程中的例子十分简单,但是它们说明了创建 Web 应用程序将用到的大部分 servlet 概念。还有更多可用的功能(配置等),但是几乎用 Java 语言编写的每个 Web 应用程序的核心都是一个或多个 servlet,它们在幕后的一台或多台服务器上充当业务逻辑的网守。 更重要的是,您了解了很好地使用 servlet 的一些技术。Web 应用程序常常会变成一大堆凌乱的代码。通过使用由基本 OOP 原则驱动的简单技术,您可以避免这种混乱,创建易于增强和维护的应用程序。 |
|