天天看点

j2ee入门01——从一个简单例子开始1 servlet 简介 2 servlet开发例子3 总结

        本系列打算从最基础开始,详细介绍j2ee开发技术的原理及相应框架;

        本文例子来自如下文章《Java Servlet 技术简介》。

1 servlet 简介

1.1 servlet 的作用

当使用交互式 Web 站点时,您所看到的所有内容都是在浏览器中显示的。在这些场景背后,有一个 Web 服务器接收会话 中来自于您的请求,可能要切换到其他代码(可能位于其他服务器上)来处理该请求和访问数据,并生成在浏览器中显示的结果。

servlet 就是用于该过程的网守(gatekeeper)。它驻留在 Web 服务器上,处理新来的请求和输出的响应。它与表示无关,实际上也不它应该与表示有关。您可以使用 servlet 编写一个流,将内容添加到 Web 页面中,但那通常也不是一个好办法,因为它有鼓励表示与业务逻辑的混合的倾向。

1.2 servlet 的替代品

servlet 不是服务于 Web 页面的惟一方式。满足该目的的最早技术之一是公共网关接口(CGI),但那样就要为每个请求派生不同的进程,因而会影响效率。还有专用服务器扩展,如 Netscape Server API(NSAPI),但那些都是完全专用的。在 Microsoft 的世界里,有活动服务器页面(ASP)标准。servlet 为所有这些提供了一个替代品,并提供了一些好处:

它们与 Java 语言一样是与平台无关的。

它们允许您完全访问整个 Java 语言 API,包括数据访问库(如 JDBC)。

大多数情况下,它们内在地比 CGI 更高效,因为 servlet 为请求派生新的线程,而非不同的进程。

对于 servelet 有一个广泛的行业支持,包括用于最流行的 Web 和应用程序服务器的容器。

servlet 是对专业编程人员工具箱的强大补充。

1.3 但什么是 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() 来清除它。

1.4 如何“运行”servlet

“运行”servlet 就像运行 Java 程序一样。一旦配置了容器,使容器了解 servlet,并知道某些 URL 会致使容器调用该 servlet,该容器就将按照预定的次序调用生命周期方法。因此,运行 servlet 主要是指正确配置它,然后将浏览器指向正确的 URL。当然,servlet 中的代码正是发现有趣的业务逻辑的地方。您不必担心低层事件的进展,除非发生某种错误。

不幸的是,经常会发生 一些令人沮丧的错误,尤其是在设置 servlet 时。致使 servlet 应用程序令人头痛的最大原因就是配置文件。您无法有效地调试它们。您只能通过试错法弄清楚这些错误,比如尽力破译可能会或不会在浏览器中看到的错误消息。

 2 servlet开发例子

2.1 一个简单的例子

2.1.1 这个简单的 servlet 要完成的任务

第一个 servlet 将完成极少量的工作,但是它将暴露编写 servlet 的所有基本要求。它将在浏览器窗口中输出一些简单的无格式文本:

Hello, World!

2.1.2 声明类

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();
     }
}
           

2.1.3 配置 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>
           

2.2 动作 servlet

2.2.1 简介

在 Web 开发初期,许多专业编程人员都不得不弄清当他们继续时,如何较好地使用 servlet。最普遍的结果之一就是在服务器上暴露 servlet。每种类型的请求都有一个。

这很快就变得令人头痛,因此,编程人员开始在其 servlet 中包含条件逻辑使之更具适应性,以便处理多种类型的请求。一段时间后,这也产生了一些糟糕的代码。有一种更好的方式,称作动作 servlet(action servlet),它实现了名为模型 2 的概念。据我了解,该思想是由 David M. Geary(关于他的更多信息,请参阅 参考资料)首次写到的,但是它已经较好的用于流行的 servlet 库中了,例如 Jakarta Struts 项目。

在动作 servlet 中,并没有指示 servlet 行为的条件逻辑,而是具有动作(编程人员定义的类),servlet 授权这些类来处理不同类型的请求。大多数情况下,这个面向对象(OO)的方法要优于拥有多个 servlet,或在一个 servlet 中有多个 if 条件。

2.2.2 我们的示例动作 servlet 执行的操作

我们的示例动作 servlet 将是一个极简单的、基于浏览器的应用程序的网守(gatekeeper),该应用程序将允许我们创建、存储、查看以及删除合同列表项。这些记录项的格式都非常良好。最后,为了使用该应用程序,用户将必须登录它,但是,我们稍后将在 用户和数据 中添加这项功能。

2.2.3 表示

这毕竟是一篇关于 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 技术将起作用,可以说明我们需要介绍的概念。在这样的简单情况下,将一点点逻辑和一点点表示混合是可以接受的。但从专业的角度来说,多数情况下,这种做法是不明智的,即使许多编程人员都这样做。

2.2.4 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 页面。

2.2.5 JSP 页面的用户视图

在我们的简单例子中,我们不会花太多时间谈论 JSP 技术。JSP 技术可以使事情简单,不会陷入一般表示的细节中,特别是不会陷入 JSP 技术细节中。(有关的更多信息,请再次参阅 参考资料。)我们还会将所有事情放置在一个页面上,即使这样做有些不太现实。这将最大程度地减少仅仅为了说明如何使用 servlet 的重要概念而必须创建的页面数。

我们的最初页面将显示合同列表,这将来自于一个包含了该列表的对象。它还将包含一个用于添加新合同的表单。该页将如图 5 所示。

图 5. 合同列表页面

j2ee入门01——从一个简单例子开始1 servlet 简介 2 servlet开发例子3 总结

虽然并非一件艺术作品,但该页在顶部按照良好的格式显示了所有合同。每一个页面都有 Delete 链接,用户可以单击它来删除特定的合同。该表单包含名称和地址值字段,以及关于合同类型(我们的简单示例中是 family 或 acquaintance)的单选按钮。这个简单页面将允许我们探索如何在 servlet 应用程序使用简单的动作框架。它还将让我们探索如何在用户会话期间使用请求,以及对 servlet 从浏览器接收的内容进行响应。

现在,我们准备创建该页面。

2.2.5 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()%>" target="_blank" rel="external nofollow" 
             >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>
           

2.2.6 创建 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 语句和大量的附加代码。正如我们将看到的,通过动作,需要执行的每个动作都已完全封装。

这样做很好,但是我们需要一些附加类来完成该任务。这就是动作框架要做的事。

2.2.7 简单的动作框架

我们的简单动作框架有 4 个主要组件:

ActionFactory。该工厂将请求中的动作名转换成 servlet 可以用来完成其工作的动作类。

Action 接口。该接口定义所有动作的极其简单的公共接口。

名为 ContactsAction 的抽象类。该类实现了所有动作共用的一个方法,并强制子类实现另一个方法(perform())。

ContactsAction 的三个子类。这些子类使 servlet 能够进行自我引导、添加新合同和删除合同。

在 servlet 的 service() 方法中,该过程以 ActionFactory 开始。

2.2.7.1 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 中添加新的条目。

2.2.7.2 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 里使用了该方法体中的代码,因此,您不会对它感到陌生。

2.2.7.3 用 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";
     }
}
           

其他添加合同和删除合同的代码,在这里就不贴了,有兴趣的到原文去找;

2.2.8 运行应用程序

如果好奇心还没有占上风,那么您现在就应该运行这个应用程序,看看它是如何工作的。

启动浏览器,并输入下列 URL:

http://localhost:8080/contacts/
           

如果 Tomcat 运行正确,您就应查看 contactList.jsp,其列表中没有合同。在 add 表单上的文本字段中输入一些值,然后并单击 Add 按钮。您将在该列表中看到新的合同,该合同名称的左边有一个 Delete 链接。除非您修改它,否则其类型将设为 Acquaintance(单选钮的默认类型选择)。为了简便起见,我们没有对该表单进行任何验证,因此,您可以输入所有字段值完全相同的多个合同。每个合同都有一个惟一的 ID,因此,每个合同将分开显示,您可以逐个删除它们。

说得简单点 —— 我们有了一个实用的 Web 应用程序!但我们无法保存合同列表,因此,每当启动该应用程序时,我们都必须重新输入它们。更糟的是,该应用程序的每位用户都有相同的合同列表。我们是可以通过添加对于惟一用户的支持,以及通过在文件中存储数据(可以工作的最简单的数据库),来解决这些问题。在下一小节中,我们将完成这两项工作。

2.3 用户和数据

2.3.1 增强应用程序

在这一小节中,我们将对代码和现有的 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 框架用于应用程序中所进行的工作可能有点小题大做。

2.3.2 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 是创建系统时的中间步骤。它允许我们启动并运行系统,并帮助我们创建动作框架。然后,它的有效寿命就结束了,我们将删除它。编写一些代码并不代表您必须永远维护它们。

2.3.3 修改 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" target="_blank" rel="external nofollow" >Logout</a>
           

我们将该链接置于“Contacts 1.0”头旁边。当用户单击该链接时,servlet 将执行 LogoutAction。

2.3.4 添加登录/退出页面

与其他页面相比,支持登录和退出的页面都十分简单。惟一的差别存在于 <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 变量来显示出错消息。

2.3.5 添加 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。

2.3.6 添加 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,显示个性化的再见。

2.4 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() 方法允许您输入明文字符串,然后运行该类,在控制台上输出已编码的密码。

2.4.1 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 中。

3 总结

根据以上的例子,我们可以猜测(其实是肯定)得出如下结论:

        1、所有请求的入口都是servlet;于是你会发现,不管是struct还是spring等,实际上在web.xml中配置的入口都是servlet;

        2、请求参数传递:用户请求某个地址时,参数是如何传入servlet的?从例子中我们可以看到参数是被包装在HttpServletRequest中了。这里会涉及到两个问题:1)验证参数,比如输入的数字不能是小数等;2)参数与对象之间的绑定;

            1)参数验证:方法主要有3种:1、前端js验证;2、获取HttpServletRequest参数时验证;3、转为类对象时验证;

            2)参与与类对象绑定:在大多数框架,会将传入参数绑定到类对象中,程序实际上处理的是类对象;

        3、动作框架:如果每个请求都配置一个servlet,过于混乱;如本文中所述设计一个动作框架,这个是比较初级的;通常框架会提供一个基础类来做这个事情,不需要用户设计;

        4、请求或者是结果处理:如果我想对请求做一些处理,比如设置编码方式,或者验证用户权限;这时候该如何处理?总不可能对每个请求处理的代码里面都加入相同的代码吧?对于这个处理,一般是用servlet的filter、或者是spring中的interceptor、或者利用aop来实现;

        5、表现层:对于用户看到的部分,使用哪种技术来实现,是jsp还是其他?对于选定的技术,实际上还需要解决,返回参数如何显示在jsp(或其他文件)上;

        6、数据存储:1)数据存储在数据库时,需要调用对应的程序,一般是通过调用jdbc来与数据交互;这里就涉及到一个重要问题,那就是事务;

                                2)数据源切换:通常的理论设计框架,都在鼓吹是否支持切换数据库,比如从oracle切换到mysql,也就是做到数据库无关性;(个人感觉这个需求似乎没那么强烈)

                                3)缓存:对实时性要求比较高的系统,所有数据都从数据库读取,显然效率太差;于是需要引入数据缓存机制,比如hibernate中ehance,或者redis等开源组件;

        7、监听:通常我们可能考虑在系统初始化时做一些事情,或者在某些事件发生时做一些事情。那么这个就需要用到监听了。实际上在web.xml中是可以配置listener的。

        8、上下文:一般的应用都要求做一些国际化等之类的事情,这个可能就需要维护上下文,用于支持国际化等;

        9、session和cookie:web开发这两个东西是免不了的了。

       10、日志:日志本质上不应该在这里提,因为它是属于实现的一部分,但是考虑到在系统bug分析、运维、用户行为分析(su) 

        以上基本覆盖了一个web框架需要做的事情,在分析各个框架时以这个思路去考虑其对应的配置到底是做什么的,就可以一目了然了,而不是被各种框架五花八门的实现给迷惑了。当然,在一些框架中可能还用到一些比较方便的功能:

        1、bean自动装配:如果系统有很多类,这些类都要在配置文件中进行配置显然是一个很繁琐的事情,于是自动装配bean就被提出来了。这个主要是利用java的注解和反射。

        2、分布式(多线程):由于web网站有可能访问量特别大,因此需要进行扩容,这里就设计分布式(多线程);