WriterListener


你可以試著使用 AsyncContext 來改寫一下〈簡介 ServletContext〉裏的電子書下載範例:

package cc.openhome;

import java.io.*;
import java.util.concurrent.CompletableFuture;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(
    urlPatterns = { "/ebook" }, 
    initParams = {
    @WebInitParam(name = "PDF_FILE", value = "/WEB-INF/jdbc.pdf") }, 
    asyncSupported = true
)
public class Ebook extends HttpServlet {
    private String PDF_FILE;

    @Override
    public void init() throws ServletException {
        super.init();
        PDF_FILE = getInitParameter("PDF_FILE");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String coupon = request.getParameter("coupon");

        if ("123456".equals(coupon)) {
            AsyncContext ctx = request.startAsync();
            CompletableFuture.runAsync(() -> {
                response.setContentType("application/pdf");

                try (InputStream in = getServletContext().getResourceAsStream(PDF_FILE)) {
                    OutputStream out = response.getOutputStream();
                    byte[] buffer = new byte[1024];
                    int length = -1;
                    while ((length = in.read(buffer)) != -1) {
                        out.write(buffer, 0, length);
                    }
                } catch (IOException ex) {
                    throw new UncheckedIOException(ex);
                } finally {
                    ctx.complete();
                }
            });
        }
    }
}

這會使得容器分配的執行緒可以儘快地服務其他請求,然而,回應時的 ServletOutputStream 是阻斷式,而檔案讀取也是,這表示 CompletableFuture 處理時的執行緒,遇到這些阻斷式 I/O 時,然而必須等待,無法儘早回到執行緒池中。

在檔案讀取的部份,你可以試著 NIO2 的非阻斷 API,那麼請求的讀取呢?在 Servlet 3.1 中,ServletOutputStream 可以實現非阻斷輸出,這可以透過對 ServletOutputStream 註冊一個 WriteListener 實例來達到:

package javax.servlet;

import java.io.IOException;

public interface WriteListener extends java.util.EventListener{
    public void onWritePossible() throws IOException;
    public void onError(java.lang.Throwable throwable);
}

ServletOutputStream 可以寫出的時候,會呼叫 onWritePossible 方法,若發生例外的話,會呼叫 onError(),要註冊 WriteListener 實例,必須在非同步 Servlet 中進行,例如,可以將〈簡介 ServletContext〉裏的電子書下載範例改寫,使用 ServletOutputStream 的非阻斷功能:

package cc.openhome;

import java.io.*;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet(
    urlPatterns = { "/ebook" }, 
    initParams = {
    @WebInitParam(name = "PDF_FILE", value = "/WEB-INF/jdbc.pdf") }, 
    asyncSupported = true
)
public class Ebook extends HttpServlet {
    private String PDF_FILE;

    @Override
    public void init() throws ServletException {
        super.init();
        PDF_FILE = getInitParameter("PDF_FILE");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String coupon = request.getParameter("coupon");

        if ("123456".equals(coupon)) {
            AsyncContext ctx = request.startAsync();

            ServletOutputStream out = response.getOutputStream();
            out.setWriteListener(new WriteListener() {
                InputStream in = getServletContext().getResourceAsStream(PDF_FILE);

                @Override
                public void onError(Throwable t) {
                    try {
                        in.close();
                    }
                    catch(IOException ex) {
                        throw new UncheckedIOException(ex);
                    }
                    throw new RuntimeException(t);
                }

                @Override
                public void onWritePossible() throws IOException {
                    byte[] buffer = new byte[1024];
                    int length = 0;
                    while (out.isReady() && (length = in.read(buffer)) != -1) {
                        out.write(buffer, 0, length);
                    }
                    if(length == -1) {
                        in.close();
                        ctx.complete();
                    }
                }
            });
        }
    }
}

在這個例子當中,每次 ServletOutputStream 可以寫出資料時,會呼叫 onWritePossible(),在檔案讀不到資料時,length 會是 -1,這時完成非同步請求,基於簡化範例,檔案讀取的動作就還是先用阻斷式的 API。