Servlet3中異步Servlet特性介紹
譯文在Jave EE 6規(guī)范中,關(guān)于Servlet 3規(guī)范的相關(guān)功能增強(qiáng),一直是讓大部分用戶忽略的,連直到最新的Spring MVC 3.2才支持Servlet 3的異步調(diào)用。這可能跟大部分用戶使用的JAVE EE容器依然是舊的有關(guān)系(如支持Servlet 3規(guī)范的需要Tomcat 7,但目前不少用戶還在使用Tomcat 6)。
在本文中,將以實(shí)際的例子來(lái)講解下Servlet 3規(guī)范中對(duì)異步操作的支持。
首先要簡(jiǎn)單了解,在Servlet 3中,已經(jīng)支持使用注解的方式去進(jìn)行Servlet的配置,這樣就不需要在web.xml中進(jìn)行傳統(tǒng)的xml的配置了,最常用的注解是使用@WebServlet、@WebFilter、@WebInitParam,它們分別等價(jià)于傳統(tǒng)xml配置中的<Servlet>、<WebFilter>、<InitParam>,其他參數(shù)可參考Servlet 3中的規(guī)范說(shuō)明。
下面我們開(kāi)始了解下,如果不使用異步特性的一個(gè)例子,代碼如下:
- @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);
- //如果超過(guò)10秒,默認(rèn)用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();
- }
- }
- }
運(yùn)行上面的例子,輸入
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.
可以觀察到,在主線程啟動(dòng)后,servlet線程為了處理longProcessing的請(qǐng)求,足足等待了8秒,最后才輸出結(jié)果進(jìn)行響應(yīng),這樣對(duì)于高并發(fā)的應(yīng)用來(lái)說(shuō)這是很大的瓶頸,因?yàn)楸仨氁降鹊酱幚淼姆椒ㄍ瓿珊螅琒ervlet容器中的線程才能繼續(xù)接收其他請(qǐng)求,在此之前,Servlet線程一直處于阻塞狀態(tài)。
在Servlet 3.0規(guī)范前,是有一些相關(guān)的解決方案的,比如常見(jiàn)的就是使用一個(gè)單獨(dú)的工作線程(worker thread)去處理這些耗費(fèi)時(shí)間的工作,而Servlet 容器中的線程在把工作交給工作線程處理后則馬上回收到Servlet容器中去。比如Tomcat的Comet、WebLogic的的FutureResponseServlet和WebSphere的Asynchronous Request Dispatcher都是這類(lèi)型的解決方案。
但只這些方案的弊端是沒(méi)辦法很容易地在不修改代碼的情況下遷移到其他Servlet容器中,這就是Servlet 3中要定義異步Servlet的原因所在。
下面我們通過(guò)例子來(lái)說(shuō)明異步Servlet的實(shí)現(xiàn)方法:
1、 首先設(shè)置servlet要支持異步屬性,這個(gè)只需要設(shè)置asyncSupported屬性為true就可以了。
2、 因?yàn)閷?shí)際上的工作是委托給另外的線程的,我們應(yīng)該實(shí)現(xiàn)一個(gè)線程池,這個(gè)可以通過(guò)使用Executors框架去實(shí)現(xiàn)(具體參考http://www.journaldev.com/1069/java-thread-pool-example-using-executors-and-threadpoolexecutor一文),并且使用Servlet Context listener去初始化線程池。
3、 我們需要通過(guò)ServletRequest.startAsync()方法獲得AsyncContext的實(shí)例。AsyncContext提供了方法去獲得ServletRequest和ServletResponse的對(duì)象引用。它也能使用dispatch()方法去將請(qǐng)求forward到其他資源。
4、 我們將實(shí)現(xiàn)Runnable接口,并且在其實(shí)現(xiàn)方法中處理各種耗時(shí)的任務(wù),然后使用AsyncContext對(duì)象去將請(qǐng)求dispatch到其他資源中去或者使用ServletResponse對(duì)象輸出。一旦處理完畢,將調(diào)用AsyncContext.complete()方法去讓容器知道異步處理已經(jīng)結(jié)束。
5、 我們還可以在AsyncContext對(duì)象增加AsyncListener的實(shí)現(xiàn)類(lèi)以實(shí)現(xiàn)相關(guān)的徽調(diào)方法,可以使用這個(gè)去提供將錯(cuò)誤信息返回給用戶(如超時(shí)或其他出錯(cuò)信息),也可以做一些資源清理的工作。
我們來(lái)看下完成后例子的工程結(jié)構(gòu)圖如下:
#p#
下面我們看下實(shí)現(xiàn)了ServletContextListener類(lèi)的監(jiān)聽(tīng)類(lèi)代碼:
- 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) {
- // 創(chuàng)建線程池
- 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線程的實(shí)現(xiàn)代碼,如下:
- 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) {
- // 休眠指定的時(shí)間
- try {
- Thread.sleep(secs);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
請(qǐng)?jiān)谶@里注意AsyncContext的使用方法,以及當(dāng)完成異步調(diào)用時(shí)必須調(diào)用asyncContext.complete()方法。
現(xiàn)在看下AsyncListener類(lèi)的實(shí)現(xiàn)
- 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");
- //這里可以拋出錯(cuò)誤信息
- }
- @Override
- public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
- System.out.println("AppAsyncListener onStartAsync");
- //可以記錄相關(guān)日志
- }
- @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");
- }
- }
其中請(qǐng)注意可以監(jiān)聽(tīng)onTimeout事件的使用,可以有效地返回給用戶端出錯(cuò)的信息。最后來(lái)重新改寫(xiě)下前文提到的測(cè)試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);
- // 如果超過(guò)10秒則設(shè)置為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.");
- }
- }
下面運(yùn)行這個(gè)Servlet程序,輸入:
http://localhost:8080/AsyncServletExample/AsyncLongRunningServlet?time=8000,運(yùn)行結(jié)果為:
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
但如果我們運(yùn)行一個(gè)time=9999的輸入,則運(yùn)行結(jié)果為:
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主線程很快執(zhí)行完畢并且所有的處理額外的工作都是在另外一個(gè)線程中處理的,不存在阻塞問(wèn)題。
原文鏈接:http://www.javacodegeeks.com/2013/08/async-servlet-feature-of-servlet-3.html