15

Web アプリケーションから大量のファイルをダウンロードする必要があります。

明らかに長時間実行されるアクションであることが予想されます (年に 1 回使用されます [-per-customer] )。そのため、時間は問題ではありません (タイムアウトに達しない限り、何らかの形のキープアライブ ハートビートを作成します)。非表示のiframeを作成し、それを使用しcontent-disposition: attachmentてブラウザー内で開く代わりにファイルのダウンロードを試みる方法と、進行状況メーターを描画するためのクライアント サーバー通信をインスタンス化する方法を知っています。

ダウンロードの実際のサイズ (およびファイル数) は不明ですが、簡単にするために、10MB ごとに 100 個のファイルで構成される 1GB と仮想的に考えることができます。

これはワンクリック操作であるべきなので、私が最初に考えたのは、すべてのファイルをデータベースから読み取りながら動的に生成された ZIP にグループ化し、ユーザーに ZIP を保存するように求めることでした。

問題は、WebApp で複数の小さなバイト配列から巨大なアーカイブを作成する際のベスト プラクティスとは何か、既知の欠点とトラップとは何かということです。

それはランダムに次のように分割できます。

  • 各バイト配列を物理的な一時ファイルに変換する必要がありますか、またはメモリ内の ZIP に追加できますか?
  • はいの場合、可能性のある名前の同等性を処理する必要があることはわかっています(データベース内の異なるレコードで同じ名前を持つことはできますが、同じファイルシステムやZIP内ではできません):他に考えられる問題はありますか(ファイルシステムに常に十分な物理スペースがあると仮定して)?
  • メモリ内で操作全体を実行するのに十分な RAM があるとは限らないため、ユーザーに送信する前に ZIP を作成してファイル システムにフィードする必要があると思います。ファイルを保存する場所をユーザーに尋ねてから、サーバーからクライアントへの一定のデータフローを開始するなど、別の方法で (たとえば、websocketを使用して) はありますか ( Sci-Fiだと思います)?
  • その他の関連する既知の問題や、頭をよぎるベスト プラクティスを教えていただければ幸いです。
4

3 に答える 3

15

各 BLOB をデータベースからクライアントのファイル システムに直接ストリーミングすることによって作成された、完全に動的な ZIP ファイルのキックオフ例。

次のパフォーマンスを持つ巨大なアーカイブでテスト済み:

  • サーバーのディスク容量のコスト: 0 メガバイト
  • サーバーのRAMコスト: ~ xx メガバイト。Runtime.getRuntime().freeMemory()ループの前、最中、後に同じルーチンを複数回 ( を使用して) 実行すると、異なる明らかにランダムな結果が得られたため、メモリ消費はテストできません (または、少なくとも適切に行う方法がわかりません)。ただし、byte[]を使用するよりもメモリ消費量が少なく、十分です。


FileStreamDto.javaInputStreamの代わりに使用byte[]

public class FileStreamDto implements Serializable {
    @Getter @Setter private String filename;
    @Getter @Setter private InputStream inputStream; 
}


Java サーブレット(または Struts2 アクション)

/* Read the amount of data to be streamed from Database to File System,
   summing the size of all Oracle's BLOB, PostgreSQL's ABYTE etc: 
   SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions
*/          
Long overallSize = getMyService().precalculateZipSize();

// Tell the browser is a ZIP
response.setContentType("application/zip"); 
// Tell the browser the filename, and that it needs to be downloaded instead of opened
response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\"");        
// Tell the browser the overall size, so it can show a realistic progressbar
response.setHeader("Content-Length", String.valueOf(overallSize));      

ServletOutputStream sos = response.getOutputStream();       
ZipOutputStream zos = new ZipOutputStream(sos);

// Set-up a list of filenames to prevent duplicate entries
HashSet<String> entries = new HashSet<String>();

/* Read all the ID from the interested records in the database, 
   to query them later for the streams: 
   SELECT my_id FROM my_table WHERE my_conditions */           
List<Long> allId = getMyService().loadAllId();

for (Long currentId : allId){
    /* Load the record relative to the current ID:         
       SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId            
       Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */
    FileStreamDto fileStream = getMyService().loadFileStream(currentId);

    // Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream
    ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename()));
    zos.putNextEntry(zipEntry);

    // Use Apache Commons to transfer the InputStream from the DB to the OutputStream
    // on the File System; at this moment, your file is ALREADY being downloaded and growing
    IOUtils.copy(fileStream.getInputStream(), zos);

    zos.flush();
    zos.closeEntry();

    fileStream.getInputStream().close();                    
}

zos.close();
sos.close();    


重複エントリを処理するためのヘルパー メソッド

private String getUniqueFileName(HashSet<String> entries, String completeFileName){                         
    if (entries.contains(completeFileName)){                                                
        int extPos = completeFileName.lastIndexOf('.');
        String extension = extPos>0 ? completeFileName.substring(extPos) : "";          
        String partialFileName = extension.length()==0 ? completeFileName : completeFileName.substring(0,extPos);
        int x=1;
        while (entries.contains(completeFileName = partialFileName + "(" + x + ")" + extension))
            x++;
    } 
    entries.add(completeFileName);
    return completeFileName;
}



ダイレクト ストリーミングのアイデアをくれた@prunge に感謝します。

于 2013-05-20T16:49:43.370 に答える
9

一度にメモリに収まらない大きなコンテンツの場合は、コンテンツをデータベースから応答にストリーミングします。

この種のことは、実際には非常に単純です。AJAX や websockets は必要ありません。ユーザーがクリックする簡単なリンクを介して、大きなファイルのダウンロードをストリーミングできます。また、最新のブラウザーには、独自の進行状況バーを備えた適切なダウンロード マネージャーがあります。

このためにゼロからサーブレットを作成する場合は、データベース BLOB にアクセスして入力ストリームを取得し、コンテンツを HTTP 応答出力ストリームにコピーします。Apache Commons IO ライブラリがある場合は、IOUtils.copy()を使用できます。それ以外の場合は、自分で行うことができます。

その場で ZIP ファイルを作成するには、ZipOutputStreamを使用します。これらのいずれかを応答出力ストリーム (サーブレットまたはフレームワークが提供するものから) に対して作成し、データベースから各 BLOB を取得します。putNextEntry()最初に使用してから、前述のように各 BLOB をストリーミングします。

潜在的な落とし穴/問題:

  • ダウンロード サイズとネットワーク速度によっては、リクエストが完了するまでに時間がかかる場合があります。ファイアウォールなどがこれを妨害し、リクエストを早期に終了させる可能性があります。
  • これらのファイルを要求するとき、ユーザーが適切な企業ネットワークに接続していることを願っています。リモート/ドッジ/モバイル接続ではさらに悪化します (2.0G の 1.9G をダウンロードした後にドロップアウトした場合、ユーザーは最初からやり直す必要があります)。
  • 特に巨大な ZIP ファイルを圧縮すると、サーバーに多少の負荷がかかる可能性があります。これが問題になる場合は、作成時に圧縮をオフ/オフにする価値があるかもしれませんZipOutputStream
  • 2GB (または 4 GB) を超える ZIP ファイルは、一部の ZIP プログラムで問題が発生する可能性があります。最新の Java 7 は ZIP64 拡張機能を使用していると思うので、このバージョンの Java は巨大な ZIP ファイルを正しく書き込みますが、クライアントには大きな zip ファイルをサポートするプログラムがありますか? 特に古いSolarisサーバーでは、以前にこれらの問題に確実に遭遇しました
于 2013-05-17T04:06:17.963 に答える
2

複数のダウンロードを同時に試したい場合があります。ここでこれに関連する議論を見つけました-Javaマルチスレッドファイルのダウンロードパフォーマンス

お役に立てれば。

于 2013-05-17T02:54:20.313 に答える