你可以試著使用 AsyncContext
來改寫一下〈getPart()、getParts()〉裏的檔案上傳範例:
package cc.openhome;
import java.io.*;
import java.util.concurrent.CompletableFuture;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
@MultipartConfig
@WebServlet(
urlPatterns={"/asyncUpload"},
asyncSupported = true
)
public class AsyncUpload extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
AsyncContext ctx = request.startAsync();
doAsyncUpload(ctx).thenRun(() -> {
try {
ctx.getResponse().getWriter().println("Upload Successfully");
ctx.complete();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
private CompletableFuture<Void> doAsyncUpload(AsyncContext ctx)
throws IOException, ServletException {
Part photo = ((HttpServletRequest) ctx.getRequest()).getPart("photo");
String filename = photo.getSubmittedFileName();
return CompletableFuture.runAsync(() -> {
// 阻斷式 I/O
try(InputStream in = photo.getInputStream();
OutputStream out = new FileOutputStream("c:/workspace/" + filename)) {
byte[] buffer = new byte[1024];
int length = -1;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
這會使得容器分配的執行緒可以儘快地服務其他請求,然而,請求的 getPart()
是阻斷式,而檔案寫入也是,這表示 CompletableFuture
處理時的執行緒,遇到這些阻斷式 I/O 時,然而必須等待,無法儘早回到執行緒池中。
在檔案寫出的部份,你可以試著 NIO2 的非阻斷 API,那麼請求的讀取呢?在 Servlet 3.1 中,ServletInputStream
可以實現非阻斷輸入,這可以透過對 ServletInputStream
註冊一個 ReadListener
實例來達到:
package javax.servlet;
import java.io.IOException;
public interface ReadListener extends java.util.EventListener{
public abstract void onDataAvailable() throws IOException;
public abstract void onAllDataRead() throws IOException;
public abstract void onError(java.lang.Throwable throwable);
}
在 ServletInputStream
有資料的時候,會呼叫 onDataAvailable()
方法,而全部資料讀取完畢後會呼叫 onAllDataRead()
,若發生例外的話,會呼叫 onError()
,要註冊 ReadListener
實例,必須在非同步 Servlet 中進行,例如,可以將〈getReader()、 getInputStream()〉中檔案上傳的範例改寫,使用 ServletInputStream
的非阻斷功能:
package cc.openhome;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
@WebServlet(
urlPatterns = { "/asyncUpload" },
asyncSupported = true
)
public class AsyncUpload extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
AsyncContext ctx = request.startAsync();
ServletInputStream in = request.getInputStream();
in.setReadListener(new ReadListener() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
@Override
public void onDataAvailable() throws IOException {
byte[] buffer = new byte[1024];
int length = -1;
while(in.isReady() && (length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
}
@Override
public void onAllDataRead() throws IOException {
// 這邊依舊是阻斷式輸出
byte[] body = out.toByteArray();
String contentAsTxt = new String(body, "ISO-8859-1");
String filename = filename(contentAsTxt);
Range fileRange = fileRange(contentAsTxt, request.getContentType());
write(body,
contentAsTxt.substring(0, fileRange.start)
.getBytes("ISO-8859-1")
.length,
contentAsTxt.substring(0, fileRange.end)
.getBytes("ISO-8859-1")
.length,
"c:/workspace/" + filename
);
response.getWriter().println("Upload Successfully");
ctx.complete();
}
@Override
public void onError(Throwable throwable) {
ctx.complete();
throw new RuntimeException(throwable);
}
});
}
private String filename(String contentTxt) throws UnsupportedEncodingException {
Pattern pattern = Pattern.compile("filename=\"(.*)\"");
Matcher matcher = pattern.matcher(contentTxt);
matcher.find();
return matcher.group(1);
}
private static class Range {
final int start;
final int end;
public Range(int start, int end) {
this.start = start;
this.end = end;
}
}
private Range fileRange(String content, String contentType) {
Pattern pattern = Pattern.compile("filename=\".*\"\\r\\n.*\\r\\n\\r\\n(.*+)");
Matcher matcher = pattern.matcher(content);
matcher.find();
int start = matcher.start(1);
String boundary = contentType.substring(
contentType.lastIndexOf("=") + 1, contentType.length());
int end = content.indexOf(boundary, start) - 4;
return new Range(start, end);
}
private void write(byte[] content, int start, int end, String file)
throws IOException {
try(FileOutputStream fileOutputStream = new FileOutputStream(file)) {
fileOutputStream.write(content, start, (end - start));
}
}
}
在這個例子當中,每次有資料可以讀取時,會呼叫 onDataAvailable()
,在 ServletInputStream
準備好可讀取時,將讀取的資料放到 ByteArrayOutputStream
,而全部資料都讀取完成之後,於 onAllDataRead()
進行檔案寫出的動作,基於簡化範例,檔案寫出的動作就還是先用阻斷式的 API。