你可以試著使用 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。