ITEEDU

第3章 第一个servlet

现在就让我们看看JavaServer体系结构的内部情况。在本章中,我们将要编写一个非常简单的servlet,它可以接收一个HTML POST请求,处理参数,并形成HTML结果发送给客户(也可以是Web浏览器)。如果你曾经用过CGI脚本,相信你一定会发现使用servlet确实容易得多。

3.1 基本流程

在讨论servlet的基本流程之前,让我们先来回顾一下JavaServer体系结构以及一个HTTP请求如何调用servlet。

如图3.1所示,客户(Web浏览器)向服务器发送一个请求以加载一个HTML页面。服务器上的HTTP Web服务将会接收该请求,识别出这是一个读HTML文件的请求,然后调用文件servlet实现实际的文件输入/输出。该HTML页面将会被返回给客户并在Web浏览器中显示。如果Web浏览器发出了HTML POST请求,那么HTTP Web服务将会再次收到该请求,如果POST请求要求加载一个servlet,那么该请求将会发送给调用者servlet,并由调用者servlet调用请求打开的servlet。之后,该servlet进行某种处理并将返回的数据通过HTTP发送回客户。

那么,HTTP Web服务是如何知道要调用setvlet呢?在客户端,你必须指定一个URL,它能命名你要调用的那个servlet。例如:
http://some.server.com/servlet/my_servlet?arguments

由于这个URL使用了HTTP作为协议,所以HTTP Web服务将会接收到这个请求。让我们看看Java Web服务是如何将这个URL转变成被调用的servlet。请回顾第2章我们讨论的服务器管理,你可能还记得Web服务中的servlet别名设置(参见图2.15)。正是通过该页面管理的servlet别名,使得别名“/servlet”被分派给servlet调用者。一旦Web服务从URL中找到了“/servlet”,调用者servlet就会被调用以响应该请求。

servlet的基本流程如下:

1.加载servlet。如果servlet尚未加载,调用者servlet将会将其解析并加载。请注意,servlet既可以保存在本地,也可以从远程主机加载——你可以通过Java Web服务器的HTTP Web服务的servlet控制页面(参见图2.25)来控制。servlet只加载一次。servlet的多个线程将处理客户的多个请求。

2.初始化servlet。servlet的init()方法被调用,以便servlet执行诸如连接数据库之类的初始化操作。init()方法只在servlet加载后调用一次,而且对servlet的其他任何调用都要在init()方法执行结束之后才能处理。

3.对于HTML POST请求,调用servlet的doPost()方法。

4.servlet执行某种处理并将通过输出流返回应答。

5.响应最初由HTTP Web服务接收。Web服务可能还会进行某种处理,例如servlet chaining以及Server-Side Include。在第4章和第5章中将仔细讨论这部分内容。

JavaServer体系结构使你很容易集中于实现需要功能的编码,你只要编写实现这些功能的servlet程序段就可以了,无须再考虑servlet的加载和卸载、HTTP协议的处理、链的执行以及Server-Side Include的调用等问题。servlet API在对要做的工作的划分方面做了很多工作。

3.2 在开始之前

在编写你的第一个servlet之前,你一定要确认如下软件已经安装:

·某个版本的Java Develope's Kit(JDK)。本书所附光盘中JDK是1.1.7版。Java2(JDK1.2)也行,不过建议不要使用1.1.4以前的版本。JDK用来编译你的servlet。请按照JDK中的安装指南安装它,并且确认环境变量PATH和CLASSPATH被正确设置。

·Servlet API类文件。你在编译你的servlet时还需要一些Servlet API类文件,你可以直接从JavaSoft上下载它们,或者安装Java Web服务器以及一些第三方厂商提供的servlet引擎(例如JRun和ServletExec)。Servlet API的JAR文件一定要加入到CLASSPATH中。

·一个支持servlet的Web服务器。它们包括Java Web服务器、Jigsaw、Apache以及其他一些有servlet引擎附件的Web服务器。要想得到一份支持servlet的Web服务器的完整列表,请访问JavaSoft的主页。附录B中还列出了一些关于支持servlet的Web服务器的信息的其他站点。

3.3 servlet的实例:Properties

为了更好地说明编写servlet是多么容易,我们先来看看这个servlet,它将把一个含有客户信息、所有递交的参数以及所有服务器系统属性的HTML页面返回给客户。

3.3.1 编写servlet

编写响应HTML请求的servlet只需要两步: 1.创建一个扩展了javax.servlet.http.HttpServlet接口的servlet类。javax.servlet.http.HttpServlet这个接口是javax.servlet.GenericServlet的扩展接口,它包含了分析HTTP首部和将客户端信息打包到javax.servlet.http.HttpServletRequest类中的那些代码。Servlet API的详细描述请参考附录。 2.重写doGet和doPost方法之一或全部。这里是servlet实际完成工作的地方。 servlet也可以重写init和destroy方法以实现servlet特殊的初始化和析构。重写init和destroy的典型例子就是在init方法中建立数据库连接并在destroy方法中断开它。我们的属性servlet(见图3.2)就是以上这些步骤的一个很好的例子。在init和destroy方法中没有完成任何动作,它们只是用来说明如何重写这两个方法。本书中所有示例的源程序都可以在随书光盘中找到。

      package javaservlets.samples;
      import javax.servlet.*;
      import javax.servlet.http.*;
      
      /**
      *
      <p>This is a simple servlet that will echo information about
  * the client and also provide a listing of all of the
  * system properties on the server.
  */

  public class Properties extends HttpServlet
  {
  /**
   * <p>Performs the HTTP POST operation
   *
   * @param req The request from the client
   * @param resp The response from the servlet
   */

   public void doPost(HttpServletRequest req,
             HttpServletResponse resp)
   throws ServletException, java.io.IOException
   {
    // Set the content type of the response
    resp.setContentType("text/html");

    // Create a PrintWriter to write the response
    java.io.PrintWriter out =
     new java.io.PrintWriter(resp.getOutputStream());

    // Print the HTML header
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Java Servlets Sample - Properties</title>");
    out.println("</head>");
    out.println("<h2><center>");
    out.println("Information About You</center></h2>");
    out.println("<br>");

    // Create a table with information about the client
    out.println("<center><table border>");
   
    out.println("<tr>");
    out.println("<td>Method</td>");
    out.println("<td>" + req.getMethod() + "</td>");
    out.println("</tr>");

    out.println("<tr>");
    out.println("<td>User</td>");
    out.println("<td>" + req.getRemoteUser() + "</td>");
    out.println("</tr>");
 
    out.println("<tr>");
    out.println("<td>Client</td>");
    out.println("<td>" + req.getRemoteHost() + "</td>");
    out.println("</tr>");

    out.println("<tr>");
    out.println("<td>Protocol</td>");
    out.println("<td>" + req.getProtocol() + "</td>");
    out.println("</tr>");
 
    java.util.Enumeration enum = req.getParameterNames();
    while (enum.hasMoreElements()) {
     String name = (String) enum.nextElement();
     out.println("<tr>");
     out.println("<td>Parameter '" + name + "'</td>");
     out.println("<td>" + req.getParameter(name) + "</td>");
     out.println("</tr>");
    }
        out.println("</table></center><br><hr><br>");
		// Create a table with information about the server
        out.println("<h2><center>");
    out.println("Server Properties</center></h2>");
    out.println("<br>");
		out.println("<center><table border width=80%>");
		java.util.Properties props = System.getProperties();
    enum = props.propertyNames();
		 while (enum.hasMoreElements()) {
     String name = (String) enum.nextElement();
     out.println("<tr>");
     out.println("<td>" + name + "</td>");
     out.println("<td>" + props.getProperty(name) + "</td>");
     out.println("</tr>");
    }
    out.println("</table></center>");

    // Wrap up
    out.println("</html>");
    out.flush();
   }

   /**
   * <p>Initialize the servlet. This is called once when the
   * servlet is loaded. It is guaranteed to complete before any
   * requests are made to the servlet
   *
   * @param cfg Servlet configuration information
   */

   public void init(ServletConfig cfg)
    throws ServletException
   {
    super.init(cfg);
   }

   /**
   * <p>Destroy the servlet. This is called once when the servlet
   * is unloaded.
   */

   public void destroy()
   {
    super.destroy();
   }
  } 

图3.2 属性servlet的代码清单 请注意这里是如何使用HttpServletRequest对象来收集客户信息的。另外,还要注意我们通过HttpServletResponse对象获得的输出流,把格式化的HTML发送给客户;通过创建使用这个输出流的PrintWriter对象,向客户发送格式化的HTML数据变得非常容易。

3.3.2 配置服务器

为了运行我们的属性servlet,我们还要配置一下服务器。让我们来看一看配置Java Web服务器以使用属性servlet。回忆一下第2章,我们使用管理工具来配置servlet的属性。图3.3显示了Web服务的servlet控制页面。 我们将这个servlet称为“properties”(属性)(如图3.4中的ACTION标记),当然你也可以使用喜欢的名字,servlet的名字与servlet类的名字并不一定要一致。接下来,servlet的类文件要被放在Java Web服务器的CLASSPATH中。是不是很奇怪?Java Web服务器设置了它自己的CLASSPATH,而没有使用服务器的CLASSPATH环境变量。关于如何修改Java Web服务器的CLASSPATH,请参阅Java Web服务器的文档。另外,你总是可以把servlet类文件放在Java Web服务器根目录下的/servlet目录里。在我们的例子中,properties servlet在包javaservlets.samples中,所以Properties.class文件必须被放在/JavaWebServer/servlets/javaservlets/samples目录中。

3.3.3 编写调用servlet的HTML

为了调用servlet,我们还得创建一个用servlet的URL POST请求的HTML页面。图3.4所示的HTML将等到用户按下一个按钮之后再提交POST请求。

<html>
  
    <head>
    </h3>
  </h3>
  <title>Java Servlets Sample - Properties</title>
  </head>
  <body>

  <form METHOD="POST"
  ACTION="/servlet/Properties"
  ENCTYPE="x-www-form-encoded">
  <h2><center>Java Servlets Sample - Properties</center></h2>
  <hr>
  <p>Press the button below to call a sample servlet that will
  return information about you, and also list the system properties
  on the server. This is done via a simple servlet.
  <br><br>
  <center>
  <INPUT NAME="Test" TYPE="submit" VALUE="Test Properties servlet">
  </center>
  <br>
  <hr>
    </body>
  </html>

图3.4 调用properties的HTML清单 千万不要忘记把HTML文件放在客户可以访问的地方。一般它们会被放在Web服务器的公共HTML目录里,对于Java Web服务器,要把它们放在Java Web服务器主目录里的/public_html目录中。

3.3.4 看看它做得怎样

我们已经编好了这个servlet,在Java Web服务器中加入了它的配置,还写了调用它的HTML,并且把所有的文件放在了该放的位置。图3.5显示了加载servlet的HTML页面,图3.6显示了properties servlet返回的HTML页面。

3.3.5 处理问题

你也许还会遇到一些servlet的问题,处理这些问题的最好办法是什么呢?为了更好地回答这个问题,我们将遇到的故障分为两大类:逻辑错误和异

逻辑错误

如果你遇到了逻辑错误,解决问题的最好方法是用调试器发现和改正它。你可以使用一些第三方厂商(如Live Software和New Atlanta)提供的专门用来调试servlet的商业软件,或者使用你习惯的IDE(目前大部分IDE支持对servlet的连编和调试)。如果你不打算买调试器(或像我一样不喜欢使用IDE),你还可以使用log()方法,通过它你可以将文本和异常输出到log文件中。其中log文件的位置是由servlet引擎定义的。除此之外,你还可以用System.out.println()取得同样效果,不过其输出位置因servlet引擎而异。有些servlet引擎将输出直接发送到控制台,而有些servlet引擎将输出重定向到文件中去。

异常

你可以根据你的需要来处理你的servlet中出现的异常,不过处理非预期servlet的异常和处理一般性错误的方法有很大不同。比如,如果用户要求下载一个不存在的文件,最完美的方式是返回HTML状态码404(没找到)。你也可以生成一个HTML页面来告诉用户找不到这个文件,不过向浏览器返回状态码不是简单得多吗?更何况一些Web服务器甚至为你准备了很漂亮的一页给404(Java Web服务器加入了一个有趣的小Duke图像和一些友好的文字)。你所要做的一切就是在HttpServletResponse对象被传递给doGet()或者doPost()方法时用合适的状态码调用sendError()(参照附录A.5节 “javax.servlet.http.HttpServletResponse”,那里列出了全部的状态码)。在处理servlet内部的异常时,你也可以用SC_INTERNAL_SERVER_ERROR状态码调用sendError()方法,或者产生一个UnavailableException,以表明该servlet已经不可用。使用这个异常又分为两种:永久的和暂时的。暂时的UnavailableException造成servlet引擎在一段时间内阻塞某个对servlet的所有请求。一个使用数据库的servlet可能由于数据库每周要停机检查而造成异常,就是暂时的UnavailableException的一个很好的例子。在数据库停机期间,servlet接收到一个请求,这时它根据时间表上的时间产生一个UnavailableException,以便在数据库恢复正常时,恢复servlet的服务。附A.21是提供了对UnavailableException的完整描述。

3.3.6 servlet重新加载

如果你按照我前面的方法将Properties Servlet放在了“<HOME>/servlets”目录中(其中HOME是servlet引擎的主目录),那么,在你修改了这个servlet的时候,你会发现该servlet已经被自动地重新加载了。这种奇妙的功能被称为servlet的重新加载,它是由一个servlet引擎实现的custom类加载器实现的。为了提高性能,大多数Java类加载器会对其加载的每一个类进行高速缓存;而servlet引擎则有所不同,在servlet每一次接收请求时,servlet引擎将会检查它的时间戳,一旦发现servlet有所改变,立即重新加载。这样做的主要原因是为了减少开发和测试servlet的时间——每次修改之后你不必都把servlet引擎关闭再启动了。而且关闭servlet引擎经常是一件令人着急的工作。需要特别注意的是只有在不得已时servlet 的支持类才有可能被重新加载,对这些支持类的修改,并不会造成servlet的重新加载。还有就是“<HOME>/servlets”目录是自动被用来搜索servlet的,如果你把它也加入到CLASSPATH中,你会使servlet的重新加载功能失效,因为这样的话所有的servlet都会由标准的类加载器加载。 你还可以把servlet放在“<HOME>/classes”目录里。该目录被自动放入servlet引擎的CLASSPATH,但是其中的servlet会由标准的类加载器加载——当然就不会自动重新加载了。为什么要这样做呢?有人认为将servlet放在生产系统中可以提高性能,因为这样servlet引擎就无须在每次请求到来时进行时间戳检查了。实际性能的提高与你所使用的操作系统密切相关。 另外,不要忘了,只要你原意,你还可以修改servlet引擎的CLASSPATH以加入你的其他servlet目录。我想这样做的惟一理由就是你可能已经有了一个生产系统,这时又有一个servlet要使用其他实用工具类。你可以将这个servlet放在“/servlets”或“/classes”中,而设置servlet引擎能够找到所使用的实用工具类。

3.4 小结

本章介绍了如何编写一个简单的servlet,它接收一个HTML POST请求并且产生一个HTML页面作为应答。我们勾画了一个servlet的基本流程以及在JavaServer体系结构中一次HTTP请求的过程。此外,我们还了解了如何配置Java Web服务器以使用servlet,以及如何编写一个HTML来调用setvlet。 下一章,我们将会看到如何将几个servlet结合起来,形成某种链。我们还要讨论如何使用用servlet映射以及MIME类型将servlet链接在一起。