簡介 AsyncContext


每個請求來到 Web 容器,Web 容器會為其分配一條執行緒來專門負責該請求,直到回應完成前,該執行緒都不會被釋放回容器。執行緒會耗用系統資源,若有些請 求需要長時間處理(例如長時間運算、等待某個資源),就會長時間佔用執行緒,若這類的請求很多,許多執行緒都被長時間佔用,對於系統就會是個效能負擔,甚至造成應用程式的效能瓶頸。

基本上一些需長時間處理的請求,通常客戶端也較不在乎請求後要有立即的回應,若可以,讓這類請求先釋放容器分配給該請求的執行緒,讓容器可以有機會將執行緒資源分配給其它的請求,可以減輕系統負擔。原先釋放了容器所分配執行緒的請求,其回應將被延後,直到處理完成(例如長時間運算完成、所需資源已獲得)再行對客戶端的回應。

在 Servlet 3.0 中,在 ServletRequest 上提供了 startAsync() 方法:

AsyncContext startAsync() throws java.lang.IllegalStateException;
AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse)
                        throws java.lang.IllegalStateException 

這兩個方法都會傳回 AsyncContext 介面的實作物件,前者會直接利用原有的請求與回應物件來建立 AsyncContext,後者可以讓你傳入自己建立的請求、回應包裹物件。在呼叫了 startAsync() 方法取得 AsyncContext 物件之後,這次的回應會被延後,並釋放容器所分配的執行緒。

你可以透過 AsyncContextgetRequest()getResponse() 方法取得請求、回應物件,此次對客戶端的回應將暫緩至呼叫 AsyncContextcomplete() 方法或 dispatch() 為止,前者表示回應完成,後者表示將回應調派給指定的 URL。

若要能呼叫 ServletRequeststartAsync() 使用 AsyncContext,你的 Servlet 必須能支援非同步處理,如果使用 @WebServlet 來標註,則可以設定其 asyncSupportedtrue。例如:

@WebServlet(urlPatterns = "/asyncXXX", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
...

如果使用 web.xml 設定 Servlet,則可以設定 <async-supported> 標籤為 true

...
<servlet> 
    <servlet-name>AsyncXXX</servlet-name> 
    <servlet-class>cc.openhome.AsyncXXX</servlet-class> 
    <async-supported>true</async-supported> 
</servlet> 
...

如果 Servlet 將會非同步處理,若其前端有過濾器,則過濾器亦需標示其支援非同步處理,如果使用 @WebFilter,同樣是可以設定其 asyncSupportedtrue。例如:

@WebFilter(urlPatterns = "/asyncXXX", asyncSupported = true) 
public class AsyncXXXFilter implements Filter{
...

如果使用 web.xml 設定過濾器,則可以設定 <async-supported> 標籤為 true

...
<filter> 
    <filter-name>AsyncXXXFilter</filter-name> 
    <filter-class>cc.openhome.AsyncXXXFilter</filter-class> 
    <async-supported>true</async-supported> 
</filter> 
...

底下示範一個非同步處理的例子,對於進來的請求,Servlet 會取得其 AsyncContext,並釋放容器所分配的執行緒,回應被延後,對於這些被延後回應的請求,建立一個 CompletableFuture 物件,使用預設的執行緒池進行非同步處理。

package cc.openhome;

import java.io.*;
import java.util.concurrent.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(
    urlPatterns={"/async"},
    asyncSupported = true
)
public class AsyncServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        response.setContentType("text/html; charset=UTF8");
        AsyncContext ctx = request.startAsync();
        doAsync(ctx).thenApplyAsync(String::toUpperCase)
                    .thenAcceptAsync(resource -> {
                        try {
                            ctx.getResponse().getWriter().println(resource);
                            ctx.complete();
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    });
    } 

    private CompletableFuture<String> doAsync(AsyncContext ctx) {
        return CompletableFuture.supplyAsync(() -> {
             try {
                String resource = ctx.getRequest().getParameter("resource");
                Thread.sleep(10000); // 模擬長時間的處理
                return String.format("%s back finally...XD", resource);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }  
        });
    }
}