kagamihogeの日記

kagamihogeの日記です。

Commons FileUpload 1.0 にやられた

今時 Struts 1.1 とか Commons FileUpload 1.0 とかビミョウな代物使ってる人は少なそうそうだけど…ちょっとはまったのでそれについてメモ。実際、知ってる人には有名な挙動(バグ?)なんだそうだけど。

どんな動作かというと、マルチパートアクセスのリクエストを投げるときに大量のフォームパラメータを伴っていると OutOfMemoryError が出る、というもの。一回に送るパラメータ数が結構多い画面があるし、同時に何ユーザかがアクセスすると確実に落ちるのでこりゃあマズイってことになった。

解消するのはカンタンで Commons FileUpload 1.1 以降を使えばいい。ついでに commons IO も必要なので一緒に入れればおk。

しかしまぁそれだけじゃなんとなくモヤモヤするのでちょっとソース追ってみた。

Struts 1.1 の、マルチパートのリクエストを受け取ってフォームの各フィールドに対応する ActionForm プロパティに関連付ける処理は CommonsMultipartRequestHandler#handleRequest あたりが核になっているらしい。

    public void handleRequest(HttpServletRequest request)
            throws ServletException {
        // Get the app config for the current request.
        ModuleConfig ac = (ModuleConfig) request.getAttribute(
                Globals.MODULE_KEY);

        // Create and configure a DIskFileUpload instance.
        DiskFileUpload upload = new DiskFileUpload();
        ...
        // Set the maximum size that will be stored in memory.
        upload.setSizeThreshold)((int) getSizeThreshold(ac))(;
        ...
        // Parse the request into file items.
        List items = null;
        try {
            items = upload.parseRequest(request);
        ...

DiskFileUpload というクラスに request を解析させることでアップロードファイルとフォームの各フィールドの詰まったリストが得られるらしい。んで、DiskFileUpload のインスタンスに対して setSizeThreshold でなんらかのバッファの大きさをしている。この値は Struts の設定情報が詰まっている ModuleConfig から取得しており、ControllerConfig の↓がデフォルト値のようだ。

    protected String memFileSize = "256K";

次は、DiskFileUpload#parseRequest(request) で、このメソッドは親クラスの FileUploadBase にある。

    public List /* FileItem */ parseRequest(HttpServletRequest req)
        throws FileUploadException
        ...
            while (nextPart)
            {
            ...
                            // A form field.
                            FileItem item = createItem(headers, true);
                            OutputStream os = item.getOutputStream();

少々長めのメソッドだが、フォームフィールドを処理するコードを示すらしきコメントがある。とりあえず先へ読み進んでみる。

次の、FileItem#getOutputStream() の実装は DefaultFileItem が使用される。

    public OutputStream getOutputStream()
        ....
        dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
        ....

更にもう一つ先のクラス DeferredFileOutputStream を見てみる。

    public DeferredFileOutputStream(int threshold, File outputFile)
        super(threshold);
        ...
        memoryOutputStream = new ByteArrayOutputStream(threshold);
        ...

ByteArrayOutputStream を作るときに threshold というバッファサイズを指定している。コードをちょっと端折ってるんだけど、ここには 262144 ≒ 256K が入る。

つまり、フォームフィールド一つを解析するために 256K のバッファを取るコードになってるみたい。そのため、フォームフィールドがたくさんあると物凄い勢いでメモリを消費していって・・・結果、メモリが溢れるというのが事の真相みたい。

じゃあ Commons FileUpload 1.1 以降や 2.x はどうなっているかっていうと…同じようにコードを追ってみる。順繰りに見ていくと Commons FileUpload が使用する Commons IO の DeferredFileOutputStream にいきついて…

    public DeferredFileOutputStream(int threshold, File outputFile)
        ...
        memoryOutputStream = new ByteArrayOutputStream();
        ...

バッファサイズの指定がなくなっている。この場合、ByteArrayOutputStream のバッファサイズはデフォルトの 32 バイトになる。なるほど。


いざ、って時に中のソースが追えるってのはやっぱり面白いなぁ・・・。