Servlet 简介



目录:

  1. 关于Servlets
  2. 怎样编写一个servlet程序
  3. 怎样用servletrunner运行一个Servlet程序

关于Servlets

Servlets是JAVA 2.0中新增的一个全新功能。

JAVA Servlets 是运行在请求/面向请求服务器上的模块,比如一个Java-enabled web 服务器, 和类似这样的延伸场合. 例如, 一个servlet可以从一个HTML订单表中获取数据然后用一些商业上的算法来更新公司相应的订单数据库。

也就是说:servlet能够象CGI脚本一样扩展WEB服务器功能,但是servlet占用很少密集资源,有很多用CGI脚本编制的一些站点由于访问量剧增,性能迅速下降,这是CGI脚本一个缺点,有关CGI脚本概念请参照本斋"CGI入门学习" 。同时由于servlet是用java编写的,因此是跨平台的。
实际servlet是电子商务真正的开始。

Servlet API, 是用来写servlet的, 编写servlet是已没有CGI脚本那样诸如关心一个servlet是这样被装载, servlet运行的服务器环境是什么, 或者用来传输数据的协议是什么等等,这样servlets就可以融合在不同的web服务器中.

Servlet可以相当有效地替代CGI脚本: 它可以方便地产生容易编写而且运行快的动态文本. 可以很方便的调试寻找出程序问题. Servlet程序是用Java Servlet API开发的, a standard Java extension. 但不是Java核心框架的一部分,可以作为通用的附加产品包被商家购买使用.

举例

下面是一些servlet应用范围:

  • 用于处理HTML表单通过HTTPS产生POSTed数据, 包括买卖订单或信用卡数据. 因此servlet可以成为订单处理系统的一部分, 和产品存货数据库一道工作,也许可以用在在线支付系统上.
  • 允许人们之间的合作. 一个servlet能并发处理多个请求; 他们可以使用在诸如在线会议这样的同步请求支持系统.
  • 转送请求. Servlet可以转送请求给其他的服务器和servlets. 这就允许在镜象同样内容的几个服务器之间平衡负载. 按照任务类型或组织范围,可以允许被用来在几个服务器中划分逻辑上的服务器.
  • servlet 编写者们可以定义彼此之间共同工作的激活代理,每个代理者是一个servlet, 而且代理者能够在他们之间传送数据.

Servlet 结构总视

在具体掌握servlet之前,须对java语言有所了解。下面是基于您了解java基础上的,在Servlet API中最重要的是Servlet interface. 所有的servlets implement(执行)这个interface, 方式多种:或者是直接的,或者通过extending 这个class执行它,如 HttpServlet. 这个Servlet interface 提供安排servlet与客户端联系的方法. Servlet 编写者可以在他们开发servlet程序时提供更多一些或所有的这样方法.

当一个servlet接收来自客户端的调用请求, 它接收两个对象: 一个是ServletRequest,另外一个是ServletResponse. 这个ServletRequest class 概括从客户端到服务器之间的联系, 而 ServletResponse class 概括从servlet返回客户端的联系.

ServletRequest interface 可以获取到这样一些信息如由客户端传送的阐述名称,客户端正在使用的协议, 产生请求并且接收请求的服务器远端主机名. 它也提供获取数据流的servlet, ServletInputStream, 这些数据是客户端引用中使用HTTP POST 和 PUT 方法递交的.  一个ServletRequest的子类可以让servlet获取更多的协议特性数据. 例如: HttpServletRequest 包含获取HTTP-specific头部信息的方法.

ServletResponse interface 给出相应客户端的servlet方法. 它允许servlet设置内容长度和回应的mime类型, 并且提供输出流, ServletOutputStream, 通过编写者可以发回相应数据. ServletResponse子类可以给出更多protocol-specific容量的信息。 例如: HttpServletResponse 包含允许servlet操作HTTP-specific头部信息的方法.

上面有关classes 和 interfaces描述构成了一个基本的Servlet框架. HTTP servlets有一些附加的可以提供session-tracking capabilities的方法. servlet编写者可以用这些API在有他人操作时维护servlet与客户端之间的状态. 

Servlet Lifecycle

服务器装载运行servlets:接收来自客户端的多个请求并且返回数据给客户端. 然后在删除移开servlets. 这就是servlets lifecycle过程. 下面详细描述:

当一个服务器装载servlet时, 它运行servlet的 init 方法. 这个方法不能反复调用,一旦调用就是再装载servlet. 直到服务器调用 destroy 方法卸载servlet后才能再调用.

在服务器装载初始化后servlet, servlet就能够处理客户端的请求. 用service 方法做到这一点. 每个客户端请求有它自己service方法: 这些方法接收客户端请求, 并且发回相应的响应.

Servlets能同时运行多个service. 这是很重要的, 这样, service方法可以按一个thread-safe 样式编写. 如:service方法更新servlet对象中的一个字段field, 这个字段可以同时存取的. 假如某个服务器不能同时并发运行service方法,也可以用SingleThreadModel interface. 这个 interface 保证不会有两个以上的线程threads并发运行.

Servlets一直运行到他们被服务器卸载。
在servlet的lifecycle中, 编写一个thread-safe编码以卸载servlet是很重要的。

编写Servlet

Servlets 执行 javax.servlet.Servlet interface. 当servlet编写者可以通过直接implement interface开发servlet, 但这样通常没有必要. 因为大多数servlet是针对用HTTP协议的web服务器, 这样最通用开发servlet办法是用 javax.servlet.http.HttpServlet 内.

HttpServlet 类通过extend GenericServlet基类执行 Servlet interface, 提供了处理HTTP协议的功能. 他的service方法支持标准HTTP/1.1请求.
 

一般地, 用HttpServlet指定的类编写的servlets可以多线程地并发运行service方法.

与客户端的交互性

Servlet编写者注意HttpServlet类有几个欠缺的方法,你可以自己定义方法中内容,但是必须使用这些方法名称以使servlet知道你想做什么,

  • doGet, 用于处理 GET、有条件的GET 和头部 HEAD请求
  • doPost, 用户处理 POST 请求
  • doPut, 用于处理 PUT 请求
  • doDelete, 用于处理 DELETE请求

HttpServlet的service方法, 一般地, 当它接收到一个OPTIONS请求时,会调用doOptions 方法, 当接收一个TRACE请求是调用doTrace . doOptions缺省执行方式是自动决定什么样的HTTP被选择并且返回哪个信息.

在你使用这些方法时,必须带两个阐述. 第一个包含来自客户端的数据HttpServletRequest. 第二个参数包含客户端的响应HttpServletResponse. 在下例中是这样的情况.

一个HttpServletRequest对象提供到达HTTP 头部数据, 也允许你获取客户端的数据. 怎样获取这些数据取决于HTTP端请求方法.

  • 不管任何HTTP方式, 你可以用 getParameterValues 方法, 这个用来返回特定名称的参数值.
  • 对于用 HTTP GET 请求的方式, 这个 getQueryString 方法将会返回一个可以用来解剖分析的.
  • 对于用HTTP POST, PUT, 和 DELETE请求的方式, 你有两种方法可以选择. 如果是文本数据,你能通过getReader方法用BufferedReader获取 ; 如果是二进制数据, 能通过getReader 方法用 ServletInputStream获取.

为了响应客户端, 一个HttpServletResponse对象提供返回数据给用户的两个方法. 你可以用getWriter 方法返回,或者 getOutputStream 方法以输出流返回. 你应该用getWriter返回文本数据,而用getOutputStream返回二进制数据.

在使用Writer 或 OutputStream之前, HTTP 头部应该先被设置. HttpServletResponse内提供这样一个方法,之后可以用writer 或 outputstream 将响应主体部分发回用户. 完成后要关闭 writer 或 output stream以便让服务器知道响应已经完毕. 

一个HTTP Servlet处理GET和HEAD方法的例子

public class SimpleServlet extends HttpServlet { 

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        // 首先设置头部
        res.setContentType("text/html");

        // 用 writer方法返回响应数据
        PrintWriter out = res.getWriter();
        out.println("<HEAD><TITLE> SimpleServlet Output</TITLE></HEAD><BODY>");
        out.println("<h1> SimpleServlet Output </h1>");
        out.println("<P>This is output is from SimpleServlet.");
        out.println("</BODY>");
        out.close();
    }

    public String getServletInfo() {
        return "A simple servlet";
    }

}

这个例子完整地现实了一个servlet. 

一个HTTP Servlet处理POST方式的例子

这里是个用HTML带POST表单的例子:

<html>
  <head><title>JdcSurvey</title></head>
  <body>
    <form action=http://demo:8080/servlet/survey method=POST>
      <input type=hidden name=survey value=Survey01Results>

      <BR><BR>How Many Employees in your Company?<BR>
        <BR>1-100<input type=radio name=employee value=1-100>
        <BR>100-200<input type=radio name=employee value=100-200>
        <BR>200-300<input type=radio name=employee value=200-300>
        <BR>300-400<input type=radio name=employee value=300-400>
        <BR>500-more<input type=radio name=employee value=500-more>

      <BR><BR>General Comments?<BR>
        <BR><input type=text name=comment>

      <BR><BR>What IDEs do you use?<BR>
        <BR>JavaWorkShop<input type=checkbox name=ide value=JavaWorkShop>
        <BR>J++<input type=checkbox name=ide value=J++>
        <BR>Cafe'<input type=checkbox name=ide value=Cafe'>

      <BR><BR><input type=submit><input type=reset>
    </form>
  </body>
</html>

这里的servlet将表单数据写入一个文件,并且用一个thank you信息响应用户. 这里servlet的方法,如下例:

      public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        // 首先设置响应的 "content type" 头部
        res.setContentType("text/html");

        //得到响应的 PrintWriter以返回文本给客户端.
        PrintWriter toClient = res.getWriter();

        try {
            //打开一个文件写入Survey的结果.
            String surveyName = req.getParameterValues("survey")[0];
            FileWriter resultsFile = new FileWriter(resultsDir
                + System.getProperty("file.separator")
                + surveyName + ".txt", true);
            PrintWriter toFile = new PrintWriter(resultsFile);

            // 从客户端得到表单数据 & 存贮在这个文件中
            toFile.println("<BEGIN>");
            Enumeration values = req.getParameterNames();
            while(values.hasMoreElements()) {
                String name = (String)values.nextElement();
                String value = req.getParameterValues(name)[0];
                if(name.compareTo("submit") != 0) {
                    toFile.println(name + ": " + value);
                }
            }
            toFile.println("<END>");

            //关闭文件.
            resultsFile.close();

            // 用一个thank you返回客户端
            toClient.println("<html>");
            toClient.println("<title>Thank you!</title>");
            toClient.println("Thank you for participating");
            toClient.println("</html>");

        } catch(IOException e) {
            e.printStackTrace();
            toClient.println(
                "A problem occured while recording your answers.  "
                + "Please try again.");
        }

        // 关闭writer; 响应完成.
        toClient.close();
    }

这个doPost方法是用getParameterNames和getParameterValues方法来从表单中获取数据的. 因为它返回文本给客户端, doPost 将调用 getWriter 方法. 在写入响应主体部分之前,它设置了响应头部字段的设置, 但响应完成后,关闭. 

Lifecycle 方法

重编Init 初始化方法

在初始化过程中, servlet应当准备好它要安排的一些资源, 以便这个servlet能够接收请求,做到这些可以不用考虑多线程, 因为在servlet初始化是只能是单进程的。 一旦初始化方法完成, servlet就能接收客户端的请求。 当然如果初始化不能成功,这个方法会扔出throw UnavailableException解释的.

初始化方法使用ServletConfig 对象作为参数. 这个方法应该保存这个对象, 以便它能有方法getServletConfig返回. 最简单的办法是,搞出一个新类,他的初始化方法数调用super.init. 如果确实这样做, 你就应当自己保存ServletConfig对象, 并且自己重编getServletConfig 方法以便它能从新的位置得到对象.

下面是个初始化方法的例子. 它是来自Survey Servlet的初始化方法, 从一个表单接收输入然后存储到文件中,为了存储survey信息, 它需要一个目录. 它以初始化参数接收这个目录. 

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

        //获取目录
        resultsDir = getInitParameter("resultsDir");

        //如果没有目录, 不处理客户端
        if (resultsDir == null) {
            throw new UnavailableException (this,
                "Not given a directory to write survey results!");
        }
    }

这里的初始化方法调用super.init 方法来管理安排ServletConfig对象. 这个初始化方法也设置了一个字段: resultsDir, 作为初始化参数提供的目录名. 如果没有目录名被提供, 这个 servlet扔出一个不适用的解释. 如果初始化方法成功完成,servlet将能处理客户端请求 

初始化参数

初始化参数的规定是一个服务器方面的规定。

如果初始化参数被规定, 都可以用同样的方法得到: 用 getInitParameter 方法. 这个方法将参数名作为自己的参数项. 

重编Destroy 方法

当服务器卸载一个servlet, 它将调用servlet的destroy方法. 这个destroy方法是与初始化方法相反,同时从内存中释放servlet.

并不是所有的调用了初始化init方法是也必须调用destroy方法.

对于大多数的servlets, 一些初始化的工作必须反做的. 如, 假设有一个servlet,它在初始化时打开一个数据库连接,他的destroy 方法如下显示:需要关闭这个连接的

    /**
     * 关闭数据库连接
     */
    public void destroy() {
        try {
            con.close();
        } catch (SQLException e) {
            while(e != null) {
                log("SQLException: " + e.getSQLState() + '\t' +
                    e.getMessage() + '\t' +
                    e.getErrorCode() + '\t');
                e = e.getNextException();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

关于一个Servlet中断涉及的多线程

但一个服务器卸载一个servlet, 它会在所有的service已经完成后调用destroy. 如果你的操作运行需要很长时间, 但destroy 被调用时还有线程在运行. 这个servlet编写者有责任确保所有的线程都已经完成;

长时间运行响应客户端请求的那些servlet应当保留当前有多少方法在运行的记录. 他的 long-running 方法应当周期性地轮询以确保他们能够继续运行下去. 如果servlet被destroy方法调用, 那么这个long-running 方法如果必要必须停止工作或清除.

举例, 变量serviceCounter用来统计有多少service方法在运行, 变量shuttingDown显示这个servlet是否被destory. 每个变量有它自己的获取方法:

public ShutdownExample extends HttpServlet {
    private int serviceCounter = 0;
    private Boolean shuttingDown;
    ...
    // serviceCounter
    protected synchronized void enteringServiceMethod() {
        serviceCounter++;
    }
    protected synchronized void leavingServiceMethod() {
        serviceCounter--;
    }
    protected synchronized int numServices() {
        return serviceCounter;
    }
    //shuttingDown
    protected setShuttingDown(Boolean flag) {
        shuttingDown = flag;
    }
    protected Boolean isShuttingDown() {
        return shuttingDown;
    }
}

这个service方法每次在它进入时要增加,而在它返回退出时要减少:

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        enteringServiceMethod();
        try {
            super.service(req, resp);
        } finally {
            leavingServiceMethod();
        }
    }

destroy方法应当检查serviceCounter, 如果存在长时间方式运行的话, 设置变量shuttingDown . 这个变量将会让那个正在处理请求的线程知道:该结束了,关闭吧! destroy 方法应当等待这几个service 方法完成, 这样就是一个清楚的关闭过程了.

    public void destroy() {
        /* 检查是否有线程在运行,如果存在,告诉他们stop. */
        if (numServices() > 0) {
            setShuttingDown(true);
        }

        /* 等待他们stop.  */
        while(numService() > 0) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e) {
            }
        }
    }

long-running 方法如必要应当检查这个变量,并且解释他们的工作:

    public void doPost(...) {
        ...
        for(i = 0; ((i < lotsOfStuffToDo) && !isShuttingDown()); i++) {
            try {
                partOfLongRunningOperation(i);
            } catch (InterruptedException e) {
            }
        }
    }

提供关于Servlet的信息

/**
 * This is a simple example of an HTTP Servlet.  It responds to the GET
 * and HEAD methods of the HTTP protocol.
 */
public class SimpleServlet extends HttpServlet { 

   ...

    public String getServletInfo() {
        return "A simple servlet";
    }
}

怎样用servletrunner来运行Servlet

一旦你写好你的 servlet, 可以运行在很多web服务器上, 或者在 servletrunner里.

属性

属性是一对key-value, 用作配置, 创建, 和servlet的初始化. 如, servlet.phone.code=PhoneServlet 的key 是 servlet.phone.code,他的value 是 PhoneServlet.

一个servlet有两个属性. 一个是servlet.name.code, 他的值是servlet的类名. 另一个是servlet.name.initargs, 他的值是保存获取servlet的初始化参数 

code 属性

servlet.name.code 属性用它类的名命名你的servlet. 如果你的servlet使用初始化参数,这个属性就必须的. 它允许服务器联合servlet 对象和他的初始化参数项,他们两有同样的名字name. 即使你的servlet没有使用初始化参数,也推荐使用这个属性,以便客户端能用它自己的名字达到servlet. 

Initargs 属性的语法

 servlet.name.initArgs 属性的值是保存初始化参数的值. 相应的一个参数的语法是:parameterName=parameterValue. 举例一个 phone servlet参数象下面:

servlet.phone.initArgs=\
        phonelist=servlets/phonelist

如果有多个初始化参数, 他们用,号间隔开,如:

servlet.dbdemo.initArgs=\
        username=fill_in_the_user,\
        password=fill_in_the_password,\
        owner=fill_in_the_name

属性文件

存在一个文件中的属性一般地叫"servlet.properties", 尽管但servletrunner运行时你可以规定另一个名字,这个文件应当保存所有将要运行的servlet的属性. 它应当是 plain text; you 可以在编辑器中创编它. 这里举个例子:

# phone servlet (sample.html)
servlet.phone.code=PhoneServlet
servlet.phone.initArgs=\
      phonelist=servlets/phonelist

# bulletin board servlet
servlet.bboard.code=BBoardServlet

# order entry servlet
servlet.dbdemo.code=OrderEntryServlet
servlet.dbdemo.initArgs=\
        username=fill_in_the_user,\
        password=fill_in_the_password,\
        owner=fill_in_the_name

用Servlet Runner

如果你要在web服务器上运行你的servlet, 请看相应服务器的说明书. 这里只解释怎样在一个随产品而带的servletrunner驱动程式环境中运行servlet.

这个servletrunner是个小的驱动工具, 它是多线程的, 这样它可以运行多个servlet. 但它在服务器启动时不自动启动的. 因为小,所以只有很小的资源开销.

这个servletrunner<JDK>/bin 目录中. 用-help 调用它会有下列信息出现。:

% ./bin/servletrunner -help
Usage: servletrunner [options]
Options:
  -p port     the port number to listen on
  -b backlog  the listen backlog
  -m max      maximum number of connection handlers
  -t timeout  connection timeout in milliseconds
  -d dir      servlet directory
  -r root     document root directory
  -s filename servlet property file name
  -v          verbose output
%

为了看见这些选项的缺省值,你可以用-v 开关调用它. 这将会启动runner. 在你得到信息后就会停止。

% ./bin/servletrunner -v
Server settings:
  port = 8080
  backlog = 50
  max handlers = 100
  timeout = 5000
  servlet dir = .
  document dir = .
  servlet propfile = .:servlet.properties

一旦 servletrunner执行, 你能运行通过在你的浏览器中直接调用他们,举例如下面:

http://machine-name:port/servlet/servlet-name

这里servlet-name 对应与您已经给你的servlet取名的名字. 如, 为了运行Phone Servlet, 他的属性servlet.phone.code=PhoneServlet, 你将用下面URL. (假设servletrunner运行在一个及其叫localhost, 在端口 8080, 这个 phone servlet驻留在servlet目录:

http://localhost:8080/servlet/phone

另一个例子, survey servlet, 作为提交表单的运行结果. 相应的servletURL是:

http://demo:8080/servlet/survey