servlet3異步原理與實踐

一捣域、什么是Servlet

servlet 是基于 Java 的 Web 組件,由容器進行管理宴合,來生成動態(tài)內(nèi)容焕梅。像其他基于 Java 的組件技術(shù)一樣,servlet 也是基于平臺無關(guān)的 Java 類格式卦洽,被編譯為平臺無關(guān)的字節(jié)碼贞言,可以被基于 Java 技術(shù)的 Web 服務(wù)器動態(tài)加載并運行。容器(Container)阀蒂,有時候也叫做 servlet 引擎该窗,是 Web 服務(wù)器為支持 servlet 功能擴展的部分弟蚀。客戶端通過 servlet 容器實現(xiàn)的 request/response paradigm(請求/應(yīng)答模式) 與 Servlet 進行交互酗失。

二义钉、什么是Servlet規(guī)范

每當一個Servlet版本發(fā)布都會對應(yīng)一個Servlet版本的規(guī)范,比如Servlet2.5规肴、Servlet3.0捶闸、Servlet3.1.
規(guī)范中描述了Java Servlet API 的標準,定義了 Java Servlet API 中類拖刃、接口鉴嗤、方法簽名的完整規(guī)范且附帶的Javadoc 文檔供開發(fā)人員查閱,目的主要是為Java Servlet 給出一個完整和清晰的解釋序调。從下圖可以看出Servlet規(guī)范版本和tomcat支持的版本的對應(yīng)關(guān)系。比如Servlet3是從tomcat7以后開始支持的兔簇。

Servlet和tomcat版本.png

三发绢、同步,異步垄琐,阻塞边酒,非阻塞

同步異步是數(shù)據(jù)通信的方式,阻塞和非阻塞是一種狀態(tài)狸窘。比如同步這種數(shù)據(jù)通訊方式里面可以有阻塞狀態(tài)也可以有非阻塞狀態(tài)墩朦。從另外一個角度理解同步和異步,就是如果一個線程干完的事情都是同步翻擒,有線程切換才能干完的事情就是異步氓涣。

四、Servlet3的異步位置

這里說的位置是指陋气,從tomcat處理整個request請求流程中劳吠,異步處于哪一步。我們先梳理出在NIO模式下(是否使用NIO跟異步?jīng)]有直接關(guān)系巩趁,這里是拿NIO模式下的tomcat流程做說明)痒玩,下面這個圖是tomcat的總體結(jié)構(gòu),里面用箭頭標明了請求線路议慰。

tomcat架構(gòu)圖.png

我們知道在tomcat的組件中Connector和Engine是最核心的兩個組件蠢古,Servlet3的異步處理就是發(fā)生在Connector中。Tomcat的組件之間的協(xié)作關(guān)系别凹,后續(xù)會單獨寫一篇文章介紹草讶。這里先有一個直觀的認識。便與后續(xù)對異步理解番川。

五到涂、Servlet3的異步流程

Servlet異步流程圖.png

接收到request請求之后脊框,由tomcat工作線程從HttpServletRequest中獲得一個異步上下文AsyncContext對象,然后由tomcat工作線程把AsyncContext對象傳遞給業(yè)務(wù)處理線程践啄,同時tomcat工作線程歸還到工作線程池浇雹,這一步就是異步開始。在業(yè)務(wù)處理線程中完成業(yè)務(wù)邏輯的處理屿讽,生成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è)務(wù)邏輯處理線程现横,并將AsyncContext 傳遞給業(yè)務(wù)線程。executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
4阁最、在異步業(yè)務(wù)邏輯處理線程中戒祠,通過asyncContext獲取request和response,處理對應(yīng)的業(yè)務(wù)速种。
5得哆、業(yè)務(wù)邏輯處理線程處理完成邏輯之后,調(diào)用AsyncContext 的complete方法哟旗。asyncContext.complete();從而結(jié)束該次異步線程處理贩据。

七、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有對應(yīng)的超時時間,如果在指定的時間內(nèi)沒有執(zhí)行完操作舍沙,response依然會走原來Servlet的結(jié)束邏輯近上,后續(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è)務(wù)線程池
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è)務(wù)工作線程
package com.test.servlet3;

import javax.servlet.AsyncContext;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Created by wangxindong on 2017/10/19.
 * 業(yè)務(wù)工作線程
 */
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的關(guān)系

對于這幾個概念往往會混淆,這里做一個梳理比較岖是,nio是一種IO的模型帮毁,對比與傳統(tǒng)的BIO,它可以利用較少的線程處理更多的連接從而增加機器的吞吐量豺撑,Tomcat NIO Connector是Tomcat的一種NIO連接模式烈疚。異步,前面提到他是一種通訊的方式聪轿,它跟NIO沒有任務(wù)關(guān)系爷肝,及時沒有NIO也可以實現(xiàn)異步,Servlet 3.0 Async是指Servlet 3規(guī)范以后支持了異步處理Servlet請求陆错,我們可以把請求線程和業(yè)務(wù)線程分開阶剑。Spring MVC Async是在Servlet3異步的基礎(chǔ)上做了一層封裝。具體的區(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相關(guān)技術(shù)文章。

8.2沽甥、Servlet 3.0 Async

是說Servlet 3.0支持了業(yè)務(wù)請求的異步處理声邦,Servlet3之前一個請求的處理流程,請求解析摆舟、READ BODY,RESPONSE BODY,以及其中的業(yè)務(wù)邏輯處理都由Tomcat線程池中的一個線程進行處理的亥曹。那么3.0以后我們可以讓請求線程(IO線程)和業(yè)務(wù)處理線程分開,進而對業(yè)務(wù)進行線程池隔離恨诱。我們還可以根據(jù)業(yè)務(wù)重要性進行業(yè)務(wù)分級媳瞪,然后再把線程池分級。還可以根據(jù)這些分級做其它操作比如監(jiān)控和降級處理照宝。servlet 3.0 從 tomcat 7.x 開始支持蛇受。

8.3、Spring MVC Async

是Spring MVC 3.2 以上版本基于Servlet 3的基礎(chǔ)做的封裝厕鹃,原理及實現(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è)務(wù)邏輯異步處理,將處理結(jié)果 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è)務(wù)邏輯START
            //...
            //業(yè)務(wù)邏輯END
            result.setResult(result);
        }
    }
}

九乍丈、Servlet3非阻塞IO

Servlet3.1以后增加了非阻塞IO實現(xiàn),需要Tomcat8.x以上支持把将。根據(jù)Servlet3.1規(guī)范中的描述”非阻塞 IO 僅對在 Servlet 中的異步處理請求有效轻专,否則,當調(diào)用 ServletInputStream.setReadListener 或ServletOutputStream.setWriteListener 方法時將拋出IllegalStateException“秸弛∶簦可以說Servlet3的非阻塞IO是對Servlet3異步的增強。Servlet3的非阻塞是利用java.util.EventListener的事件驅(qū)動機制來實現(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);//設(shè)置異步調(diào)用超時時長

        ServletInputStream in = request.getInputStream();
        //異步讀冉柿濉(實現(xiàn)了非阻塞式讀染涤辍)
        in.setReadListener(new MyReadListener(in,actx));
        //直接輸出到頁面的內(nèi)容(不等異步完成就直接給頁面)
        PrintWriter out = response.getWriter();
        out.println("<h1>直接返回頁面,不等異步處理結(jié)果了</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ā)調(diào)用
    @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ā)調(diào)用
    @Override
    public void onError(Throwable t){
        System.out.println("數(shù)據(jù) 出錯");
        t.printStackTrace();
    }
}

Servlet3.1的非阻塞IO從下面圖中可以看出是面對InputStream 和 OutPutStream流的,這里的非阻塞IO跟我們常說的JDK NIO不是一個概念菲盾,Servlet3.1的非阻塞是同jdk的事件驅(qū)動機制來實現(xiàn)颓影。
public interface ReadListener extends java.util.EventListener

Servlet異步處理流程含非阻塞IO圖.png

十、總結(jié)

通訊模型中的NIO可以利用很少的線程處理大量的連接懒鉴,提高了機器的吞吐量诡挂。Servlet的異步處理機制使得我們可以將請求異步到獨立的業(yè)務(wù)線程去執(zhí)行,使得我們能夠?qū)⒄埱缶€程和業(yè)務(wù)線程分離临谱。通訊模型的NIO跟Servlet3的異步?jīng)]有直接關(guān)系璃俗。但是我們將兩種技術(shù)同時使用就更增加了以tomcat為容器的系統(tǒng)的處理能力。自從Servlet3.1以后增加了非阻塞的IO悉默,這里的非阻塞IO是面向inputstream和outputstream流城豁,通過jdk的事件驅(qū)動模型來實現(xiàn),更一步增強了Servlet異步的高性能抄课,可以認為是一種增強版的異步機制唱星。

轉(zhuǎn)載請注明作者及出處,并附上鏈接http://www.reibang.com/p/c23ca9d26f64

參考資料
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
http://www.cnblogs.com/davenkin/p/async-servlet.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跟磨,一起剝皮案震驚了整個濱河市魏颓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吱晒,老刑警劉巖甸饱,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡叹话,警方通過查閱死者的電腦和手機偷遗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驼壶,“玉大人氏豌,你說我怎么就攤上這事∪劝迹” “怎么了泵喘?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長般妙。 經(jīng)常有香客問我纪铺,道長,這世上最難降的妖魔是什么碟渺? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任鲜锚,我火速辦了婚禮,結(jié)果婚禮上苫拍,老公的妹妹穿的比我還像新娘芜繁。我一直安慰自己,他們只是感情好绒极,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布骏令。 她就那樣靜靜地躺著,像睡著了一般垄提。 火紅的嫁衣襯著肌膚如雪榔袋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天塔淤,我揣著相機與錄音,去河邊找鬼速妖。 笑死高蜂,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的罕容。 我是一名探鬼主播备恤,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锦秒!你這毒婦竟也來了露泊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤旅择,失蹤者是張志新(化名)和其女友劉穎惭笑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡沉噩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年捺宗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片川蒙。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡蚜厉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出畜眨,到底是詐尸還是另有隱情昼牛,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布康聂,位于F島的核電站贰健,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏早抠。R本人自食惡果不足惜霎烙,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蕊连。 院中可真熱鬧悬垃,春花似錦、人聲如沸甘苍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽载庭。三九已至看彼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間囚聚,已是汗流浹背靖榕。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留顽铸,地道東北人茁计。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像谓松,于是被迫代替她去往敵國和親星压。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容