Strutsでのダウンロード(サンプル)

Strutsでのアップロード処理についてサンプルを書いたので、ついでにダウンロードについてもサンプルを掲載します。

アップロード処理と違ってStrutsの機能はなにも用意されていません。一般的なWebアプリケーションのダウンロードと一緒です。

ポイントは3つです。

  • HTTPヘッダの出力
  • レスポンスストリームに直接出力する。
  • forwardしない


ダウンロード処理は、画面遷移がありません
画面遷移とは「表示するページを出力する処理」になりますが、これがない代わりに、ダウンロードファイルの内容を出力します。

ダウンロード画面はこんなイメージです。
画面イメージ
入力値は、ダウンロード処理に関係ないのですが、ここでは出力されるCSVファイルの行数を指定しています。
行数というのは、入力値を受け取る例として用意しただけで、ダウンロード処理そのものには関係ありません。

ダウンロードボタンを押下すると、保存ダイアログが表示されます。
保存ダイアログ
この保存ダイアログは、ブラウザの機能によって表示しているため、ここのデザインを変えたいとしても変えることはできません。

Action

ブラウザに対してHTMLを出力するのではなく、「ダウンロードするためのデータを出力するよ」と指定することで、ブラウザは保存ダイアログを表示してくれます。これが「HTTPヘッダの出力」です。

//HTTPヘッダの出力
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition",
"attachment; filename=\"" + filename + "\"");

ContentTypeとして、application/octet-streamを出力すると、保存ダイアログが表示されるようになります。次のヘッダは、保存ダイアログに表示されるファイル名を指定しています。

保存ダイアログのファイル名は、日本語を表示するために、次のようにしています。
String filename = new String(
"ダウンロードサンプル.csv".getBytes("Windows-31J"), "ISO-8859-1");

これで全てのブラウザで問題ないかどうかは確認していません。(少なくともWindows上の、IE6.0やFirefox2.0では正しく日本語のファイル名でダウンロードできました。)

レスポンスストリームには、ファイルの中身を出力することになります。

PrintWriter out = new PrintWriter(
new OutputStreamWriter(response.getOutputStream(), "Windows-31J"));
for (int i = 0; i < count; i++){ out.print("表示サンプルデータ" + i + "-A"); out.print(","); out.print("表示サンプルデータ" + i + "-B"); out.print(","); out.print("表示サンプルデータ" + i + "-C"); out.print(","); out.println(); }

ここでは、CSV形式のテキストデータを出力するため、PrintWriterを使ってファイル内容を出力しています。
PrintWriterオブジェクトは、元をたどると、HTTPServletReponse(ブラウザへの出力ストリーム)から作成しています。
データの作成方法は、DBからの検索結果で作成したりと、色々なやり方があると思います。

forwardしない」というのはStrutsのAction処理の話です。
Actionのメソッドの戻り値は、ActionForwardを返す仕様になっていますが、ダウンロード処理の場合は画面遷移がないため、forwardしてはいけません。ここはnullを返すようにします。
return null;

サンプルのダウンロード


Strutsでのダウンロード(サンプル)」への19件のフィードバック

  1. 初めまして、Java及びStrutsの初心者 kazmiと申します。
    StrutsでCSVやPDFをダウンロードする方法を探していて
    こちらにたどり着き、大変参考にさせて頂きました。ありがとうございます。

    1点、お知恵をお借りできればと思いまして、、
    ご記載のサンプル内で、for 文でout.print()を繰り返している箇所がございますが、
    この辺りで例外が発生した場合の処理を検討しています。
    (DBから取得したデータを、サンプルのようにfor文で回したいと考えています)

    ダウンロードの途中でSQLException等の例外が発生した場合には
    エラー画面を表示させたいのですが、無理でしょうか?

  2. >ダウンロードの途中でSQLException等の例外が発生した場合には
    >エラー画面を表示させたいのですが、無理でしょうか?

    難しいと思います。
    というのは、ダウンロードデータの出力中はもうブラウザにデータを返却し始めているため、
    後からエラー画面のHTMLを返し始めたとしても、もう手遅れになっているからです。
    すでに送ってしまったデータを取り返すことはできないためです。

    一応、バッファリングは可能なので、バッファが満杯になるまでは取り消して、エラー画面に遷移させることができますが、それでは不完全かと思います。
    回避案としては、ダウンロードデータをあらかじめ作成し(例えばファイル)、そのデータをダウンロードさせるとか、作成したファイルにリンクさせるとかが考えられます。
    この場合、ダウンロード中にエラーが発生することが少なくなりますが、デメリットとしてそのダウンロード用の中間ファイルをサーバ側でどう後始末するか検討が必要です。

  3. やっぱり難しいですか。。
    記載して頂いた回避案も参考に、検討してみたいと思います。
    ありがとうございました。

  4. わかりやすいサンプルありがとうございます。
    今私が携わっている開発で、Java/Strutsで、Webサーバーから、他の会社のサーバー(Unix)の共有ディスクを見に行き、そこに格納してあるPDFファイルをダウンロードしたいのですが、
    (1)クライアント側では、まずjspなどでファイル名の選択画面をチェックボックスで選べる画面をつくり、
    (2)そして、その画面でSubmitをすると、Webサーバーに行き、そこで、
    (3)FormBean -> ActionFormと来たときに、こちらのサンプルのような内容を創ろうとおもっています。
    ただ、こちらのサンプルでは、テキストデータをPrintWriterで行数を制限して出力していますが、PDFファイル1ファイルをダウンロードする場合はこの出力部分をどのように変更したらよいでしょうか・・・

    ご教授、よろしくお願いいたします。

  5. PDFの場合は、バイナリデータと考えた方がよいので、
    PrintWriterではなく、OutputStreamにバイトデータとして出力すればよいと思います。
    テキストデータ:Reader/Writer
    バイナリデータ:InputStream/OutputStream
    という使い分けになります。保存されているPDFをInputStreamで徐々に読み込みながら、response.getOutputStream()で取得した、OutputStreamに徐々に出力するということになると思います。

  6. やっと、欲しかった情報にたどり着きました。
    自分が開発している業務アプリでは、例外処理として一律エラー画面にforwardする設計となっています。ところが、ダウンロードダイアログを使用し、かつ、ストリームへ出力中にIOException等が発生した場合は、エラー画面に遷移しない現象となっていたのです。
    先のcivicさんの説明にあるように、例外が発生した場合にエラー画面を表示させるのは難しいとのことでしたが、下記のように、res.reset()を発行するとエラー画面に遷移できるようになりました。

    try {
    out = new BufferedWriter(new OutputStreamWriter(res.getOutputStream()
    , “SJIS”));
    out.write(“\”test\”\t”);
    out.write(“\n”);
    } catch (Exception e) {
    //ファイルダウンロード用のHTTPヘッダをリセットします。
    res.reset();
    throw e;
    } finally {
    if (out != null) {
    // out.close();
    }
    }

    ただ、悲しいことに、コメントアウトしてあるように出力ストリームをcloseしなければの話です。
    out.close()をイキにすると真っ白な画面にデータ”test”が表示され、エラー画面に遷移しなくなります。closeによるシステムリソースの開放は必須と認識していますので、いろいろ試行錯誤していますが、解決には至っていません。res.reset()はイレギュラーな使い方なのでしょうか。
    最初、ダウンロードダイアログを消せばうまくいくんじゃないかと思い

    Robot r;
    r = new Robot();
    r.keyPress(KeyEvent.VK_ESCAPE);
    r.keyRelease(KeyEvent.VK_ESCAPE);

    などとしてみたんですが論外でした(TT)。。。
    何か情報がありましたら、ご教授ください。

  7. 前の方の質問にも書いてあるとおり、一度送ってしまったデータを取り消すのは、バッファの範囲内でしか無理です。resetは確かに取り消すことができるように見えますが、それすらも一定量超えたらreset不可能です。closeするとバッファがフラッシュされるので取り消すことができなくなるのだと思います。
    HTTPの処理というのは、
    1.「これから~データをダウンロードさせるからね」 何を送るかを宣言し、
    2.「データのバイト数は○○バイト送るからね」 送るデータのサイズを通達し、
    3.「じゃあデータ送りまーす。ダラダラ。。。」 実際にデータを送り始める
    という流れになっています。3.以降の処理でIOExcdptionが発生した場合に、エラー画面にforwardしたいというのは、1.2.で宣言したことと違うことをしようとしているのでできないんですよ。
    エラー画面にforwardするという処理にも、エラー画面のHTMLを送るね。エラー画面のデータ量は○○バイトあるよ。のように1.2.3.の処理が必要。
    取り消し(reset)というのは、プログラム的には取り消したように見えても、実はまだ1.2.3.の処理をブラウザに返さずに、サーバが保留しているだけのことで、実際はまだレスポンスしていないから取り消すことができるのです。
    1.2.を実行してしまってからではもう取り消すことができません。closeだとか、大量のデータをダウンロードすると、保留しきれなくなって1.2.を実行してしまうわけです。

    ダウンロードダイアログが出ている状態というのは、もう1.は行われており、それを受け取ったブラウザが、「おっ、ダウンロード処理ならダイアログだすか」と処理を行っている状態なので、もうエラー画面への遷移などの処理はできません。
    Webブラウザを使って、ダウンロードを実装する以上これはどうにもならないことなので、諦めるしかないと思いますよ。Webブラウザのダウンロード機能を使わないとすれば、独自に別のプログラムでダウンロードするとか、そういう方法もあるとは思いますが、本当に必要なことなのかは要検討だと思います。

    実際問題、ダウンロードデータを作成するところでエラーが発生するならともかく、ダウンロード中に発生したエラーでエラーページに遷移させるってかなり変なことですよ。エラーページへのforwardもHTMLのダウンロードですから言ってみれば、「電話が通じなくなったら、通じない旨の連絡を電話で伝えろ」っていうようなもんです。それすら通じないわけです。

    また例えば、ページに使われている画像ファイルが何らかの通信障害で表示できなかったら、ページはエラー画面に遷移するようになってますか?無理ですよね。それと同じなんですよ。
    一律エラーページにforwardする仕様だとしても、できることとできないことはハッキリと理解してもらうことも必要だと思います。

    クライアントのダウンロードダイアログを消すとかは無理です。それができてしまったら完全にセキュリティホールです。ユーザが意図しないで勝手にOKボタンを押すとかできてしまいます。
    Robotなどを使うコードは、クライアントで動くコードであり、それをサーバーサイドで実行してもクライアントには影響しません。

    長々と書きましたが、結構そういう質問が多いので詳しく説明してみました。
    moriさんに対してだけのレスではなく、同じような悩みをもって読んだ方の参考になればよいと思い書きました。

  8. moriです。
    さそくの詳細かつ明確な回答ありがとうございます。
    過去、数回ストリーム出力中にIOExceptionが発生した経緯がありました。その場合、途中でデータが切れたファイルがダウンロードされ、ユーザが気が付かないまま業務が進んでしまうこと自体に問題があるようです。ファイルの中身を確認するなりし、運用でカバーするしかなさそうですね。
    勉強になりました。。。

  9. ダウンロード用のデータを作る処理と、ダウンロードのために本当にストリーム出力する処理を分けてはどうでしょうか?
    データを作る処理の段階では、エラーが発生してもエラー画面への遷移が可能です。ここでダウンロード用の一時ファイルを作成するなどします。
    出力の部分では、もうできあがったファイルを単純に出力するだけなので、エラー発生の可能性は低いと思います。

  10. レス遅れてすみません。
    おおっ!
    試作し、データ作成時ではエラー画面に遷移できることを確認しました。
    別途、サーバ上の一時ファイルを回収する仕組みは作るとして、十分な打開策になると思います。
    本当にありがとうございました。

  11. java初心者です。
    初歩的な質問なのかもしれませんが、教えてください。
    Strutsでファイルのダウンロードを行うプログラムを開発しております。
    ダウンロードファイルを指定するjspファイルで、処理完了後「戻るボタン」を押下し、前の画面へ移るようにしたいのですが、ダウンロード処理完了後「戻るボタン」が効かなくなってしまいます。
    何か方法がございましたら、ご教示頂きたく、宜しくお願いいたします。

  12. ポップアップダイアログを開くとか、フレームに対して遷移することで、
    元の画面自体は戻ることができると思います。

  13. フレームを利用して解決しました。
    大変助かりました。ありがとうございました。

  14. 分かりやすいサンプルで、参考にさせていただきました。
    ありがとうございます!
    早速質問で恐縮なのですが、エクセルファイル(マクロ入り)の場合も、同様の処理でダウンロードできるのでしょうか?
    ファイルが壊れてしまう場合があったのですが・・・
    テスト.xlsをダウンロードする処理です。
    ↓↓↓↓
    servletResponse.setContentType(“application/excel”);
    servletResponse.setHeader(“Content-Disposition”,
    “attachment;filename=\”” + “sample.xls” + “\””);

    out = servletResponse.getOutputStream();
    in = new FileInputStream(“C:\\テスト.xls”);

    int data;
    while ((data = in.read()) != -1) {
    out.write(data);
    out.flush();
    }

  15. 読み込んだデータを返すだけなので、ファイルがなんだろうと関係ないですよ。
    マクロのあるなしで、ダウンロードの正常、破損を確認したのでしょうか。

    壊れるというのはどう壊れてしまうのでしょう。バイナリエディタでのぞいてみて、元ファイルと比較して壊れ方に特徴がないか見てみるとよいと思います。
    実はサーバー側で発生したエラー画面(html)がそのままダウンロードされて、xlsという拡張子で保存されただけだったりしませんか?

  16. 初めまして、rakuと申します。
    現在Strutsでダウンロード販売処理の業務をやっています。
    クライアント側で音楽や、画像などをダウンロードするとき、自動的にクライアント側のダウンロードマネージャー(IE、FireFoxなどのダウンロードマネージャーではなく)を起動することが可能でしょうか?
    ご教示頂きたく、宜しくお願いいたします。

  17. それはブラウザ側の独自の機能なので無理です

  18. 早速のご回答ありがとうございます。
    civic様が書いたサンプルを利用して、PCでほとんどのファイルをダウンロードできましたが、携帯キャリアに対応しないようですね。
    携帯3キャリアの場合、ファイルの拡張子によって、ContentTypeを設定しなければいけないですか?共通のContentTypeはありますか?
    (ダウンロード対象ファイルはテキストと音楽です)
    ご教授宜しくお願いいたします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください