本文結(jié)合具體的范例,介紹如何在JavaWeb應(yīng)用中對客戶請求進(jìn)行異步處理,在Servlet中進(jìn)行文件上傳。本文的參考書籍是《Tomcat與Java Web開發(fā)技術(shù)詳解》第三版,作者:孫衛(wèi)琴。
創(chuàng)新互聯(lián)一直秉承“誠信做人,踏實(shí)做事”的原則,不欺瞞客戶,是我們最起碼的底線! 以服務(wù)為基礎(chǔ),以質(zhì)量求生存,以技術(shù)求發(fā)展,成交一個(gè)客戶多一個(gè)朋友!為您提供成都做網(wǎng)站、網(wǎng)站建設(shè)、成都網(wǎng)頁設(shè)計(jì)、微信小程序開發(fā)、成都網(wǎng)站開發(fā)、成都網(wǎng)站制作、成都軟件開發(fā)、手機(jī)APP定制開發(fā)是成都本地專業(yè)的網(wǎng)站建設(shè)和網(wǎng)站設(shè)計(jì)公司,等你一起來見證!
本文所用的軟件版本為:Window10,JDK10,Tomcat9。
本文所涉及的源代碼的下載網(wǎng)址為:
http://www.javathinker.net/javaweb/upload-app.rar
在Servlet API 3.0版本之前,Servlet容器針對每個(gè)HTTP請求都會分配一個(gè)工作線程。即對于每一次HTTP請求,Servlet容器都會從主線程池中取出一個(gè)空閑的工作線程,由該線程從頭到尾負(fù)責(zé)處理請求。如果在響應(yīng)某個(gè)HTTP請求的過程中涉及到進(jìn)行I/O操作、訪問數(shù)據(jù)庫,或其他耗時(shí)的操作,那么該工作線程會被長時(shí)間占用,只有當(dāng)工作線程完成了對當(dāng)前HTTP請求的響應(yīng),才能釋放回線程池以供后續(xù)使用。
在并發(fā)訪問量很大的情況下,如果線程池中的許多工作線程都被長時(shí)間占用,這將嚴(yán)重影響服務(wù)器的并發(fā)訪問性能。所謂并發(fā)訪問性能,是指服務(wù)器在同一時(shí)間可以同時(shí)響應(yīng)眾多客戶請求的能力。為了解決這種問題,從Servlet API 3.0版本開始,引入了異步處理機(jī)制,隨后在Servlet API 3.1中又引入了非阻塞I/O來進(jìn)一步增強(qiáng)異步處理的性能。
Servlet異步處理的機(jī)制為:Servlet從HttpServletRequest對象中獲得一個(gè)AsyncContext對象,該對象表示異步處理的上下文。AsyncContext把響應(yīng)當(dāng)前請求的任務(wù)傳給一個(gè)新的線程,由這個(gè)新的線程來完成對請求的處理并向客戶端返回響應(yīng)結(jié)果。最初由Servlet容器為HTTP請求分配的工作線程便可以及時(shí)地釋放回主線程池,從而及時(shí)處理更多的請求。由此可以看出,所謂Servlet異步處理機(jī)制,就是把響應(yīng)請求的任務(wù)從一個(gè)線程傳給另一個(gè)線程來處理。
1.1 異步處理的流程
要?jiǎng)?chuàng)建支持異步處理的Serlvet類主要包含以下步驟:
(1)在Servlet類中把@WebServlet標(biāo)注的asyncSupport屬性設(shè)為true,使得該Servlet支持異步處理。例如:
@WebServlet(name="AsyncServlet1",
urlPatterns="/async1",
asyncSupported=true)
如果在web.xml文件中配置該Servlet,那么需要把
AsyncServlet1
mypack.AsyncServlet1
true
(2)在Servlet類的服務(wù)方法中,通過ServletRequest對象的startAsync()方法,獲得AsyncContext對象:
AsyncContext asyncContext = request.startAsync();
AsyncContext接口為異步處理當(dāng)前請求提供了上下文,它具有以下方法:
? setTimeout(long timeout):設(shè)置異步線程處理請求任務(wù)的超時(shí)時(shí)間(以毫秒為單位),即異步線程必須在timeout參數(shù)指定的時(shí)間內(nèi)完成任務(wù)。
? start(java.lang.Runnable run) :啟動一個(gè)異步線程,執(zhí)行參數(shù)run指定的任務(wù)。
? addListener(AsyncListener listener) :添加一個(gè)異步監(jiān)聽器。
? complete():告訴Servlet容器任務(wù)完成,返回響應(yīng)結(jié)果。
? dispatch(java.lang.String path) :把請求派發(fā)給參數(shù)path指定的Web組件。
? getRequest() :獲得當(dāng)前上下文中的ServletRequest對象。
? getResponse():獲得當(dāng)前上下文中的ServletResponse對象。
(3)調(diào)用AsyncContext對象的setTimeout(long timeout) 設(shè)置異步線程的超時(shí)時(shí)間,這一步不是必須的。
(4)啟動一個(gè)異步線程來執(zhí)行處理請求的任務(wù)。關(guān)于如何啟動異步線程,有三種方式,參見5.10.2節(jié)的例程5-25(AsyncServlet1.java)、例程5-27(AsyncServlet2.java)和例程5-28(AsyncServlet3.java)。
(5)調(diào)用AsyncContext對象的complete()方法來告訴Servlet容器已經(jīng)完成任務(wù),或者調(diào)用AsyncContext對象的的dispatch()方法把請求派發(fā)給其他Web組件。
1.2 異步處理的范例
以下例程1-1的AsyncServlet1類是一個(gè)支持異步處理的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();
//設(shè)定異步操作的超時(shí)時(shí)間
asyncContext.setTimeout(60*1000);
//啟動異步線程的方式一
asyncContext.start(new MyTask(asyncContext));
}
}
以上AsyncServlet1通過AsyncContext對象的start()方法來啟動異步線程:
asyncContext.start(new MyTask(asyncContext));
異步線程啟動后,就會執(zhí)行MyTask對象的run()方法中的代碼。AsyncContext接口的start()方法的實(shí)現(xiàn)方式取決于具體的Servlet容器。有的Servlet容器除了擁有存放工作線程的主線程池,還會另外維護(hù)一個(gè)線程池,從該線程池中取出空閑的線程來異步處理請求。
有的Servlet容器從已有的主線程池中獲得一個(gè)空閑的線程來作為異步處理請求的線程,這種實(shí)現(xiàn)方式對性能的改進(jìn)不大,因?yàn)槿绻惒骄€程和初始線程共享同一個(gè)線程池的話,就相當(dāng)于先閑置初始工作線程,再占用另一個(gè)空閑的工作線程。
以下例程1-2的MyTask類定義了處理請求的具體任務(wù),它實(shí)現(xiàn)了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秒,模擬很耗時(shí)的一段業(yè)務(wù)操作
Thread.sleep(5*1000);
asyncContext.getResponse()
.getWriter()
.write("讓您久等了!");
asyncContext.complete();
}catch(Exception e){e.printStackTrace();}
}
}
MyTask類利用AsyncContext對象的getResponse()方法來獲得當(dāng)前的ServletResponse對象,利用AsyncContext對象的complete()方法來通知Servlet容易已經(jīng)完成任務(wù)。
通過瀏覽器訪問:http://localhost:8080/helloapp/async1,會看到客戶端在耐心等待了5秒鐘后才會得到如下圖1-1所示的響應(yīng)結(jié)果。
圖1-1 AsyncServlet1的響應(yīng)結(jié)果
以下例程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();
//設(shè)定異步操作的超時(shí)時(shí)間
asyncContext.setTimeout(60*1000);
//啟動異步線程的方式二
new Thread(new MyTask(asyncContext)).start();
}
}
以上AsyncServlet2類通過“new Thread()”語句親自創(chuàng)建新的線程,把它作為異步線程。當(dāng)大量用戶并發(fā)訪問AsyncServlet2類時(shí),會導(dǎo)致服務(wù)器端創(chuàng)建大量的新線程,這會大大降低服務(wù)器的運(yùn)行性能。
以下例程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();
//設(shè)定異步操作的超時(shí)時(shí)間
asyncContext.setTimeout(60*1000);
//啟動異步線程的方式三
executor.execute(new MyTask(asyncContext));
}
public void destroy(){
//關(guān)閉線程池
executor.shutdownNow();
}
}
以上AsyncServlet3類利用Java API中的線程池ThreadPoolExecutor類來創(chuàng)建一個(gè)線程池,所有的異步線程都存放在這個(gè)線程池中。圖1-2演示了主線程池和異步處理線程池的關(guān)系。
圖1-2 主線程池和異步處理線程池的關(guān)系
使用ThreadPoolExecutor線程池類的優(yōu)點(diǎn)是可以更加靈活地根據(jù)實(shí)際應(yīng)用需求來設(shè)置線程池。在構(gòu)造ThreadPoolExecutor對象時(shí)就可以對線程池的各種選項(xiàng)進(jìn)行設(shè)置。以下是ThreadPoolExecutor類的一個(gè)構(gòu)造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue)
以上ThreadPoolExecutor類的構(gòu)造方法包含以下參數(shù):
? corePoolSize:線程池維護(hù)的線程的最少數(shù)量。
? maximumPoolSize:線程池維護(hù)的線程的最大數(shù)量。
? keepAliveTime:線程池維護(hù)的線程所允許的空閑時(shí)間。
? unit:線程池維護(hù)的線程所允許的空閑時(shí)間的單位。
? workQueue:線程池所使用的緩沖隊(duì)列。
ThreadPoolExecutor類的execute(Runnable r)方法會從線程池中取出一個(gè)空閑的線程,來執(zhí)行參數(shù)指定的任務(wù):
executor.execute(new MyTask(asyncContext));
1.3 異步監(jiān)聽器
在異步處理請求的過程中,還可以利用異步監(jiān)聽器AsyncListener來捕獲并處理異步線程運(yùn)行中的特定事件。AsyncListener接口聲明了四個(gè)方法:
? onStartAsync(AsyncEvent event):異步線程開始時(shí)調(diào)用。
? onError(AsyncEvent event): 異步線程出錯(cuò)時(shí)調(diào)用。
? onTimeout(AsyncEvent event): 異步線程執(zhí)行超時(shí)時(shí)調(diào)用。
? onComplete(AsyncEvent event): 異步線程執(zhí)行完畢時(shí)調(diào)用。
以下例程1-5的AsyncServlet4與1.2節(jié)的例程1-1的AsyncServlet1類很相似。區(qū)別在于AsyncServlet4類中的AsyncContext對象注冊了AsyncListener監(jiān)聽器。
例程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();
//設(shè)定異步操作的超時(shí)時(shí)間
asyncContext.setTimeout(60*1000);
//注冊異步處理監(jiān)聽器
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對象所注冊的異步監(jiān)聽器是一個(gè)內(nèi)部匿名類,它實(shí)現(xiàn)了AsyncListener接口的各個(gè)方法,能夠在異步線程啟動、出錯(cuò)、超時(shí)或結(jié)束時(shí)在服務(wù)器的控制臺打印出特定的語句。
1.4 非阻塞I/O
非阻塞I/O是與阻塞I/O相對的概念。阻塞I/O包括以下兩種情況:
? 當(dāng)一個(gè)線程在通過輸入流執(zhí)行讀操作時(shí),如果輸入流的可讀數(shù)據(jù)暫時(shí)還未準(zhǔn)備好,那么當(dāng)前線程會進(jìn)入阻塞狀態(tài)(也可理解為等待狀態(tài)),只有當(dāng)讀到了數(shù)據(jù)或者到達(dá)了數(shù)據(jù)末尾,線程才會從讀方法中退出。例如服務(wù)器端讀取客戶端發(fā)送的請求數(shù)據(jù)時(shí),如果請求數(shù)據(jù)很大(比如上傳文件),那么這些數(shù)據(jù)在網(wǎng)絡(luò)上傳輸需要耗費(fèi)一些時(shí)間,此時(shí)服務(wù)器端負(fù)責(zé)讀取請求數(shù)據(jù)的線程可能會進(jìn)入阻塞狀態(tài)。
? 當(dāng)一個(gè)線程在通過輸出流執(zhí)行寫操作時(shí),如果因?yàn)槟撤N原因,暫時(shí)不能向目的地寫數(shù)據(jù),那么當(dāng)前線程會進(jìn)入阻塞狀態(tài),只有當(dāng)完成了寫數(shù)據(jù)的操作,線程才會從寫方法中退出。例如當(dāng)服務(wù)器端向客戶端發(fā)送響應(yīng)結(jié)果時(shí),如果響應(yīng)正文很大(比如下載文件),那么這些數(shù)據(jù)在網(wǎng)絡(luò)上傳輸需要耗費(fèi)一些時(shí)間,此時(shí)服務(wù)器端負(fù)責(zé)輸出響應(yīng)結(jié)果的線程可能會進(jìn)入阻塞狀態(tài)。
非阻塞I/O操作也包括兩種情況:
? 當(dāng)一個(gè)線程在通過輸入流執(zhí)行讀操作時(shí),如果輸入流的可讀數(shù)據(jù)暫時(shí)還未準(zhǔn)備好,那么當(dāng)前線程不會進(jìn)入阻塞狀態(tài),而是立即退出讀方法。只有當(dāng)輸入流中有可讀數(shù)據(jù)時(shí),再進(jìn)行讀操作。
? 當(dāng)一個(gè)線程在通過輸出流執(zhí)行寫操作時(shí),如果因?yàn)槟撤N原因,暫時(shí)不能向目的地寫數(shù)據(jù),那么當(dāng)前線程不會進(jìn)入阻塞狀態(tài),而是立即退出寫方法。只有當(dāng)可以向目的地寫數(shù)據(jù)時(shí),再進(jìn)行寫操作。
在Java語言中,傳統(tǒng)的輸入/輸出操作都采用阻塞I/O的方式。本章前面幾節(jié)已經(jīng)介紹了如何用異步處理機(jī)制來提高服務(wù)器的并發(fā)訪問性能。但是,當(dāng)異步線程用阻塞I/O的方式來讀寫數(shù)據(jù)時(shí),畢竟還是會使得異步線程常常進(jìn)入阻塞狀態(tài),這還是會削弱服務(wù)器的并發(fā)訪問性能。
為了解決上述問題,從Servlet API 3.1開始,引入了非阻塞I/O機(jī)制,它建立在異步處理的基礎(chǔ)上,具體實(shí)現(xiàn)方式是引入了兩個(gè)監(jiān)聽器:
? ReadListener接口:監(jiān)聽ServletInputStream輸入流的行為。
? WriteListener接口:監(jiān)聽ServletOutputStream輸出流的行為。
ReadListener接口包含以下方法:
? onDataAvailable():輸入流中有可讀數(shù)據(jù)時(shí)觸發(fā)此方法。
? onAllDataRead():輸入流中所有數(shù)據(jù)讀完時(shí)觸發(fā)此方法。
? onError(Throwable t):輸入操作出現(xiàn)錯(cuò)誤時(shí)觸發(fā)此方法。
WriteListener接口包含以下方法:
? onWritePossible():可以向輸出流寫數(shù)據(jù)時(shí)觸發(fā)此方法。
? onError(java.lang.Throwable throwable):輸出操作出現(xiàn)錯(cuò)誤時(shí)觸發(fā)此方法。
在支持異步處理的Servlet類中進(jìn)行非阻塞I/O操作主要包括以下步驟:
(1)在服務(wù)方法中從ServletRequest對象或ServletResponse對象中得到輸入流或輸出流:
ServletInputStream input = request.getInputStream();
ServletOutputStream output = request.getOutputStream();
(2)為輸入流注冊一個(gè)讀監(jiān)聽器,或?yàn)檩敵隽髯砸粋€(gè)寫監(jiān)聽器:
//以下context引用AsyncContext對象
input.setReadListener(new MyReadListener(input, context));
output.setWriteListener(new MyWriteListener(output, context));
(3)在讀監(jiān)聽器類或?qū)懕O(jiān)聽器類中編寫包含非阻塞I/O操作的代碼 。
下面通過具體范例來演示非阻塞I/O的用法。本范例涉及到三個(gè)Web組件:upload2.htm?NoblockServlet.java?OutputServlet.java。
upload2.htm會生成一個(gè)可以上傳文件的網(wǎng)頁,它的主要源代碼如下:
OutputServlet.java的作用是向網(wǎng)頁上輸出請求范圍內(nèi)的msg屬性的值,以下是它的源代碼:
public class OutputServlet extends GenericServlet {
public void service(ServletRequest request,
ServletResponse response)
throws ServletException, IOException {
//讀取CheckServlet存放在請求范圍內(nèi)的消息
String message = (String)request.getAttribute("msg");
PrintWriter out=response.getWriter();
out.println(message);
out.close();
}
}
以下例程1-6是NonblockServlet類的源代碼,它為ServletInputStream注冊了讀監(jiān)聽器,并且在service()方法的開頭和結(jié)尾,會向客戶端打印進(jìn)入service()方法以及退出service()方法的時(shí)間。
例程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("非阻塞IO示例 ");
out.println("進(jìn)入Servlet的service()方法的時(shí)間:"
+ new java.util.Date() + ".
");
// 創(chuàng)建AsyncContext
AsyncContext context = request.startAsync();
//設(shè)置異步調(diào)用的超時(shí)時(shí)長
context.setTimeout(60 * 1000);
ServletInputStream input = request.getInputStream();
//為輸入流注冊監(jiān)聽器
input.setReadListener(new MyReadListener(input, context));
out.println("退出Servlet的service()方法的時(shí)間:"
+ new java.util.Date() + ".
");
out.flush();
}
}
以上ServletInputStream注冊的讀監(jiān)聽器為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("數(shù)據(jù)可用!");
try{
// 暫停5秒,模擬讀取數(shù)據(jù)是一個(gè)耗時(shí)操作。
Thread.sleep(5000);
int len = -1;
byte[] buff = new byte[1024];
//讀取瀏覽器向Servlet提交的數(shù)據(jù)
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("數(shù)據(jù)讀取完成!");
System.out.println(sb);
//將數(shù)據(jù)設(shè)置為request范圍的屬性
context.getRequest().setAttribute("msg" , sb.toString());
//把請求派發(fā)給OutputServlet組件
context.dispatch("/output");
}
public void onError(Throwable t){
t.printStackTrace();
}
}
MyReadListener類實(shí)現(xiàn)了ReadListener接口中的所有方法。在onDataAvailable()方法中讀取客戶端的請求數(shù)據(jù),把它存放到StringBuilder對象中。在onAllDataRead()方法中,把StringBuilder對象包含的字符串作為msg屬性存放到請求范圍內(nèi)。最后把請求派發(fā)給URL為“/output”的Web組件來處理,它和OutputServlet對應(yīng)。
通過瀏覽器訪問http://localhost:8080/helloapp/upload2.htm,將會出現(xiàn)如圖1-3所示的網(wǎng)頁。
圖1-3 upload2.htm網(wǎng)頁
在網(wǎng)頁中輸入相關(guān)數(shù)據(jù),再提交表單,該請求由URL為“/nonblock”的Web組件來處理,它和NonblockServlet組件對應(yīng)。而NonblockServlet組件會通過MyReadListener讀監(jiān)聽器采取非阻塞I/O的方式來讀取請求數(shù)據(jù),最后MyReadListener讀監(jiān)聽器把請求派發(fā)給OutputServlet。NonblockServlet和OutputServlet共同生成的響應(yīng)結(jié)果參見圖1-4。
圖1-4 NonblockServlet和OutputServlet共同生成的響應(yīng)結(jié)果
在客戶端等待圖1-4的網(wǎng)頁的內(nèi)容全部展示出來的過程中,可以看出,當(dāng)主工作線程已經(jīng)退出NonblockServlet的service()方法時(shí),讀取客戶請求數(shù)據(jù)的非阻塞I/O操作還沒有完成。那么到底是由哪個(gè)線程來執(zhí)行非阻塞I/O操作的呢?這取決于Servlet容器的實(shí)現(xiàn),用戶無需了解其中的細(xì)節(jié),反正可以肯定的是,Servlet容器會提供一個(gè)異步線程來執(zhí)行MyReadListener讀監(jiān)聽器中的非阻塞I/O操作。