プログラミング: 2026年8月アーカイブ

Spring Boot 4 で作成したファイルダウンロード画面。「ダウンロード」ボタン押下で DB を読んで CSV ファイルを生成し、そのファイルがクライアントPCにダウンロードされる。

Controller はこんな感じ。

    // ボタン押下で実行される処理
    @PostMapping("/createFile")
    public ResponseEntity<Resource> downloadFile() throws Exception {
        Path path    = HogeService.make_file_path(); // ダウンロードされるファイルを作成するパス
        HogeService.logic_make(path.toString());     // ダウンロードファイルを作成

        Resource resource    = new FileSystemResource(path);

        HttpHeaders headers    = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"");
        headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);

        return ResponseEntity.ok()
                .contentType(getContentType(path))
                .contentLength(resource.contentLength())
                .headers(headers)
                .body(resource);
    }

    // ContentType の取得
    private MediaType getContentType(Path path) throws IOException {
        try {
            return MediaType.parseMediaType(Files.probeContentType(path));
        }
        catch (IOException e) {
            return MediaType.APPLICATION_OCTET_STREAM;
        }
    }

これで、例えば

    <form action="/createFile" method="post" id="form" name="form">
        <input type="submit" name="download-btn" class="btn-primary" id="download-btn" value="ダウンロード">

みたいな形にすれば、「ダウンロード」ボタン押下でブラウザのダウンロード窓が開いてダウンロードは行われる。

しかし、例えばダウンロードに失敗したときは Web 画面上にエラーメッセージを表示したいとか、成功したら画面に「正常にダウンロードできました」などと表示したい場合はこれではダメだ。

Controller の各メソッドで返却できる値は 1種類である。
上の例のようにデータ(ダウンロードファイル)を出力するか(ダウンロード)、HTML を出力するか(画面表示)、生のテキストや JSON データなどを出力するか(API など)・・・のいずれかひとつのことしかできない。
「先にファイルを出力して、その次に結果を伝える HTML を出力する」という二段構えの出力はできない。ファイルを出力すれば画面(HTML)は表示できないし、画面(HTML)を表示(出力)すればファイルは出力できない。

・・・が、ファイルダウンロードをして結果を Web 画面に表示したい要件ってあるよねえ。

そういうときは、直接ファイルダウンロードの画面を呼び出すのではなく、JavaScript の中でファイルダウンロード画面を呼び出し一旦ファイルを受信しするようにする。そして、そのデータをクライアントに出力し、その後画面の編集をすればいい。JavaScript で処理すれば「ファイル出力か HTML 画面表示かどちらかしかできない」なんて制限ないからね。

具体的には、↓こんな感じの JavaScript。

    document.getElementById('download-btn').addEventListener('click', async (e) => {

        e.preventDefault();

        const btn    = document.getElementById('download-btn');
        const errMesg = document.getElementById('error_msg');
        const sucMesg = document.getElementById('success_msg');

        errMesg.style.display = 'none';  // メッセージ非表示
        sucMesg.style.display = 'none';  // メッセージ非表示

        try {
            btn.disabled = true;  // ボタンが押されたら、二度押し防止のため非活性化

            // ダウンロード処理呼び出し
            const action_url    = /*[[@{/createFile}]]*/ '';
            const response = await fetch(action_url, {  // fetch(非同期通信(HTTPリクエスト))でダウンロードデータ取得
                method: 'POST'
            });

            if (!response.ok) {  // ダウンロード画面からデータが取れなければ例外発生
                throw new Error()
            }

            const blob = await response.blob();  // データをBlob(バイナリの塊)として取得

            const disposition  = response.headers.get('Content-Disposition') ;  // レスポンスヘッダーからファイル名取得
            let filename = 'download.csv';  // ファイル名が取れなかったときのディフォルト値

            if (disposition && disposition.indexOf('attachment'!== -1)) {
                const fileMatch        = /filename="?([^; \n"]*)"?/.exec(disposition);  // 正規表現でファイル名取得
                if (fileMatch && fileMatch[1]) {
                    filename    = fileMatch[1];
                }
            }

            const url  = window.URL.createObjectURL(blob);  // ダウンロード用の疑似リンク作成
            const a    = document.createElement('a');     // 疑似アンカータグ作成

            a.href     = url;  // アンカータグの href 属性に疑似URLを紐づける
            a.download = filename;  // アンカータグの download 属性にファイル名をセット
            document.body.appendChild(a);  // 作成した疑似アンカータグを一時的に(操作のため)実体化
            a.click();  // 実体化したアンカータグを画面の前の人間に代わってクリック(ダウンロードスタート)

            a.remove();  // もう必要ないのでアンカータグ削除
            window.URL.revokeObjectURL(url);  // メモリに残った疑似リンク(blobデータ含む)を削除

            btn.disabled = false;  // ボタン使用可能に
            sucMesg.textContent = "正常にダウンロードが終了しました。";
            sucMesg.style.display = 'block';  // メッセージをセットして表示

        }
        catch(error) {
            btn.disabled    = false;  // ボタン使用可能に
            errMesg.textContent = "ダウンロードに失敗しました。";
            errMesg.style.display = 'block';  // メッセージをセットして表示
        }
        finally {

        }
    });

ダウンロードボタンが押されたらイベントリスナーによりこの処理が実行される。
コメントに書いているとおり、JavaScript 内で(画面の前の人間には見えないところで)実際のダウンロード画面にアクセスしデータを取得。そのデータをダウンロードするための疑似リンクを作って、そのリンクに(画面の前の人に代わって)アクセスして画面の前の人のパソコンにデータがダウンロードされるという動き。

なんでこういうことをするかというと、JavaScript 内でダウンロードの成否を判断して処理を行うため。

ダウンロードに失敗したときは例外を発生させ「失敗」メッセージを表示。最後まで問題なく処理が終われば「正常」メッセージを表示・・・となる。(直接ブラウザでダウンロードをした場合は、成否の情報は画面(JavaScript)には伝わってこないので何もできない。)

Controller のファイルダウンロードのメソッド(上の例では downloadFile メソッド)を修正する必要はない。

このアーカイブについて

このページには、2026年8月以降に書かれたブログ記事のうちプログラミングカテゴリに属しているものが含まれています。

前のアーカイブはプログラミング: 2026年7月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

月別 アーカイブ

電気ウナギ的○○ mobile ver.

携帯版「電気ウナギ的○○」はこちら