LoginSignup
19
22

More than 5 years have passed since last update.

Java Servlet の ファイルアップロード処理でストリーミングを使う方法

Posted at

Java Servlet でファイルアップロード処理を実装する場合、
(というか multipart の受信処理を実装する場合、と書いたほうが適切かも)
Apache Commons の ServletFileUpload クラスを使用するのが定石ですが、
大容量ファイルのアップロードで parseRequest メソッドを使うと
parseRequest がなかなか復帰しません。

例えば、以下の様なコードの場合、


import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

protected void parse(HttpServletRequest request) throws Exception {
    DiskFileItemFactory factory = new DiskFileItemFactory();
    ServletFileUpload upload = new ServletFileUpload(factory);

    factory.setSizeThreshold(1024);
    upload.setSizeMax(-1);
    upload.setHeaderEncoding("UTF-8");

    List list = upload.parseRequest(request);
    Iterator iterator = list.iterator();

    while (iterator.hasNext()) {
        FileItem fileitem = (FileItem)iterator.next();
        if (fileitem.isFormField()) {
            trace("key=" + fileitem.getFieldName());
            trace("value=" +  fileitem.getString());

        } else {
            trace("key=" + fileitem.getFieldName());

            InputStream is = fileitem.getInputStream();
            // InputStream を使ってメモリに展開するなり、
            // ファイルに書き込むなり・・・
            int len = 0;
            byte[] buffer = new byte[1024];

            FileOutputStream fos = new FileOutputStream("foo.dat");

            try {
                while((len = is.read(buffer)) > 0) {
                    fos.write(buffer, 0, len);
                }
            } finally {
                fos.close();
            }
        }
    }
}

一見良さそうですが、実は parseRequest メソッドは全てのデータ受信が終わるまで復帰しません。
ということは、例えば、大容量のデータをファイルに書き込む場合は、
全てのデータ受信が終わってから、まとめてファイルに書き込むことになります。

数キロバイト程度のデータであれば別にこれでも良いですが、数100MBからGB単位になると
ファイル書き込みに時間がかかり、ちょっと困ります。

実は、全てのデータ受信が終わってからパースするのではなく、逐次処理する方法が存在します。

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;

protected void parse(HttpServletRequest request) throws Exception {
    DiskFileItemFactory factory = new DiskFileItemFactory();
    ServletFileUpload upload = new ServletFileUpload(factory);

    factory.setSizeThreshold(1024);
    upload.setSizeMax(-1);
    upload.setHeaderEncoding("UTF-8");

    FileItemIterator iter = upload.getItemIterator(request);
    while (iter.hasNext()) {
        FileItemStream item = iter.next();

        if (item.isFormField()) {
            InputStream is = item.openStream();

            trace("key=" + item.getFieldName());
            trace("value=" +  Streams.asString(is));
        } else {
            trace("key=" + item.getFieldName());
            InputStream is = item.openStream();

            // InputStream を使ってメモリに展開するなり、
            // ファイルに書き込むなり・・・
            int len = 0;
            byte[] buffer = new byte[1024];

            FileOutputStream fos = new FileOutputStream("foo.dat");

            try {
                while((len = is.read(buffer)) > 0) {
                    fos.write(buffer, 0, len);
                }
            } finally {
                fos.close();
            }
        }
    }
}

parseRequest メソッドの代わりに getItemIterator メソッドを使います。
Streaming と呼んでいるようです

getItemIterator メソッドは即復帰し、その後は getItemIterator メソッドで返された
イテレータで逐次処理することができます。

イテレータの next メソッドでは FileItemStream クラスのインスタンスが返ってきます。
FileItemStreamopenStream メソッドでストリームをオープンした後は、
ファイルに書くなりなんらかの解析をさせるなりといったことが可能です。

この方法だと、データ受信と並行して処理をさせることができるので、待ち時間の短縮になって良いです。
(あと試してないですがおそらくヒープメモリの節約になりそうな気がします。
parseRequest は全データを保持する必要があるので・・・)

19
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
22