3.4 处理请求

在3.3节学习了如何创建、部署和运行一个Servlet组件,掌握了如何利用部署配置文件或者使用注解来配置Servlet,演示了如何编写了一个简单的Servlet来显示静态提示信息。Servlet编程的核心工作便是处理客户端提交的请求信息,生成动态响应信息返回到客户端。本节将深入研究在Servlet中如何处理客户端提交的请求信息。

3.4.1 请求参数

客户端提交的信息中最常见也是最重要的一类信息便是用户提交的请求参数。

在Web程序设计中,客户端以表单方式向服务器提交数据是最常见的方法。表单数据的提交方法有两种:Post方法和Get方法。Post方法一般用于更新服务器上的资源,当使用Post方法时,提交的数据包含在HTTP实体数据内。而Get方法一般用于查询服务器上的数据,当使用Get方法时,提交的数据附加在请求地址的后面,在浏览器的地址栏中可以看到。Servlet会自动将以上两种方法得到的数据进行处理,对于Post方法或Get方法提交的数据,Servlet的处理方法是一样的,用户只要简单地调用HttpServletRequest的getParameter方法,给出变量名称即可取得该变量的值。需要注意的是,变量的名称是大小写敏感的。当请求的变量不存在时,将会返回NULL。

下面演示Servlet如何处理客户端提交的信息。首先生成提交客户端信息的页面。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→“HTML”命令来生成提交数据的HTML页面login.html。页面模拟一个系统登录页面,用户名和密码信息通过表单提交到后台的Servlet处理,页面代码如程序3-5所示。保存并重新发布Web应用,打开IE浏览器,在地址栏中输入http://localhost:8080/Chapter3/login.html,页面显示如图3-12所示。

图3-12 提交表单数据的页面

程序3-5:login.html

    <! DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
    <html>
    <head>
    <meta "charset=UTF-8">
    <title>提交表单数据</title>
    </head>
    <body bgcolor="#FFFFFF">
    <h1 align="center"> <b>欢迎登录系统</b></h1>
    <form action="GetPostData" method ="post">
    <p> </p>
      <table width="52%" border="2" align="center">
        <tr bgcolor="#FFFFCC">
          <td align="center" width="43%"> <div align="center">用户名:</div></td>
          <td width="57%"> <div align="left">
            <input type="text" name="username">
          </div></td>
        </tr>
        <tr bgcolor="#CCFF99">
          <td align="center" width="43%"> <div align="center">密码:</div></td>
          <td width="57%"> <div align="left">
            <input type="password" name="password">
          </div></td>
        </tr>
      </table>
    <p align="center">
    <input type="reset" name="Reset" value="重置">
    <input type="submit" name="Submit2" value="提交">
    </p>
    </form>
    </body>
    </html>

下面生成处理客户端请求的Servlet。在“项目”视图中选中Web应用程序Chapter3,右击,在弹出的快捷菜单中选择“新建”→Servlet命令,弹出“新建Servlet”对话框。在包com.servlet中创建一个名为GetPostData的Servlet。Servlet的URL为“/GetPostData”。注意,URL必须和login.html页面中form对象的action的属性值一致,表单提交的信息才能发送到Servlet来处理。

下面要做的是为Servlet添加处理表单提交信息的代码。代码如程序3-6所示。

程序3-6:GetPostData.java

    package com.servlet;
    …
    @WebServlet(name = "GetPostData", urlPatterns = {"/GetPostData"})
    public class GetPostData extends HttpServlet {
    protected void processRequest(HttpServletRequest request, HttpServletResponse
    response)
                throws ServletException, IOException {
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter out = response.getWriter();
            try {
                out.println(
                        "<BODY BGCOLOR=\"#FDF5E6\">\n" +
                        "<H1 ALIGN=CENTER>" + "get post data " + "</H1>\n" +
                        "<UL>\n" +
                        " <LI><B>username</B>: "
                        + request.getParameter("username") + "\n" +
                        " <LI><B>password</B>: "
                        + request.getParameter("password") + "\n" +
                        "</UL>\n" +
                        "</BODY></HTML>");
                        } finally {
                out.close();
            }
        }
        protected void doPost(HttpServletRequest request, HttpServletResponse
        response)
                throws ServletException, IOException {
            processRequest(request, response);
        }
        …
        }

程序说明:doPost方法调用processRequest方法来处理客户提交的请求。方法processRequest的输入参数request是HttpServletRequest接口的实例,代表对Servlet发出的客户端请求。通过调用request的方法getParameter可以很方便地获取客户端请求参数。getParameter的参数为客户端请求参数名称。通过调用HttpServletResponse的方法getWriter可获得客户端显示对象PrintWriter,最后调用PrintWriter的println方法来显示获取的客户端请求参数。

保存程序后,重新发布Web应用。打开浏览器,在地址栏中输入http://localhost:8080/Chapter3/login.html,得到登录页面login.html。在“用户名”文本框中输入john,在“密码”文本框中输入123,单击“提交”按钮,表单数据被提交到Servlet GetPostData。返回的运行结果页面如图3-13所示,可以看到Servlet已经正确地获取到客户端提交的用户参数。

图3-13 显示获取的客户端的输入信息

Servlet获取客户端提交的信息就是这么简单。但不要高兴得太早,单击浏览器的“后退”按钮,回到如图3-12所示的提交表单数据页面。在“用户名”文本框中输入“张三”,在“密码”文本框中输入123,单击“提交”按钮,得到如图3-14所示的页面。页面中本来应该显示的汉字信息全部显示为乱码。

图3-14 提交汉字信息后的错误显示

造成这种现象的原因是客户端提交的参数值为汉字。不同于西文字母编码,每个汉字编码占2字节,而利用getParameter方法获取客户端请求变量,默认的编码方式是西文,如此得到的只是半个汉字,显示为乱码自然就不奇怪了。解决问题的方法很简单,根据程序3-5,开发人员知道页面的编码字符集为UTF-8,因此只需要在程序3-6的processRequest方法中第一行位置添加如下代码:

    request.setCharacterEncoding("UTF-8");

其中,调用setCharacterEncoding方法确保参数信息以UTF-8编码方式提取。

重新发布Web应用,打开浏览器,在地址栏中输入http://localhost:8080/Chapter3/login.html,调出提交表单数据页面。在“用户名”文本框中输入“张三”,在“密码”文本框中输入123,提交,得到结果如图3-15所示。可以看到汉字信息已经正确显示了。

图3-15 显示获取的客户端的汉字信息

表单提交的数据中的有些参数的值可能不止一个,如复选框对应的参数。如果参数有多个值,这时应该调用getParameterValues方法,这个方法将会返回一个字符串数组。

下面通过一个调查问卷的示例来说明如何获取请求中的多值变量。在Web应用项目中新建HTML页面multiChoice.html。页面包含进行调查问卷的一个复选框,如图3-16所示。页面完整代码如程序3-7所示。

图3-16 包含复选框的输入页面

程序3-7:multiChoice.html

    <! DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=gb2312">
    <title>多值变量提交</title>
    </head>
    <body bgcolor="#FFFFFF">
    选出你喜欢吃的水果:
    <form name="form1" method="post" action="multichoice">
    <input type="checkbox" name="checkbox1" value="苹果">
      苹果
    <input type="checkbox" name="checkbox1" value="西瓜">
      西瓜
    <input type="checkbox" name="checkbox1" value="桃子">
     桃子
    <input type="checkbox" name="checkbox1" value="葡萄">
      葡萄
    <input type="submit" name="Submit" value="提交">
    <input type="reset" name="reset" value="重置">
      </form>
    </body>
    </html>

下面创建处理客户端请求的Servlet。Servlet名称MultiChoiceServlet,所在的Java包为com.servlet,对应的URL模式为“/multichoice”。注意,URL必须和multiChoice.html页面中form对象的action的属性值一致,表单提交的信息才能发送到Servlet来处理。主要代码如程序3-8所示。

程序3-8:MultiChoiceServlet.java

    package com.servlet;
    …
    @WebServlet(name = "MultiChoiceServlet", urlPatterns = {"/multichoice"})
    public class MultiChoiceServlet extends HttpServlet {
    protected void processRequest(HttpServletRequest request, HttpServletResponse
    response)
              throws ServletException, IOException {
          request.setCharacterEncoding("UTF-8"); //解决编码问题
          PrintWriter out = response.getWriter();
          try {
              String[] paramValues = request.getParameterValues("checkbox1");
              Stringtemp = new String("");
              for (int i = 0; i < paramValues.length; i++) {
                  temp += paramValues[i] + "  ";
              }
              out.println("你喜欢吃的水果有:" + temp + "。");
          } finally {
              out.close();
          }
        }
      …
    }

程序说明:由于复选框对应的请求参数有多个值,程序调用request.getParameterValues ("checkbox1")获得参数值数组,然后采用遍历的方式,将其中的每个值取出显示。程序运行结果如图3-17所示。

图3-17 获取复选框提交的信息

3.4.2 Header

在接收到的请求信息中,除了用户提交的参数外,还有一类重要的信息称为Header,它代表客户端发出的请求的头部信息,相当于客户端和浏览器之间通信的控制信息,用来表示与请求相关的一些特定信息(如浏览器类型、客户端操作系统等)以及对服务器返回的响应的一些特殊要求(如可以接受的内容类型、编码格式等)。通过这些Header, Servlet将可以更加灵活地生成适应客户端需求的各种响应。

常见的Header信息如表3-1所示。

表3-1 常见Header说明

在Servlet中读取请求Header的值是很简单的,只要调用HttpServletRequest的getHeader方法就可以了,方法的参数为Header的名称,返回值为String类型的Header内容。如果指定的Header不存在,则返回Null。另外,某些Header如Accept-Charset、Accept-Language等可能对应多个值,此时可调用getHeaderNames将返回一个Enumeration,它代表指定名称的Header的所有值。

下面创建一个Servlet来显示请求中的各种Header信息,代码如程序3-9所示。

程序3-9:ShowRequestHeader.java

    package com.servlet;
    …
    @WebServlet(name = "ShowRequestHeader", urlPatterns = {"/ShowRequestHeader"})
    public class ShowRequestHeader extends HttpServlet {
     protected void processRequest(HttpServletRequest request, HttpServletResponse
     response)
              throws ServletException, IOException {
          response.setContentType("text/html; charset=UTF-8");
          PrintWriter out = response.getWriter();
          try {
              out.println(
                      "<HTML>\n"
                      + "<HEAD><TITLE>" + "显示Header信息"
                      + "" + "</TITLE></HEAD>\n"
                      + "<BODY BGCOLOR=\"#FDF5E6\">\n"
                    + "<H1 ALIGN=\"CENTER\">" + "显示Header信息" + "</H1>\n"
                      +  "<TABLE BORDER=1 ALIGN=\"CENTER\">\n"
                      +"<TR BGCOLOR=\"#FFAD00\">\n"
                      + "<TH>Header Name<TH>Header Value");
              Enumeration headerNames = request.getHeaderNames();
              while (headerNames.hasMoreElements()) {
                  String headerName = (String) headerNames.nextElement();
                  out.println("<TR><TD>" + headerName);
                  out.println(" <TD>" + request.getHeader(headerName));
              }
              out.println("</TABLE>\n</BODY></HTML>");
            } finally {
              out.close();
            }
        }
        …
    }

程序说明:在上面的代码中,调用HttpServletRequest的getHeaderNames获取当前请求的所有Header信息,返回值是一个Enumeration,程序中对它进行了遍历并通过表格来显示。

运行结果如图3-18所示。

图3-18 显示请求Header信息

从图3-18中可以看到,请求的Header中包含了主机名称、端口、cookie、客户端浏览器类型以及客户端接受的MIME类型和语言属性等。Servlet组件可以根据上述信息进行特殊的处理,如根据浏览器的类型来决定生成何种页面代码。

在使用Header信息时需要注意的是,在HTTP 1.1支持的所有Header中,只有Host是必需的,因此在调用getHeader(headerName)来获取Header信息的过程中,特别要注意返回的信息是否为Null。

3.4.3 上传文件

文件上传一直是Web应用中一个常见的功能需求,Servlet对文件上传提供了强大支持。HttpServletRequest提供了两个方法用于从请求中解析出上传的文件:

·Part getPart(String name)

·Collection<Part>getParts()

前者用于获取请求中指定name的文件(注意name指上传组件的名称而不是被上传文件的文件名),后者用于获取所有的上传文件。每一个文件用一个javax.servlet.http.Part实例来表示,它代表了上传的表单数据的一部分,可以有自己的数据实体、Header信息等。Part接口提供了处理文件的简易方法,比如write、delete等。因此,利用HttpServletRequest和Part来保存上传的文件变得非常简单,如下所示:

    Part photo=request.getPart("photo");
    photo.write("/tmp/photo.jpg");

下面通过一个示例来演示如何利用Part来实现文件上传。首先创建上传信息提交页面,如程序3-10所示。

程序3-10:upload.html

    <html>
        <head>
          <title></title>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        </head>
        <body>
          <form action="UpLoad" enctype="multipart/form-data" method ="post" >
              文件1<input type="file" name="file1"/>
              文件2<input type="file" name="file2"/>
              <input type="submit" name="upload" value="上传" />
          </form>
        </body>
    </html>

程序说明:页面主要提供一个上传界面,应注意form标记的三个属性,其中,action代表表单提交的URL地址;enctype代表表单内容的编码格式,要实现文件上传,必须设为multipart/form-data;同时method属性也必须设为post。

下面创建一个Servlet组件来处理上传文件信息,代码如程序3-11所示。

程序3-11:UploadServlet

    package com.servlet;
    …
    @WebServlet(name = "UpLoadServlet", urlPatterns = {"/UpLoad"})
      @MultipartConfig(location = "c:\\td")
      public class UploadServlet extends HttpServlet {
      protected void processRequest(HttpServletRequest request, HttpServletResponse
      response)
              throws ServletException, IOException {
          response.setContentType("text/html; charset=UTF-8");
          PrintWriter out = response.getWriter();
          try {
              response.setContentType("text/html; charset=UTF-8");
              for (Part p : request.getParts()) {
                  if (p.getContentType().contains("image")) {
                    String fname = getFileName(p);
                      p.write(fname);
                      System.out.println(fname);
                  }
                }
            } catch (Exception e) {
                System.out.println(e.toString());
            } finally {
                out.close();
            }
        }
        private String getFileName(Part part) {
            String header = part.getHeader("Content-Disposition");
            String fileName = header.substring(header.indexOf("filename=\"") + 10,
            header.lastIndexOf("\""));
            fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
            return fileName;
        }
    …
    }

程序说明:在上面的Servlet中,除了注解@WebServlet外,还增加了注解@MultipartConfig,用来声明Servlet可以处理multipart/form-data格式的请求,它的属性location用来设置上传文件的存放路径。注意这个路径必须是已经存在的,否则将抛出异常。

在processRequest方法中,调用HttpServletRequest接口的getParts方法获得请求上传的Part,然后利用Part的write方法将其写入服务器。为了获得上传文件名称,程序自定义了方法getFileName,其中上传文件的名称包含在Part实例的名为content-disposition的Header中。

值得一提的是,在上面的示例中,还通过调用getContentType方法对上传附件的类型进行判断,实现只允许保存图像类型的附件。开发人员还需要注意,在调用Part的write方法后,Part实例将被释放。还有一点需要注意:如果请求的MIME类型不是multipart/form-data,若调用HttpServletRequest的getPart(Stringname)或getParts方法,将会有异常抛出。

3.4.4 异步请求处理

由3.2节的内容可知,对于Servlet组件接收到的每个请求,都会产生一个线程来处理请求并返回响应。这就产生了一个问题:如果客户端的请求处理是一项比较耗时的过程,例如,需要访问Web服务或者后台数据库,则当有大量用户请求此Servlet组件时,在Web容器中将会产生大量的线程,导致Web容器性能急剧恶化。

为了解决这一问题,Servlet规范中提供了对请求的异步处理支持。在异步处理模式下,客户端请求的处理流程变为:当Servlet接收到请求之后,首先需要对请求携带的数据进行一些预处理;接着,Servlet线程将请求转交给一个异步线程来执行业务处理,Servlet线程本身返回至容器并可处理其他客户端的请求,注意此时Servlet还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有HttpServletRequest和HttpServletResponse对象的引用),或者将请求继续转发给其他Servlet。如此一来,Servlet线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。

异步处理特性可以应用于Servlet和Filter两种组件,关于Filter将在3.10节进行详细讨论。由于异步处理的工作模式和普通工作模式在实现上有着本质的区别,因此默认情况下,Servlet和Filter并没有开启异步处理特性,如果希望使用该特性,则必须按照如下的方式配置:

对于使用传统的部署描述文件(web.xml)的配置方式,Servlet为<servlet>和<filter>标签增加了<async-supported>子标签,该标签的默认取值为false;如果需要启用异步处理支持,则将其设为true即可。以Servlet为例,其配置方式如下所示:

    <servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>com.demo.MyServlet</servlet-class>
    <async-supported>true</async-supported>
    </servlet>

对于使用@WebServlet和@WebFilter的情况,这两个注解都提供了asyncSupported属性,默认该属性的取值为false,要启用异步处理支持,只需将该属性设置为true即可。以@WebServlet为例,其配置方式如下所示:

    @WebServlet(name  =  "AsyncServlet",  urlPatterns  =  {"/AsyncServlet"},
    asyncSupported = true)

下面通过一个示例来演示Servlet组件的异步处理特性。代码如程序3-12所示。

程序3-12:AsyncServlet.java

    package com.servlet;
    …
    @WebServlet(name  =  "AsyncServlet",  urlPatterns  =  {"/AsyncServlet"},
    asyncSupported = true)
    public class AsyncServlet extends HttpServlet {
        protected void processRequest(HttpServletRequest request,
        HttpServletResponse response)
              throws ServletException, IOException {
          response.setContentType("text/html; charset=UTF-8");
          PrintWriter out = response.getWriter();
          out.println("进入Servlet的时间:" + new Date() + ".");
          out.flush();
          //在子线程中执行业务调用,并由其负责输出响应,主线程退出
          AsyncContext ctx = request.startAsync();
          new Thread(new Executor(ctx)).start();
          out.println("<br>");
          out.println("结束Servlet的时间:" + new Date() + ".");
          out.flush();
          }
      public class Executor implements Runnable {
          private AsyncContext ctx = null;
          public Executor(AsyncContext ctx) {
              this.ctx = ctx;
          }
          public void run() {
              try {
                //等待30秒钟,以模拟业务方法的执行
                Thread.sleep(30000);
                PrintWriter out = ctx.getResponse().getWriter();
                out.println("<br>");
                out.println("业务处理完毕的时间:" + new Date() + ".");
                out.flush();
                ctx.complete();
              } catch (Exception e) {
                e.printStackTrace();
              }
          }
      }
      …
    }

程序说明:在processRequest方法中,首先调用HttpServletRequest的startAsync方法获得一个AsyncContext实例,它代表对当前请求处理的上下文环境的封装,然后将此实例作为参数传递到一个单独的线程中执行。

在新的线程中,通过调用AsyncContext的getResponse()可以获得对此请求处理响应对象,调用getRequest方法获得请求信息,因此可以与在Servlet中处理请求一样来进行业务逻辑操作。

运行程序,将得到如图3-19所示的运行结果。

图3-19 异步处理提示信息

从上面的运行页面可以看出,Servlet处理线程结束后,业务处理由异步线程托管依然在继续,最后才由异步线程将结果信息输出到用户界面。

3.4.5 异步IO处理

Servlet 3.0开始支持异步请求处理,但却只允许使用传统IO操作方式,如下面的代码片段所示:

    protected  void  doGet(HttpServletRequest  request,  HttpServletResponse
    response)
        throws IOException, ServletException {
      ServletInputStream input = request.getInputStream();
      byte[] b = new byte[1024];
      int len = -1;
      while ((len = input.read(b)) ! = -1) {
        //…
      }
    }

如果读取数据阻塞或者读取数据的速度很慢,那么Servlet的线程将会一直等待读取数据。这种情况在写数据的时候也会遇到。这一IO操作的局限将会限制Web容器的可扩展性。

Servlet 3.1开始加入对非阻塞IO(Nonblocking IO)的支持。非阻塞IO允许开发人员只在数据准备好的时候再进行读写操作。此功能不仅增加了服务器的扩展性,还增加了服务器可处理的连接数。但是需要注意的是,非阻塞IO只允许在异步Servlet和异步Filter中使用。

为支持非阻塞IO, Servlet 3.1引入了两个新的接口:ReadListener和WriteListener。

ReadListener有三个回调方法:

·onDataAvailable——在数据没有阻塞、已经完全准备好可以读取的时候调用。

·onAllDataRead——所有数据读取完成后调用。

·onError——处理中发生错误的时候调用。

WriteListener有两个回调方法:

·onWritePossible——当数据准备好进行无阻塞写出的时候调用。

·onError——当操作出错的时候调用。

Servlet中可对ServletInputStream调用setReadListener方法或者对ServletOutputStream调用setWriterListener方法来使用非阻塞IO取代传统IO。ServletInputStream的isFinished方法可以用于检查非阻塞读取的状态。注意ServletInputStream只允许注册一个ReadListener。

ServletOutputStream的canWrite方法可用于检测数据是否已经准备好进行非阻塞写出,同样,ServletOutputStream也只允许注册一个WriteListener。

下面通过一个示例来演示如何实现非阻塞IO的Servlet。该示例由两个Servlet和一个ReadListener来实现,分别如程序3-13、程序3-14和程序3-15所示。

程序3-13:ClientServlet

    package chapt3;
    …
    @WebServlet(name = "ClientServlet", urlPatterns = {"/ClientServlet"})
    public class ClientServlet extends HttpServlet {
        OutputStream output = null;
        InputStream input = null;
        protected void processRequest(HttpServletRequest request,
        HttpServletResponse response)
              throws ServletException, IOException {
          response.setContentType("text/html; charset=UTF-8");
          PrintWriter out = response.getWriter();
          out.println("<html>");
          out.println("<head>");
          out.println("<title>非阻塞IO演示</title>");
          out.println("</head>");
          out.println("<body>");
          String urlPath = "http://"
                  + request.getServerName()
                  + ":" + request.getLocalPort() //default http port is 8080
                  + request.getContextPath()
                  + "/ServiceServlet";
          URL url = new URL(urlPath);
          HttpURLConnection conn = (HttpURLConnection) url.openConnection();
          conn.setDoOutput(true);
          conn.setRequestMethod("POST");
          conn.setChunkedStreamingMode(2);
          conn.setRequestProperty("Content-Type", "text/plain");
          conn.connect();
          try {
              output = conn.getOutputStream();
              // 发送第一部分信息
              String firstPart = "hello...";
              out.println("Sending to server: " + firstPart + "</br>");
              writeData(output, firstPart);
              Thread.sleep(2000);
              // 发送第二部分信息
              String secondPart = "World...";
              out.println("Sending to server: " + secondPart + "</br></br>");
              out.flush();
              writeData(output, secondPart);
              Thread.sleep(2000);
              // 发送第三部分信息
              String thirdPart = "The End...";
              out.println("Sending to server: " + thirdPart + "</br></br>");
              out.flush();
              writeData(output, thirdPart);
              // 从服务器返回信息
              input = conn.getInputStream();
              printEchoData(out, input);
              out.println("Please check server log for detail");
              out.flush();
          } catch (IOException ioException) {
              Logger.getLogger(ReadListenerImpl.class.getName()).log
              (Level.SEVERE,
                    "Please check the connection or url path", ioException);
          } catch (InterruptedException interruptedException) {
              Logger.getLogger(ReadListenerImpl.class.getName()).log
              (Level.SEVERE,
                    "Thread sleeping error", interruptedException);
          } finally {
              if (input ! = null) {
                  try {
                    input.close();
                  } catch (Exception ex) {
                  }
              }
              if (output ! = null) {
                  try {
                    output.close();
                  } catch (Exception ex) {
                  }
              }
          }
          out.println("</body>");
          out.println("</html>");
        }
        …
        protected void writeData(OutputStream output, String data) throws
        IOException {
          if (data ! = null && ! data.equals("") && output ! = null) {
              output.write(data.getBytes());
              output.flush();
          }
        }
        protected void printEchoData(PrintWriter out, InputStream input) throws
        IOException {
          while (input.available() > 0 && input ! = null && out ! = null) {
              out.print((char) input.read());
          }
          out.println("</br>");
        }
    }

程序说明:程序用来模拟对服务器组件ServiceServlet发起IO请求操作。在processRequest方法中,首先创建一个与ServiceServlet的连接conn,并通过调用conn的setChunkedStreamingMode(2)方法设置此连接为分块传输模式,之后调用连接对象conn的getOutputStream方法获得ClientServlet的输出流(即ServiceServlet的输入流),就可以对输出流进行操作了。在发送完三段信息后,调用连接对象的getInputStream来获得ClientServlet的输入流并将输入结果显示出来。其中方法writeData和printEchoData是操作输入输出流的辅助方法。

程序3-14:ServiceServlet.java

    package chapt3;
    …
    @WebServlet(name = "ServiceServlet", urlPatterns = {"/ServiceServlet"} ,
    asyncSupported = true)
    public class ServiceServlet extends HttpServlet {
        protected void processRequest(HttpServletRequest request,
        HttpServletResponse response)
              throws ServletException, IOException {
          final AsyncContext context = request.startAsync();
          final ServletInputStream input = request.getInputStream();
          final ServletOutputStream output = response.getOutputStream();
          input.setReadListener(new ReadListenerImpl(input, output, context));
          System.out.println("ServiceServlet returned");
        }
        …
    }

程序说明:程序用来实现非阻塞IO操作。具体步骤是在processRequest方法中首先获得请求对象的ServletInputStream、ServletOutputStream和AsyncContext,并以此为参数来构造一个javax.servlet.ReadListener的实例对象,之后调用ServletInputStream的setReadListener并将新建的ReadListener的实例对象来实现异步IO。注意Servlet的注解中要加上属性asyncSupported并设置为true,因为非阻塞IO是在异步Servlet的基础上实现的。

程序3-15:ReadListenerImpl.java

    package chapt3;
    import java.io.IOException;
    import javax.servlet.AsyncContext;
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.ServletOutputStream;
    public class ReadListenerImpl implements ReadListener {
        private ServletInputStream input;
        private ServletOutputStream output;
        private AsyncContext context;
        private StringBuilder sb = new StringBuilder();
        ReadListenerImpl(ServletInputStream input, ServletOutputStream output,
        AsyncContext context) {
          this.input = input;
          this.output = output;
          this.context = context;
        }
          @Override
        public void onDataAvailable() throws IOException {
          System.out.println("Data is available");
          while (input.isReady() && ! input.isFinished()) {
              sb.append((char) input.read());
          }
          sb.append(" ");
        }
        @Override
        public void onAllDataRead() throws IOException {
              try {
              output.print("Total Received Bytes: " + sb.length() + "</br>");
              output.print("Received Contents: " + sb.toString() + "</br>");
              output.flush();
          } finally {
              context.complete();
          }
          System.out.println("Data is all read");
        }
        @Override
        public void onError(Throwable t) {
          context.complete();
          System.out.println("--> onError");
        }
    }

程序说明:程序实现了ReadListener接口,并通过重载onDataAvailable、onAllDataRead和onError方法来对IO事件进行响应。

运行程序3-13,将得到如图3-20所示的运行结果,显示ClientServlet和ServiceServlet之间的交互已经成功完成。

图3-20 程序3-13运行结果

查看NetBeans的服务器日志窗口,如图3-21所示,可以看到在处理完第一部分数据请求后,ServiceServlet的主线程已经返回,而对Servlet的IO操作却在后台一直异步运行,直到所有数据全部读取完毕。

图3-21 异步IO操作在服务器后台输出的日志信息