servlet 是基於 Java 的 Web 組件,由容器進行管理,來生成動態內容。像其餘基於 Java 的組件技術同樣,servlet 也是基於平臺無關的 Java 類格式,被編譯爲平臺無關的字節碼,能夠被基於 Java 技術的 Web 服務器動態加載並運行。容器(Container),有時候也叫作 servlet 引擎,是 Web 服務器爲支持 servlet 功能擴展的部分。客戶端經過 servlet 容器實現的 request/response paradigm(請求/應答模式) 與 Servlet 進行交互。html
每當一個Servlet版本發佈都會對應一個Servlet版本的規範,好比Servlet2.五、Servlet3.0、Servlet3.1.
規範中描述了Java Servlet API 的標準,定義了 Java Servlet API 中類、接口、方法簽名的完整規範且附帶的Javadoc 文檔供開發人員查閱,目的主要是爲Java Servlet 給出一個完整和清晰的解釋。從下圖能夠看出Servlet規範版本和tomcat支持的版本的對應關係。好比Servlet3是從tomcat7之後開始支持的。java
Servlet和tomcat版本.pngapache
同步異步是數據通訊的方式,阻塞和非阻塞是一種狀態。好比同步這種數據通信方式裏面能夠有阻塞狀態也能夠有非阻塞狀態。從另一個角度理解同步和異步,就是若是一個線程幹完的事情都是同步,有線程切換才能幹完的事情就是異步。tomcat
這裏說的位置是指,從tomcat處理整個request請求流程中,異步處於哪一步。咱們先梳理出在NIO模式下(是否使用NIO跟異步沒有直接關係,這裏是拿NIO模式下的tomcat流程作說明),下面這個圖是tomcat的整體結構,裏面用箭頭標明瞭請求線路。服務器
tomcat架構圖.png架構
咱們知道在tomcat的組件中Connector和Engine是最核心的兩個組件,Servlet3的異步處理就是發生在Connector中。Tomcat的組件之間的協做關係,後續會單獨寫一篇文章介紹。這裏先有一個直觀的認識。便與後續對異步理解。app
Servlet異步處理流程圖.png異步
接收到request請求以後,由tomcat工做線程從HttpServletRequest中得到一個異步上下文AsyncContext對象,而後由tomcat工做線程把AsyncContext對象傳遞給業務處理線程,同時tomcat工做線程歸還到工做線程池,這一步就是異步開始。在業務處理線程中完成業務邏輯的處理,生成response返回給客戶端。在Servlet3.0中雖然處理請求能夠實現異步,可是InputStream和OutputStream的IO操做仍是阻塞的,當數據量大的request body 或者 response body的時候,就會致使沒必要要的等待。從Servlet3.1之後增長了非阻塞IO,須要tomcat8.x支持。async
咱們使用的大體步驟以下:
一、聲明Servlet,增長asyncSupported屬性,開啓異步支持。@WebServlet(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
二、經過request獲取異步上下文AsyncContext。AsyncContext asyncCtx = request.startAsync();
三、開啓業務邏輯處理線程,並將AsyncContext 傳遞給業務線程。executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
四、在異步業務邏輯處理線程中,經過asyncContext獲取request和response,處理對應的業務。
五、業務邏輯處理線程處理完成邏輯以後,調用AsyncContext 的complete方法。asyncContext.complete();從而結束該次異步線程處理。ide
package com.test.servlet3; 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; import java.io.IOException; import java.util.concurrent.ThreadPoolExecutor; /** * Created by wangxindong on 2017/10/19. */ @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); // max 10 seconds if (secs > 10000) secs = 10000; AsyncContext asyncCtx = request.startAsync(); asyncCtx.addListener(new AppAsyncListener()); asyncCtx.setTimeout(9000);//異步servlet的超時時間,異步Servlet有對應的超時時間,若是在指定的時間內沒有執行完操做,response依然會走原來Servlet的結束邏輯,後續的異步操做執行完再寫回的時候,可能會遇到異常。 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."); } }
package com.test.servlet3; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebListener; import java.io.IOException; import java.io.PrintWriter; /** * Created by wangxindong on 2017/10/19. */ @WebListener public class AppAsyncListener implements AsyncListener { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onComplete"); // we can do resource cleanup activity here } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onError"); //we can return error response to client } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onStartAsync"); //we can log the event here } @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("AppAsyncListener onTimeout"); //we can send appropriate response to client ServletResponse response = asyncEvent.getAsyncContext().getResponse(); PrintWriter out = response.getWriter(); out.write("TimeOut Error in Processing"); } }
package com.test.servlet3; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Created by wangxindong on 2017/10/19. * 在監聽中初始化線程池 */ @WebListener public class AppContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent servletContextEvent) { // create the thread pool 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(); } }
package com.test.servlet3; import javax.servlet.AsyncContext; import java.io.IOException; import java.io.PrintWriter; /** * Created by wangxindong on 2017/10/19. * 業務工做線程 */ 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(); } //complete the processing asyncContext.complete(); } private void longProcessing(int secs) { // wait for given time before finishing try { Thread.sleep(secs); } catch (InterruptedException e) { e.printStackTrace(); } } }
對於這幾個概念每每會混淆,這裏作一個梳理比較,nio是一種IO的模型,對比與傳統的BIO,它能夠利用較少的線程處理更多的鏈接從而增長機器的吞吐量,Tomcat NIO Connector是Tomcat的一種NIO鏈接模式。異步,前面提到他是一種通信的方式,它跟NIO沒有任務關係,及時沒有NIO也能夠實現異步,Servlet 3.0 Async是指Servlet 3規範之後支持了異步處理Servlet請求,咱們能夠把請求線程和業務線程分開。Spring MVC Async是在Servlet3異步的基礎上作了一層封裝。具體的區別以下:
Tomcat的Connector 有三種模式,BIO,NIO,APR,Tomcat NIO Connector是其中的NIO模式,使得tomcat容器能夠用較少的線程處理大量的鏈接請求,再也不是傳統的一請求一線程模式。Tomcat的server.xml配置protocol="org.apache.coyote.http11.Http11NioProtocol",Http11NioProtocol 從 tomcat 6.x 開始支持。NIO的細節能夠參看NIO相關技術文章。
是說Servlet 3.0支持了業務請求的異步處理,Servlet3以前一個請求的處理流程,請求解析、READ BODY,RESPONSE BODY,以及其中的業務邏輯處理都由Tomcat線程池中的一個線程進行處理的。那麼3.0之後咱們可讓請求線程(IO線程)和業務處理線程分開,進而對業務進行線程池隔離。咱們還能夠根據業務重要性進行業務分級,而後再把線程池分級。還能夠根據這些分級作其它操做好比監控和降級處理。servlet 3.0 從 tomcat 7.x 開始支持。
是Spring MVC 3.2 以上版本基於Servlet 3的基礎作的封裝,原理及實現方式同上,使用方式以下:
@Controller @RequestMapping("/async/TestController") public class TestController { @ResponseBody @RequestMapping("/{testUrl}") public DeferredResult<ResponseEntity<String>> testProcess(@PathVariable String testUrl) { final DeferredResult<ResponseEntity<String>> deferredResult = new DeferredResult<ResponseEntity<String>>(); // 業務邏輯異步處理,將處理結果 set 到 DeferredResult new Thread(new AsyncTask(deferredResult)).start(); return deferredResult; } private static class AsyncTask implements Runnable { private DeferredResult result; private AsyncTask(DeferredResult result) { this.result = result; } @Override public void run() { //業務邏輯START //... //業務邏輯END result.setResult(result); } } }
Servlet3.1之後增長了非阻塞IO實現,須要Tomcat8.x以上支持。根據Servlet3.1規範中的描述」非阻塞 IO 僅對在 Servlet 中的異步處理請求有效,不然,當調用 ServletInputStream.setReadListener 或ServletOutputStream.setWriteListener 方法時將拋出IllegalStateException「。能夠說Servlet3的非阻塞IO是對Servlet3異步的加強。Servlet3的非阻塞是利用java.util.EventListener的事件驅動機制來實現的。
package com.test.servlet3Noblock; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * Created by wangxindong on 2017/10/23. */ @WebServlet(urlPatterns = "/AsyncLongRunningServlet2", asyncSupported = true) public class AsyncLongRunningServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); AsyncContext actx = request.startAsync();//經過request得到AsyncContent對象 actx.setTimeout(30*3000);//設置異步調用超時時長 ServletInputStream in = request.getInputStream(); //異步讀取(實現了非阻塞式讀取) in.setReadListener(new MyReadListener(in,actx)); //直接輸出到頁面的內容(不等異步完成就直接給頁面) PrintWriter out = response.getWriter(); out.println("<h1>直接返回頁面,不等異步處理結果了</h1>"); out.flush(); } }
package com.test.servlet3Noblock; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import java.io.IOException; import java.io.PrintWriter; /** * Created by wangxindong on 2017/10/23. */ public class MyReadListener implements ReadListener { private ServletInputStream inputStream; private AsyncContext asyncContext; public MyReadListener(ServletInputStream input,AsyncContext context){ this.inputStream = input; this.asyncContext = context; } //數據可用時觸發執行 @Override public void onDataAvailable() throws IOException { System.out.println("數據可用時觸發執行"); } //數據讀完時觸發調用 @Override public void onAllDataRead() throws IOException { try { Thread.sleep(3000);//暫停5秒,模擬耗時處理數據 PrintWriter out = asyncContext.getResponse().getWriter(); out.write("數據讀完了"); out.flush(); System.out.println("數據讀完了"); } catch (InterruptedException e) { e.printStackTrace(); } } //數據出錯觸發調用 @Override public void onError(Throwable t){ System.out.println("數據 出錯"); t.printStackTrace(); } }
public interface ReadListener extends java.util.EventListener
通信模型中的NIO能夠利用不多的線程處理大量的鏈接,提升了機器的吞吐量。Servlet的異步處理機制使得咱們能夠將請求異步到獨立的業務線程去執行,使得咱們可以將請求線程和業務線程分離。通信模型的NIO跟Servlet3的異步沒有直接關係。可是咱們將兩種技術同時使用就更增長了以tomcat爲容器的系統的處理能力。自從Servlet3.1之後增長了非阻塞的IO,這裏的非阻塞IO是面向inputstream和outputstream流,經過jdk的事件驅動模型來實現,更一步加強了Servlet異步的高性能,能夠認爲是一種加強版的異步機制。
轉載請註明做者及出處,並附上連接https://my.oschina.net/wangxindong/blog/1555194
能夠關注個人公衆號,同步更新技術文章
參考資料
https://tomcat.apache.org/tomcat-7.0-doc/config/http.html
http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/org/apache/catalina/connector/Request.java tomcat源碼地址
https://www.journaldev.com/2008/async-servlet-example