在JavaWeb应用中对客户请求的异步处理

本文结合具体的范例,介绍如何在JavaWeb应用中对客户请求进行异步处理,在Servlet中进行文件上传。本文的参考书籍是《Tomcat与Java Web开发技术详解》第三版,做者:孙卫琴。html

本文所用的软件版本为:Window10,JDK10,Tomcat9。java

本文所涉及的源代码的下载网址为:
http://www.javathinker.net/javaweb/upload-app.rarweb

在Servlet API 3.0版本以前,Servlet容器针对每一个HTTP请求都会分配一个工做线程。即对于每一次HTTP请求,Servlet容器都会从主线程池中取出一个空闲的工做线程,由该线程从头至尾负责处理请求。若是在响应某个HTTP请求的过程当中涉及到进行I/O操做、访问数据库,或其余耗时的操做,那么该工做线程会被长时间占用,只有当工做线程完成了对当前HTTP请求的响应,才能释放回线程池以供后续使用。
在并发访问量很大的状况下,若是线程池中的许多工做线程都被长时间占用,这将严重影响服务器的并发访问性能。所谓并发访问性能,是指服务器在同一时间能够同时响应众多客户请求的能力。为了解决这种问题,从Servlet API 3.0版本开始,引入了异步处理机制,随后在Servlet API 3.1中又引入了非阻塞I/O来进一步加强异步处理的性能。
Servlet异步处理的机制为:Servlet从HttpServletRequest对象中得到一个AsyncContext对象,该对象表示异步处理的上下文。AsyncContext把响应当前请求的任务传给一个新的线程,由这个新的线程来完成对请求的处理并向客户端返回响应结果。最初由Servlet容器为HTTP请求分配的工做线程即可以及时地释放回主线程池,从而及时处理更多的请求。由此能够看出,所谓Servlet异步处理机制,就是把响应请求的任务从一个线程传给另外一个线程来处理。
1.1 异步处理的流程
要建立支持异步处理的Serlvet类主要包含如下步骤:
(1)在Servlet类中把@WebServlet标注的asyncSupport属性设为true,使得该Servlet支持异步处理。例如:数据库

@WebServlet(name="AsyncServlet1",
            urlPatterns="/async1",
            asyncSupported=true)

若是在web.xml文件中配置该Servlet,那么须要把<async-supported>元素设为true:浏览器

<servlet>   
    <servlet-name>AsyncServlet1</servlet-name>   
    <servlet-class>mypack.AsyncServlet1</servlet-class>   
    <async-supported>true</async-supported>   
  </servlet>

(2)在Servlet类的服务方法中,经过ServletRequest对象的startAsync()方法,得到AsyncContext对象:服务器

AsyncContext asyncContext = request.startAsync();

AsyncContext接口为异步处理当前请求提供了上下文,它具备如下方法:
 setTimeout(long timeout):设置异步线程处理请求任务的超时时间(以毫秒为单位),即异步线程必须在timeout参数指定的时间内完成任务。
 start(java.lang.Runnable run) :启动一个异步线程,执行参数run指定的任务。
 addListener(AsyncListener listener) :添加一个异步监听器。
 complete():告诉Servlet容器任务完成,返回响应结果。
 dispatch(java.lang.String path) :把请求派发給参数path指定的Web组件。
 getRequest() :得到当前上下文中的ServletRequest对象。
 getResponse():得到当前上下文中的ServletResponse对象。
(3)调用AsyncContext对象的setTimeout(long timeout) 设置异步线程的超时时间,这一步不是必须的。
(4)启动一个异步线程来执行处理请求的任务。关于如何启动异步线程,有三种方式,参见5.10.2节的例程5-25(AsyncServlet1.java)、例程5-27(AsyncServlet2.java)和例程5-28(AsyncServlet3.java)。
(5)调用AsyncContext对象的complete()方法来告诉Servlet容器已经完成任务,或者调用AsyncContext对象的的dispatch()方法把请求派发給其余Web组件。
1.2 异步处理的范例
如下例程1-1的AsyncServlet1类是一个支持异步处理的Servlet范例。
例程1-1 AsyncServlet1.java网络

package mypack;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;

@WebServlet(name="AsyncServlet1",
            urlPatterns="/async1",
            asyncSupported=true)

public class AsyncServlet1 extends HttpServlet{

  public void service(HttpServletRequest request,
              HttpServletResponse response)
              throws ServletException,IOException{  

     response.setContentType("text/plain;charset=GBK");
     AsyncContext asyncContext = request.startAsync();
     //设定异步操做的超时时间
     asyncContext.setTimeout(60*1000);  

     //启动异步线程的方式一
     asyncContext.start(new MyTask(asyncContext));
  }
}

以上AsyncServlet1经过AsyncContext对象的start()方法来启动异步线程:
asyncContext.start(new MyTask(asyncContext));
异步线程启动后,就会执行MyTask对象的run()方法中的代码。AsyncContext接口的start()方法的实现方式取决于具体的Servlet容器。有的Servlet容器除了拥有存放工做线程的主线程池,还会另外维护一个线程池,从该线程池中取出空闲的线程来异步处理请求。
有的Servlet容器从已有的主线程池中得到一个空闲的线程来做为异步处理请求的线程,这种实现方式对性能的改进不大,由于若是异步线程和初始线程共享同一个线程池的话,就至关于先闲置初始工做线程,再占用另外一个空闲的工做线程。
如下例程1-2的MyTask类定义了处理请求的具体任务,它实现了Runnable接口。
例程1-2 MyTask.java并发

package mypack;
import javax.servlet.*;
import javax.servlet.http.*;

public class MyTask implements Runnable{
  private AsyncContext asyncContext;

  public MyTask(AsyncContext asyncContext){
    this.asyncContext = asyncContext;
  }

  public void run(){
    try{
      //睡眠5秒,模拟很耗时的一段业务操做
      Thread.sleep(5*1000);
      asyncContext.getResponse()
                  .getWriter()
                  .write("让您久等了!");   
      asyncContext.complete();
    }catch(Exception e){e.printStackTrace();}
  }
}

MyTask类利用AsyncContext对象的getResponse()方法来得到当前的ServletResponse对象,利用AsyncContext对象的complete()方法来通知Servlet容易已经完成任务。
经过浏览器访问:http://localhost:8080/helloapp/async1,会看到客户端在耐心等待了5秒钟后才会获得以下图1-1所示的响应结果
在JavaWeb应用中对客户请求的异步处理
图1-1 AsyncServlet1的响应结果app

如下例程1-3的AsyncServlet2类介绍了启动异步线程的第二种方式。
例程1-3 AsyncServlet2.java异步

@WebServlet(name="AsyncServlet2",
            urlPatterns="/async2",
            asyncSupported=true)

public class AsyncServlet2 extends HttpServlet{

  public void service(HttpServletRequest request,
              HttpServletResponse response)
              throws ServletException,IOException{  

     response.setContentType("text/plain;charset=GBK");
     AsyncContext asyncContext = request.startAsync();
     //设定异步操做的超时时间
     asyncContext.setTimeout(60*1000);  

     //启动异步线程的方式二
     new Thread(new MyTask(asyncContext)).start();
  }
}

以上AsyncServlet2类经过“new Thread()”语句亲自建立新的线程,把它做为异步线程。当大量用户并发访问AsyncServlet2类时,会致使服务器端建立大量的新线程,这会大大下降服务器的运行性能。
如下例程1-4的AsyncServlet3类介绍了启动异步线程的第三种方式。
例程1-4 AsyncServlet3.java

package mypack;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@WebServlet(name="AsyncServlet3",
            urlPatterns="/async3",
            asyncSupported=true)

public class AsyncServlet3 extends HttpServlet{
  private static ThreadPoolExecutor executor = 
          new ThreadPoolExecutor(100, 200, 50000L, 
                 TimeUnit.MILLISECONDS, 
                 new ArrayBlockingQueue<>(100));

  public void service(HttpServletRequest request,
              HttpServletResponse response)
              throws ServletException,IOException{  

     response.setContentType("text/plain;charset=GBK");
     AsyncContext asyncContext = request.startAsync();
     //设定异步操做的超时时间
     asyncContext.setTimeout(60*1000); 

     //启动异步线程的方式三 
     executor.execute(new MyTask(asyncContext));
  }

  public void destroy(){
    //关闭线程池
    executor.shutdownNow();
  }
}

以上AsyncServlet3类利用Java API中的线程池ThreadPoolExecutor类来建立一个线程池,全部的异步线程都存放在这个线程池中。图1-2演示了主线程池和异步处理线程池的关系。
在JavaWeb应用中对客户请求的异步处理
图1-2 主线程池和异步处理线程池的关系

使用ThreadPoolExecutor线程池类的优势是能够更加灵活地根据实际应用需求来设置线程池。在构造ThreadPoolExecutor对象时就能够对线程池的各类选项进行设置。如下是ThreadPoolExecutor类的一个构造方法:

public ThreadPoolExecutor(int corePoolSize,
                     int maximumPoolSize,
                     long keepAliveTime,
                     TimeUnit unit,
                     BlockingQueue<Runnable> workQueue)

以上ThreadPoolExecutor类的构造方法包含如下参数:
 corePoolSize:线程池维护的线程的最少数量。
 maximumPoolSize:线程池维护的线程的最大数量。
 keepAliveTime:线程池维护的线程所容许的空闲时间。
 unit:线程池维护的线程所容许的空闲时间的单位。
 workQueue:线程池所使用的缓冲队列。

ThreadPoolExecutor类的execute(Runnable r)方法会从线程池中取出一个空闲的线程,来执行参数指定的任务:

executor.execute(new MyTask(asyncContext));

1.3 异步监听器
在异步处理请求的过程当中,还能够利用异步监听器AsyncListener来捕获并处理异步线程运行中的特定事件。AsyncListener接口声明了四个方法:
 onStartAsync(AsyncEvent event):异步线程开始时调用。
 onError(AsyncEvent event): 异步线程出错时调用。
 onTimeout(AsyncEvent event): 异步线程执行超时时调用。
 onComplete(AsyncEvent event): 异步线程执行完毕时调用。
如下例程1-5的AsyncServlet4与1.2节的例程1-1的AsyncServlet1类很类似。区别在于AsyncServlet4类中的AsyncContext对象注册了AsyncListener监听器。
例程1-5 AsyncServlet4.java

@WebServlet(name="AsyncServlet4",
            urlPatterns="/async4",
            asyncSupported=true)

public class AsyncServlet4 extends HttpServlet{
  public void service(HttpServletRequest request,
              HttpServletResponse response)
              throws ServletException,IOException{  

     response.setContentType("text/plain;charset=GBK");
     AsyncContext asyncContext = request.startAsync();
     //设定异步操做的超时时间
     asyncContext.setTimeout(60*1000); 

     //注册异步处理监听器 
     asyncContext.addListener(new AsyncListener(){

       public void onComplete(AsyncEvent asyncEvent) 
                                throws IOException{
         System.out.println("on Complete...");
       }

       public void onTimeout(AsyncEvent asyncEvent) 
                                throws IOException{
         System.out.println("on Timeout...");
       }

       public void onError(AsyncEvent asyncEvent)
                                throws IOException{
         System.out.println("on Error...");
       }

       public void onStartAsync(AsyncEvent asyncEvent)
                                throws IOException{
         System.out.println("on Start...");
       }
     });

     asyncContext.start(new MyTask(asyncContext));
  }
}

以上AsyncContext对象所注册的异步监听器是一个内部匿名类,它实现了AsyncListener接口的各个方法,可以在异步线程启动、出错、超时或结束时在服务器的控制台打印出特定的语句。
1.4 非阻塞I/O
非阻塞I/O是与阻塞I/O相对的概念。阻塞I/O包括如下两种状况:
 当一个线程在经过输入流执行读操做时,若是输入流的可读数据暂时还未准备好,那么当前线程会进入阻塞状态(也可理解为等待状态),只有当读到了数据或者到达了数据末尾,线程才会从读方法中退出。例如服务器端读取客户端发送的请求数据时,若是请求数据很大(好比上传文件),那么这些数据在网络上传输须要耗费一些时间,此时服务器端负责读取请求数据的线程可能会进入阻塞状态。
 当一个线程在经过输出流执行写操做时,若是由于某种缘由,暂时不能向目的地写数据,那么当前线程会进入阻塞状态,只有当完成了写数据的操做,线程才会从写方法中退出。例如当服务器端向客户端发送响应结果时,若是响应正文很大(好比下载文件),那么这些数据在网络上传输须要耗费一些时间,此时服务器端负责输出响应结果的线程可能会进入阻塞状态。
非阻塞I/O操做也包括两种状况:
 当一个线程在经过输入流执行读操做时,若是输入流的可读数据暂时还未准备好,那么当前线程不会进入阻塞状态,而是当即退出读方法。只有当输入流中有可读数据时,再进行读操做。
 当一个线程在经过输出流执行写操做时,若是由于某种缘由,暂时不能向目的地写数据,那么当前线程不会进入阻塞状态,而是当即退出写方法。只有当能够向目的地写数据时,再进行写操做。
在Java语言中,传统的输入/输出操做都采用阻塞I/O的方式。本章前面几节已经介绍了如何用异步处理机制来提升服务器的并发访问性能。可是,当异步线程用阻塞I/O的方式来读写数据时,毕竟仍是会使得异步线程经常进入阻塞状态,这仍是会削弱服务器的并发访问性能。
为了解决上述问题,从Servlet API 3.1开始,引入了非阻塞I/O机制,它创建在异步处理的基础上,具体实现方式是引入了两个监听器:
 ReadListener接口:监听ServletInputStream输入流的行为。
 WriteListener接口:监听ServletOutputStream输出流的行为。
ReadListener接口包含如下方法:
 onDataAvailable():输入流中有可读数据时触发此方法。
 onAllDataRead():输入流中全部数据读完时触发此方法。
 onError(Throwable t):输入操做出现错误时触发此方法。
WriteListener接口包含如下方法:
 onWritePossible():能够向输出流写数据时触发此方法。
 onError(java.lang.Throwable throwable):输出操做出现错误时触发此方法。
在支持异步处理的Servlet类中进行非阻塞I/O操做主要包括如下步骤:
(1)在服务方法中从ServletRequest对象或ServletResponse对象中获得输入流或输出流:
ServletInputStream input = request.getInputStream();
ServletOutputStream output = request.getOutputStream();
(2)为输入流注册一个读监听器,或为输出流注册一个写监听器:
//如下context引用AsyncContext对象
input.setReadListener(new MyReadListener(input, context));
output.setWriteListener(new MyWriteListener(output, context));
(3)在读监听器类或写监听器类中编写包含非阻塞I/O操做的代码 。
下面经过具体范例来演示非阻塞I/O的用法。本范例涉及到三个Web组件:upload2.htmNoblockServlet.javaOutputServlet.java。
upload2.htm会生成一个能够上传文件的网页,它的主要源代码以下:

<form name="uploadForm" method="POST"
    enctype="MULTIPART/FORM-DATA"
    action="nonblock">
    <table>
      <tr>
       <td><div align="right">User Name:</div></td>
       <td><input type="text" name="username" size="30"/> </td>
      </tr>
      <tr>
       <td><div align="right">Upload File1:</div></td>
       <td><input type="file" name="file1" size="30"/> </td>
      </tr>
      <tr>
        <td><input type="submit" name="submit" value="upload"></td>
        <td><input type="reset" name="reset" value="reset"></td>
      </tr>
    </table>
  </form>
OutputServlet.java的做用是向网页上输出请求范围内的msg属性的值,如下是它的源代码:
public class OutputServlet extends GenericServlet {

  public void service(ServletRequest request,
              ServletResponse response)
              throws ServletException, IOException {

    //读取CheckServlet存放在请求范围内的消息
    String message = (String)request.getAttribute("msg");
    PrintWriter out=response.getWriter();

    out.println(message); 
    out.close();
  }
}

如下例程1-6是NonblockServlet类的源代码,它为ServletInputStream注册了读监听器,而且在service()方法的开头和结尾,会向客户端打印进入service()方法以及退出service()方法的时间。
例程1-6 NonblockServlet.java

package mypack;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;

@WebServlet(urlPatterns="/nonblock",
            asyncSupported=true)
public class NonblockServlet extends HttpServlet{

  public void service(HttpServletRequest request ,
              HttpServletResponse response)
              throws IOException , ServletException{

    response.setContentType("text/html;charset=GBK");
    PrintWriter out = response.getWriter();
    out.println("<title>非阻塞IO示例</title>");
    out.println("进入Servlet的service()方法的时间:"
      + new java.util.Date() + ".<br/>");

    // 建立AsyncContext 
    AsyncContext context = request.startAsync();
    //设置异步调用的超时时长
    context.setTimeout(60 * 1000);

    ServletInputStream input = request.getInputStream();
    //为输入流注册监听器
    input.setReadListener(new MyReadListener(input, context));

    out.println("退出Servlet的service()方法的时间:"
               + new java.util.Date() + ".<br/><hr>");
    out.flush();
  }
}

以上ServletInputStream注册的读监听器为MyReadListener类,如下例程1-7是它的源代码。
例程1-7 MyReadListener.java

package mypack;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class MyReadListener implements ReadListener{
  private ServletInputStream input;
  private AsyncContext context;
  private StringBuilder sb = new StringBuilder();

  public MyReadListener(ServletInputStream input , 
                        AsyncContext context){
    this.input = input;
    this.context = context;
  }

  public void onDataAvailable(){
    System.out.println("数据可用!");
    try{
      // 暂停5秒,模拟读取数据是一个耗时操做。
      Thread.sleep(5000);

       int len = -1;
      byte[] buff = new byte[1024];

      //读取浏览器向Servlet提交的数据
      while (input.isReady() && (len = input.read(buff)) > 0){
        String data = new String(buff , 0 , len);
        sb.append(data);
      }
    }catch (Exception ex){ex.printStackTrace();}
  }

  public void onAllDataRead(){
    System.out.println("数据读取完成!");
    System.out.println(sb);
    //将数据设置为request范围的属性
    context.getRequest().setAttribute("msg" , sb.toString());
    //把请求派发給OutputServlet组件
    context.dispatch("/output");
  }

  public void onError(Throwable t){
    t.printStackTrace();  
  }
}

MyReadListener类实现了ReadListener接口中的全部方法。在onDataAvailable()方法中读取客户端的请求数据,把它存放到StringBuilder对象中。在onAllDataRead()方法中,把StringBuilder对象包含的字符串做为msg属性存放到请求范围内。最后把请求派发給URL为“/output”的Web组件来处理,它和OutputServlet对应。
经过浏览器访问http://localhost:8080/helloapp/upload2.htm,将会出现如图1-3所示的网页。
在JavaWeb应用中对客户请求的异步处理
图1-3 upload2.htm网页

在网页中输入相关数据,再提交表单,该请求由URL为“/nonblock”的Web组件来处理,它和NonblockServlet组件对应。而NonblockServlet组件会经过MyReadListener读监听器采起非阻塞I/O的方式来读取请求数据,最后MyReadListener读监听器把请求派发給OutputServlet。NonblockServlet和OutputServlet共同生成的响应结果参见图1-4。
在JavaWeb应用中对客户请求的异步处理
图1-4 NonblockServlet和OutputServlet共同生成的响应结果

在客户端等待图1-4的网页的内容所有展现出来的过程当中,能够看出,当主工做线程已经退出NonblockServlet的service()方法时,读取客户请求数据的非阻塞I/O操做尚未完成。那么究竟是由哪一个线程来执行非阻塞I/O操做的呢?这取决于Servlet容器的实现,用户无需了解其中的细节,反正能够确定的是,Servlet容器会提供一个异步线程来执行MyReadListener读监听器中的非阻塞I/O操做。

相关文章
相关标签/搜索