ReadListener


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