淺談servlet3異步原理與實踐
一、什么是Servlet
servlet 是基于 Java 的 Web 組件,由容器進行管理,來生成動態(tài)內(nèi)容。像其他基于 Java 的組件技術一樣,servlet 也是基于平臺無關的 Java 類格式,被編譯為平臺無關的字節(jié)碼,可以被基于 Java 技術的 Web 服務器動態(tài)加載并運行。容器(Container),有時候也叫做 servlet 引擎,是 Web 服務器為支持 servlet 功能擴展的部分??蛻舳送ㄟ^ servlet 容器實現(xiàn)的 request/response paradigm(請求/應答模式) 與 Servlet 進行交互。
二、什么是Servlet規(guī)范
每當一個Servlet版本發(fā)布都會對應一個Servlet版本的規(guī)范,比如Servlet2.5、Servlet3.0、Servlet3.1.
規(guī)范中描述了Java Servlet API 的標準,定義了 Java Servlet API 中類、接口、方法簽名的完整規(guī)范且附帶的Javadoc 文檔供開發(fā)人員查閱,目的主要是為Java Servlet 給出一個完整和清晰的解釋。從下圖可以看出Servlet規(guī)范版本和tomcat支持的版本的對應關系。比如Servlet3是從tomcat7以后開始支持的。

Servlet和tomcat版本.png
三、同步,異步,阻塞,非阻塞
同步異步是數(shù)據(jù)通信的方式,阻塞和非阻塞是一種狀態(tài)。比如同步這種數(shù)據(jù)通訊方式里面可以有阻塞狀態(tài)也可以有非阻塞狀態(tài)。
四、Servlet3的異步位置
這里說的位置是指,從tomcat處理整個request請求流程中,異步處于哪一步。我們先梳理出在NIO模式下(是否使用NIO跟異步?jīng)]有直接關系,這里是拿NIO模式下的tomcat流程做說明),下面這個圖是tomcat的總體結構,里面用箭頭標明了請求線路。

tomcat架構圖.png
我們知道在tomcat的組件中Connector和Engine是最核心的兩個組件,Servlet3的異步處理就是發(fā)生在Connector中。Tomcat的組件之間的協(xié)作關系,后續(xù)會單獨寫一篇文章介紹。這里先有一個直觀的認識。便與后續(xù)對異步理解。
五、Servlet3的異步流程

Servlet異步處理流程圖.png
接收到request請求之后,由tomcat工作線程從HttpServletRequest中獲得一個異步上下文AsyncContext對象,然后由tomcat工作線程把AsyncContext對象傳遞給業(yè)務處理線程,同時tomcat工作線程歸還到工作線程池,這一步就是異步開始。在業(yè)務處理線程中完成業(yè)務邏輯的處理,生成response返回給客戶端。在Servlet3.0中雖然處理請求可以實現(xiàn)異步,但是InputStream和OutputStream的IO操作還是阻塞的,當數(shù)據(jù)量大的request body 或者 response body的時候,就會導致不必要的等待。從Servlet3.1以后增加了非阻塞IO,需要tomcat8.x支持。
六、Servlet3的異步使用步驟
我們使用的大致步驟如下:
1、聲明Servlet,增加asyncSupported屬性,開啟異步支持。@WebServlet(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
2、通過request獲取異步上下文AsyncContext。AsyncContext asyncCtx = request.startAsync();
3、開啟業(yè)務邏輯處理線程,并將AsyncContext 傳遞給業(yè)務線程。executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
4、在異步業(yè)務邏輯處理線程中,通過asyncContext獲取request和response,處理對應的業(yè)務。
5、業(yè)務邏輯處理線程處理完成邏輯之后,調用AsyncContext 的complete方法。asyncContext.complete();從而結束該次異步線程處理。
七、Servlet3的異步使用示例
7.1、AsyncLongRunningServlet.java 處理Servlet請求,并開啟異步
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有對應的超時時間,如果在指定的時間內(nèi)沒有執(zhí)行完操作,response依然會走原來Servlet的結束邏輯,后續(xù)的異步操作執(zhí)行完再寫回的時候,可能會遇到異常。
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.");
}
}
7.2、AppAsyncListener.java 異步監(jiān)聽器
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");
}
}
7.3、AppContextListener.java Servlet上下文監(jiān)聽器,可以在里面初始化業(yè)務線程池
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.
* 在監(jiān)聽中初始化線程池
*/
@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();
}
}
7.4、AsyncRequestProcessor.java 業(yè)務工作線程
package com.test.servlet3;
import javax.servlet.AsyncContext;
import java.io.IOException;
import java.io.PrintWriter;
/**
* Created by wangxindong on 2017/10/19.
* 業(yè)務工作線程
*/
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();
}
}
}
八、Tomcat NIO Connector ,Servlet 3.0 Async,Spring MVC Async的關系
對于這幾個概念往往會混淆,這里做一個梳理比較,nio是一種IO的模型,對比與傳統(tǒng)的BIO,它可以利用較少的線程處理更多的連接從而增加機器的吞吐量,Tomcat NIO Connector是Tomcat的一種NIO連接模式。異步,前面提到他是一種通訊的方式,它跟NIO沒有任務關系,及時沒有NIO也可以實現(xiàn)異步,Servlet 3.0 Async是指Servlet 3規(guī)范以后支持了異步處理Servlet請求,我們可以把請求線程和業(yè)務線程分開。Spring MVC Async是在Servlet3異步的基礎上做了一層封裝。具體的區(qū)別如下:
8.1、Tomcat NIO Connector
Tomcat的Connector 有三種模式,BIO,NIO,APR,Tomcat NIO Connector是其中的NIO模式,使得tomcat容器可以用較少的線程處理大量的連接請求,不再是傳統(tǒng)的一請求一線程模式。Tomcat的server.xml配置protocol="org.apache.coyote.http11.Http11NioProtocol",Http11NioProtocol 從 tomcat 6.x 開始支持。NIO的細節(jié)可以參看NIO相關技術文章。
8.2、Servlet 3.0 Async
是說Servlet 3.0支持了業(yè)務請求的異步處理,Servlet3之前一個請求的處理流程,請求解析、READ BODY,RESPONSE BODY,以及其中的業(yè)務邏輯處理都由Tomcat線程池中的一個線程進行處理的。那么3.0以后我們可以讓請求線程(IO線程)和業(yè)務處理線程分開,進而對業(yè)務進行線程池隔離。我們還可以根據(jù)業(yè)務重要性進行業(yè)務分級,然后再把線程池分級。還可以根據(jù)這些分級做其它操作比如監(jiān)控和降級處理。servlet 3.0 從 tomcat 7.x 開始支持。
8.3、Spring MVC Async
是Spring MVC 3.2 以上版本基于Servlet 3的基礎做的封裝,原理及實現(xiàn)方式同上,使用方式如下:
@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>>();
// 業(yè)務邏輯異步處理,將處理結果 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() {
//業(yè)務邏輯START
//...
//業(yè)務邏輯END
result.setResult(result);
}
}
}
九、Servlet3非阻塞IO
Servlet3.1以后增加了非阻塞IO實現(xiàn),需要Tomcat8.x以上支持。根據(jù)Servlet3.1規(guī)范中的描述”非阻塞 IO 僅對在 Servlet 中的異步處理請求有效,否則,當調用 ServletInputStream.setReadListener 或ServletOutputStream.setWriteListener 方法時將拋出IllegalStateException“??梢哉fServlet3的非阻塞IO是對Servlet3異步的增強。Servlet3的非阻塞是利用java.util.EventListener的事件驅動機制來實現(xiàn)的。
9.1、AsyncLongRunningServlet.java 接收請求,獲取讀取請求監(jiān)聽器ReadListener
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();
//異步讀?。▽崿F(xiàn)了非阻塞式讀?。?
in.setReadListener(new MyReadListener(in,actx));
//直接輸出到頁面的內(nèi)容(不等異步完成就直接給頁面)
PrintWriter out = response.getWriter();
out.println("<h1>直接返回頁面,不等異步處理結果了</h1>");
out.flush();
}
}
9.2、MyReadListener.java 異步處理
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;
}
//數(shù)據(jù)可用時觸發(fā)執(zhí)行
@Override
public void onDataAvailable() throws IOException {
System.out.println("數(shù)據(jù)可用時觸發(fā)執(zhí)行");
}
//數(shù)據(jù)讀完時觸發(fā)調用
@Override
public void onAllDataRead() throws IOException {
try {
Thread.sleep(3000);//暫停5秒,模擬耗時處理數(shù)據(jù)
PrintWriter out = asyncContext.getResponse().getWriter();
out.write("數(shù)據(jù)讀完了");
out.flush();
System.out.println("數(shù)據(jù)讀完了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//數(shù)據(jù)出錯觸發(fā)調用
@Override
public void onError(Throwable t){
System.out.println("數(shù)據(jù) 出錯");
t.printStackTrace();
}
}
十、總結
通訊模型中的NIO可以利用很少的線程處理大量的連接,提高了機器的吞吐量。Servlet的異步處理機制使得我們可以將請求異步到獨立的業(yè)務線程去執(zhí)行,使得我們能夠將請求線程和業(yè)務線程分離。通訊模型的NIO跟Servlet3的異步?jīng)]有直接關系。但是我們將兩種技術同時使用就更增加了以tomcat為容器的系統(tǒng)的處理能力。自從Servlet3.1以后增加了非阻塞的IO,這里的非阻塞IO是面向inputstream和outputstream流,通過jdk的事件驅動模型來實現(xiàn),更一步增強了Servlet異步的高性能,可以認為是一種增強版的異步機制。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
使用spring mail發(fā)送html郵件的示例代碼
本篇文章主要介紹了使用spring mail發(fā)送html郵件的示例代碼,這里整理了詳細的示例代碼,具有一定的參考價值,有興趣的可以了解一下2017-09-09
Java字符串拼接的五種方法及性能比較分析(從執(zhí)行100次到90萬次)
字符串拼接一般使用“+”,但是“+”不能滿足大批量數(shù)據(jù)的處理,Java中有以下五種方法處理字符串拼接及性能比較分析,感興趣的可以了解一下2021-12-12
Spring Boot 項目做性能監(jiān)控的操作流程
這篇文章主要介紹了Spring Boot 項目如何做性能監(jiān)控,本文通過實例代碼圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07

