在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操做。

相關文章
相關標籤/搜索