servlet3異步原理與實踐

一、什麼是Servlet

servlet 是基於 Java 的 Web 組件,由容器進行管理,來生成動態內容。像其他基於 Java 的組件技術一樣,servlet 也是基於平臺無關的 Java 類格式,被編譯爲平臺無關的字節碼,可以被基於 Java 技術的 Web 服務器動態加載並運行。容器(Container),有時候也叫做 servlet 引擎,是 Web 服務器爲支持 servlet 功能擴展的部分。客戶端通過 servlet 容器實現的 request/response paradigm(請求/應答模式) 與 Servlet 進行交互。

二、什麼是Servlet規範

每當一個Servlet版本發佈都會對應一個Servlet版本的規範,比如Servlet2.5、Servlet3.0、Servlet3.1.
規範中描述了Java Servlet API 的標準,定義了 Java Servlet API 中類、接口、方法簽名的完整規範且附帶的Javadoc 文檔供開發人員查閱,目的主要是爲Java Servlet 給出一個完整和清晰的解釋。從下圖可以看出Servlet規範版本和tomcat支持的版本的對應關係。比如Servlet3是從tomcat7以後開始支持的。

Servlet和tomcat版本.png
Servlet和tomcat版本.png

三、同步,異步,阻塞,非阻塞

同步異步是數據通信的方式,阻塞和非阻塞是一種狀態。比如同步這種數據通訊方式裏面可以有阻塞狀態也可以有非阻塞狀態。從另外一個角度理解同步和異步,就是如果一個線程幹完的事情都是同步,有線程切換才能幹完的事情就是異步。

四、Servlet3的異步位置

這裏說的位置是指,從tomcat處理整個request請求流程中,異步處於哪一步。我們先梳理出在NIO模式下(是否使用NIO跟異步沒有直接關係,這裏是拿NIO模式下的tomcat流程做說明),下面這個圖是tomcat的總體結構,裏面用箭頭標明瞭請求線路。

tomcat架構圖.png
tomcat架構圖.png

我們知道在tomcat的組件中Connector和Engine是最核心的兩個組件,Servlet3的異步處理就是發生在Connector中。Tomcat的組件之間的協作關係,後續會單獨寫一篇文章介紹。這裏先有一個直觀的認識。便與後續對異步理解。

五、Servlet3的異步流程

Servlet異步流程圖.png
Servlet異步流程圖.png

接收到request請求之後,由tomcat工作線程從HttpServletRequest中獲得一個異步上下文AsyncContext對象,然後由tomcat工作線程把AsyncContext對象傳遞給業務處理線程,同時tomcat工作線程歸還到工作線程池,這一步就是異步開始。在業務處理線程中完成業務邏輯的處理,生成response返回給客戶端。在Servlet3.0中雖然處理請求可以實現異步,但是InputStream和OutputStream的IO操作還是阻塞的,當數據量大的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、開啓業務邏輯處理線程,並將AsyncContext 傳遞給業務線程。executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
4、在異步業務邏輯處理線程中,通過asyncContext獲取request和response,處理對應的業務。
5、業務邏輯處理線程處理完成邏輯之後,調用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有對應的超時時間,如果在指定的時間內沒有執行完操作,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.");
    }
}
7.2、AppAsyncListener.java 異步監聽器
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上下文監聽器,可以在裏面初始化業務線程池
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();
    }
}
7.4、AsyncRequestProcessor.java 業務工作線程
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();
        }
    }
}

八、Tomcat NIO Connector ,Servlet 3.0 Async,Spring MVC Async的關係

對於這幾個概念往往會混淆,這裏做一個梳理比較,nio是一種IO的模型,對比與傳統的BIO,它可以利用較少的線程處理更多的連接從而增加機器的吞吐量,Tomcat NIO Connector是Tomcat的一種NIO連接模式。異步,前面提到他是一種通訊的方式,它跟NIO沒有任務關係,及時沒有NIO也可以實現異步,Servlet 3.0 Async是指Servlet 3規範以後支持了異步處理Servlet請求,我們可以把請求線程和業務線程分開。Spring MVC Async是在Servlet3異步的基礎上做了一層封裝。具體的區別如下:

8.1、Tomcat NIO Connector

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相關技術文章。

8.2、Servlet 3.0 Async

是說Servlet 3.0支持了業務請求的異步處理,Servlet3之前一個請求的處理流程,請求解析、READ BODY,RESPONSE BODY,以及其中的業務邏輯處理都由Tomcat線程池中的一個線程進行處理的。那麼3.0以後我們可以讓請求線程(IO線程)和業務處理線程分開,進而對業務進行線程池隔離。我們還可以根據業務重要性進行業務分級,然後再把線程池分級。還可以根據這些分級做其它操作比如監控和降級處理。servlet 3.0 從 tomcat 7.x 開始支持。

8.3、Spring MVC Async

是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非阻塞IO

Servlet3.1以後增加了非阻塞IO實現,需要Tomcat8.x以上支持。根據Servlet3.1規範中的描述”非阻塞 IO 僅對在 Servlet 中的異步處理請求有效,否則,當調用 ServletInputStream.setReadListener 或ServletOutputStream.setWriteListener 方法時將拋出IllegalStateException“。可以說Servlet3的非阻塞IO是對Servlet3異步的增強。Servlet3的非阻塞是利用java.util.EventListener的事件驅動機制來實現的。

9.1、AsyncLongRunningServlet.java 接收請求,獲取讀取請求監聽器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();
        //異步讀取(實現了非阻塞式讀取)
        in.setReadListener(new MyReadListener(in,actx));
        //直接輸出到頁面的內容(不等異步完成就直接給頁面)
        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;
    }
    //數據可用時觸發執行
    @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();
    }
}

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

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

十、總結

通訊模型中的NIO可以利用很少的線程處理大量的連接,提高了機器的吞吐量。Servlet的異步處理機制使得我們可以將請求異步到獨立的業務線程去執行,使得我們能夠將請求線程和業務線程分離。通訊模型的NIO跟Servlet3的異步沒有直接關係。但是我們將兩種技術同時使用就更增加了以tomcat爲容器的系統的處理能力。自從Servlet3.1以後增加了非阻塞的IO,這裏的非阻塞IO是面向inputstream和outputstream流,通過jdk的事件驅動模型來實現,更一步增強了Servlet異步的高性能,可以認爲是一種增強版的異步機制。

轉載請註明作者及出處,並附上鍊接http://blog.csdn.net/wangxindong11/article/details/78591396

第一時間分享技術
第一時間分享技術

參考資料
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章