Servlet3中異步Servlet特性介紹
在Jave EE 6規範中,關于Servlet 3規範的相關功能增強,一直是讓大部分使用者忽略的,連直到最新的Spring MVC 3.2才支援Servlet 3的異步調用。這可能跟大部分使用者使用的JAVE EE容器依然是舊的有關系(如支援Servlet 3規範的需要Tomcat 7,但目前不少使用者還在使用Tomcat 6)。
在本文中,将以實際的例子來講解下Servlet 3規範中對異步操作的支援。
首先要簡單了解,在Servlet 3中,已經支援使用注解的方式去進行Servlet的配置,這樣就不需要在web.xml中進行傳統的xml的配置了,最常用的注解是使用 @WebServlet、@WebFilter、@WebInitParam,它們分别等價于傳統xml配置中 的<Servlet>、<WebFilter>、<InitParam>,其他參數可參考Servlet 3中的規範說明。
下面我們開始了解下,如果不使用異步特性的一個例子,代碼如下:
@WebServlet("/LongRunningServlet")
public class LongRunningServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("LongRunningServlet Start::Name="
+ Thread.currentThread().getName() + "::ID="
+ Thread.currentThread().getId());
String time = request.getParameter("time");
int secs = Integer.valueOf(time);
//如果超過10秒,預設用10秒
if (secs > 10000)
secs = 10000;
longProcessing(secs);
PrintWriter out = response.getWriter();
long endTime = System.currentTimeMillis();
out.write("Processing done for " + secs + " milliseconds!!");
System.out.println("LongRunningServlet Start::Name="
+ Thread.currentThread().getName() + "::ID="
+ Thread.currentThread().getId() + "::Time Taken="
+ (endTime - startTime) + " ms.");
}
private void longProcessing(int secs) {
//故意讓線程睡眠
try {
Thread.sleep(secs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運作上面的例子,輸入
http://localhost:8080/AsyncServletExample/LongRunningServlet?time=8000,則可以看到輸出為:
LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103
1. LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103::Time Taken=8002 ms.
可以觀察到,在主線程啟動後,servlet線程為了處理longProcessing的請求,足足等待了8秒,最後才輸出結果進行響應,這樣對于 高并發的應用來說這是很大的瓶頸,因為必須要同步等到待處理的方法完成後,Servlet容器中的線程才能繼續接收其他請求,在此之前,Servlet線 程一直處于阻塞狀态。
在Servlet 3.0規範前,是有一些相關的解決方案的,比如常見的就是使用一個單獨的工作線程(worker thread)去處理這些耗費時間的工作,而Servlet 容器中的線程在把工作交給工作線程處理後則馬上回收到Servlet容器中去。比如Tomcat的Comet、WebLogic的的 FutureResponseServlet和WebSphere的Asynchronous Request Dispatcher都是這類型的解決方案。
但隻這些方案的弊端是沒辦法很容易地在不修改代碼的情況下遷移到其他Servlet容器中,這就是Servlet 3中要定義異步Servlet的原因所在。
下面我們通過例子來說明異步Servlet的實作方法:
1、 首先設定servlet要支援異步屬性,這個隻需要設定asyncSupported屬性為true就可以了。
2、 因為實際上的工作是委托給另外的線程的,我們應該實作一個線程池,這個可以通過使用Executors架構去實作(具體參考 http://www.journaldev.com/1069/java-thread-pool-example-using-executors- and-threadpoolexecutor一文),并且使用Servlet Context listener去初始化線程池。
3、 我們需要通過ServletRequest.startAsync()方法獲得AsyncContext的執行個體。AsyncContext提供了方法去獲 得ServletRequest和ServletResponse的對象引用。它也能使用dispatch()方法去将請求forward到其他資源。
4、 我們将實作Runnable接口,并且在其實作方法中處理各種耗時的任務,然後使用AsyncContext對象去将請求dispatch到其他資源中去 或者使用ServletResponse對象輸出。一旦處理完畢,将調用AsyncContext.complete()方法去讓容器知道異步處理已經結 束。
5、 我們還可以在AsyncContext對象增加AsyncListener的實作類以實作相關的徽調方法,可以使用這個去提供将錯誤資訊傳回給使用者(如逾時或其他出錯資訊),也可以做一些資源清理的工作。
我們來看下完成後例子的工程結構圖如下:
下面我們看下實作了ServletContextListener類的監聽類代碼:
AppContextListener.java
package com.journaldev.servlet.async;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class AppContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent servletContextEvent) {
// 建立線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100));
servletContextEvent.getServletContext().setAttribute("executor",
executor);
}
public void contextDestroyed(ServletContextEvent servletContextEvent) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent
.getServletContext().getAttribute("executor");
executor.shutdown();
}
}
然後是worker線程的實作代碼,如下:
AsyncRequestProcessor.java
package com.journaldev.servlet.async;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.AsyncContext;
public class AsyncRequestProcessor implements Runnable {
private AsyncContext asyncContext;
private int secs;
public AsyncRequestProcessor() {
}
public AsyncRequestProcessor(AsyncContext asyncCtx, int secs) {
this.asyncContext = asyncCtx;
this.secs = secs;
}
@Override
public void run() {
System.out.println("Async Supported? "
+ asyncContext.getRequest().isAsyncSupported());
longProcessing(secs);
try {
PrintWriter out = asyncContext.getResponse().getWriter();
out.write("Processing done for " + secs + " milliseconds!!");
} catch (IOException e) {
e.printStackTrace();
}
//完成異步線程處理
asyncContext.complete();
}
private void longProcessing(int secs) {
// 休眠指定的時間
try {
Thread.sleep(secs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
請在這裡注意AsyncContext的使用方法,以及當完成異步調用時必須調用asyncContext.complete()方法。
現在看下AsyncListener類的實作
AppAsyncListener.java
package com.journaldev.servlet.async;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebListener;
@WebListener
public class AppAsyncListener implements AsyncListener {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("AppAsyncListener onComplete");
// 在這裡可以做一些資源清理工作
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("AppAsyncListener onError");
//這裡可以抛出錯誤資訊
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("AppAsyncListener onStartAsync");
//可以記錄相關日志
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("AppAsyncListener onTimeout");
ServletResponse response = asyncEvent.getAsyncContext().getResponse();
PrintWriter out = response.getWriter();
out.write("TimeOut Error in Processing");
}
}
其中請注意可以監聽onTimeout事件的使用,可以有效地傳回給使用者端出錯的資訊。最後來重新改寫下前文提到的測試Servlet的代碼如下:
AsyncLongRunningServlet.java
package com.journaldev.servlet.async;
import java.io.IOException;
import java.util.concurrent.ThreadPoolExecutor;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
public class AsyncLongRunningServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("AsyncLongRunningServlet Start::Name="
+ Thread.currentThread().getName() + "::ID="
+ Thread.currentThread().getId());
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
String time = request.getParameter("time");
int secs = Integer.valueOf(time);
// 如果超過10秒則設定為10秒
if (secs > 10000)
secs = 10000;
AsyncContext asyncCtx = request.startAsync();
asyncCtx.addListener(new AppAsyncListener());
asyncCtx.setTimeout(9000);
ThreadPoolExecutor executor = (ThreadPoolExecutor) request
.getServletContext().getAttribute("executor");
executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
long endTime = System.currentTimeMillis();
System.out.println("AsyncLongRunningServlet End::Name="
+ Thread.currentThread().getName() + "::ID="
+ Thread.currentThread().getId() + "::Time Taken="
+ (endTime - startTime) + " ms.");
}
}
下面運作這個Servlet程式,輸入:
http://localhost:8080/AsyncServletExample/AsyncLongRunningServlet?time=8000,運作結果為:
AsyncLongRunningServlet Start::Name=http-bio-8080-exec-50::ID=124
AsyncLongRunningServlet End::Name=http-bio-8080-exec-50::ID=124::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onComplete
但如果我們運作一個time=9999的輸入,則運作結果為:
AsyncLongRunningServlet Start::Name=http-bio-8080-exec-44::ID=117
AsyncLongRunningServlet End::Name=http-bio-8080-exec-44::ID=117::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onTimeout
AppAsyncListener onError
AppAsyncListener onComplete
Exception in thread "pool-5-thread-6" java.lang.IllegalStateException: The request associated with the AsyncContext has already completed processing.
at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:439)
at org.apache.catalina.core.AsyncContextImpl.getResponse(AsyncContextImpl.java:197)
at com.journaldev.servlet.async.AsyncRequestProcessor.run(AsyncRequestProcessor.java:27)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:680)
可以看到,Servlet主線程很快執行完畢并且所有的處理額外的工作都是在另外一個線程中處理的,不存在阻塞問題。
原文連結:http://www.javacodegeeks.com/2013/08/async-servlet-feature-of-servlet-3.html