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的實現類以實現相關的徽調方法,可以使用這個去提供將錯誤信息返回給用戶(如超時或其他出錯信息),也可以做一些資源清理的工作。
我們來看下完成后例子的工程結構圖如下:
#p#
下面我們看下實現了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類的實現
- 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的代碼如下:
#p#
- 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