今回は、以前実装していたSpring BootのWEB画面上でファイルダウンロード処理に、DB更新処理を追加してみたので、そのサンプルプログラムを共有する。
ファイルダウンロードを行う際、最後にInputStreamオブジェクト, OutputStreamオブジェクトをそれぞれcloseする必要があるが、エラー発生時でも確実にcloseできるようにするには、finally句でこれを実行する必要がある。
例えばプログラム上で、DB更新⇒ファイルダウンロードの順に記載している場合、DB更新処理・ファイルダウンロード処理がほぼ同じタイミングで実行されるため、finally句でclose処理を実行しないと、DB更新処理に失敗した場合にclose処理が実行されなくなってしまう。
今回は、下記「前提条件」に記載した記事に、finally句でclose処理を実行する部分と、ファイルダウンロード前のチェック処理の追加を行ったので、その内容について共有する。
前提条件
下記記事の実装が完了していること。
完成イメージ
ここでは、完成した画面イメージの共有を行う。
Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスした場合の初期表示は以下の通りで、「PDFプレビュー」ボタンを押下すると、以下のように、別画面にPDFプレビュー画面が表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
1 | select * from download_history |
なお、ダウンロード履歴テーブルは、本記事で作成するものとする。
また、「ダウンロード」ボタンを押下すると、以下のように、画面下にファイル操作のダイアログが表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
1 | select * from download_history |
さらに、一覧に表示されるものの、ファイルダウンロードデータが無い場合は、以下のように、ダウンロードエラー画面が表示される。
なお、上記の場合は、ダウンロード履歴テーブルへのレコード追加はされない。
やってみたこと
ファイル履歴テーブルとそのシーケンスの作成
今回は、ファイル履歴テーブルとそのシーケンスを追加した。実行したSQLは以下の通り。
1 2 3 4 5 | create table download_history ( history_id number(10) primary key not null, file_data_id number(6) not null, download_date timestamp not null ); |
1 | create sequence download_sequence INCREMENT BY 1 START WITH 1 MAXVALUE 9999999999 |
実行後の確認結果は以下の通り。テーブルの確認はdescコマンドで、シーケンスの確認はuser_sequencesテーブルで、それぞれ確認している。
1 | desc DOWNLOAD_HISTORY |
1 | select * from user_sequences where sequence_name = 'DOWNLOAD_SEQUENCE' |
サンプルプログラムの作成
今回作成したサンプルプログラムの構成は以下の通り。なお、下図の赤枠は、前提条件に記載した記事と変更になったソースコードを示しており、今後記載する。
ダウンロード履歴テーブルにアクセスするためのエンティティクラスは以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package com.example.demo; import lombok.Data; import java.io.Serializable; import java.sql.Timestamp; /** * ダウンロード履歴テーブル(download_history)アクセス用エンティティ */ @Data public class DownloadHistory implements Serializable { /** シリアルバージョンID */ private static final long serialVersionUID = 1L; /** ダウンロード履歴ID */ private long historyId; /** ファイルデータID */ private long fileDataId; /** ダウンロード日付 */ private Timestamp downloadDate; } |
また、ダウンロード履歴テーブルにアクセスするためのMapperインタフェースは以下の通り。今回はシーケンスの採番も行っているので、そのselect文も含めている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package com.example.demo; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface DownloadHistoryMapper { /** * ダウンロード履歴のシーケンス番号を取得する * @return ダウンロード履歴のシーケンス番号 */ @Select("select DOWNLOAD_SEQUENCE.nextval as nextval from dual") long gen_history_sequence(); /** * 指定したダウンロード履歴テーブル(download_history)のデータを追加する * @param downloadHistory ダウンロード履歴(download_history) */ @Insert("INSERT INTO download_history ( history_id, file_data_id, download_date ) " + " VALUES ( #{historyId}, #{fileDataId}, #{downloadDate} )") void insert(DownloadHistory downloadHistory); } |
さらに、コントローラクラスは以下の通り。ここでは、downloadメソッド内で、エラーチェック処理とダウンロード履歴テーブルへの登録処理を追加している。また、InputStreamオブジェクト, OutputStreamオブジェクトのclose処理はfinally句で実装している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLEncoder; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.List; @Controller public class DemoController { /** * ファイルデータテーブル(file_data)へアクセスするMapper */ @Autowired private FileDataMapper fileDataMapper; /** * ダウンロード履歴テーブル(download_history)へアクセスするMapper */ @Autowired private DownloadHistoryMapper downloadHistoryMapper; /** * ファイルデータ一覧表示処理 * @param model Modelオブジェクト * @return 一覧画面 */ @RequestMapping("/") public String index(Model model){ //ファイルデータテーブル(file_data)を全件取得 List<FileData> list = fileDataMapper.findAll(); model.addAttribute("fileDataList", list); //一覧画面へ移動 return "list"; } /** * ファイルデータ登録画面への遷移処理 * @return ファイルデータ登録画面 */ @PostMapping("/to_add") public String to_add(){ return "add"; } /** * ファイルデータ登録処理 * @param uploadFile アップロードファイル * @return ファイルデータ一覧表示処理 */ @PostMapping("/add") @Transactional(readOnly = false) public String add(@RequestParam("upload_file") MultipartFile uploadFile){ //最大値IDを取得 long maxId = fileDataMapper.getMaxId(); //追加するデータを作成 FileData fileData = new FileData(); fileData.setId(maxId + 1); fileData.setFilePath(uploadFile.getOriginalFilename()); try{ fileData.setFileObj(uploadFile.getInputStream()); }catch(Exception e){ System.err.println(e); } //1件追加 fileDataMapper.insert(fileData); //一覧画面へ遷移 return "redirect:/to_index"; } /** * 追加完了後に一覧画面に戻る * @param model Modelオブジェクト * @return 一覧画面 */ @GetMapping("/to_index") public String toIndex(Model model){ return index(model); } /** * ファイルダウンロード処理 * @param id ID * @param response HttpServletResponse * @return 画面遷移先 */ @Transactional(readOnly = false) @RequestMapping("/download") public String download(@RequestParam("id") String id , HttpServletResponse response){ //ダウンロード対象のファイルデータを取得 FileData data = fileDataMapper.findById(Long.parseLong(id)); //ダウンロード対象のファイルデータがnullの場合はエラー画面に遷移 if(data == null || data.getFileObj() == null){ return "download_error"; } //PDFの場合 if(data.getFilePath().endsWith(".pdf")){ //PDFプレビューの設定を実施 response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "inline;"); }else{ //ファイルダウンロードの設定を実施 response.setContentType("application/octet-stream"); //ファイルの種類は指定しない response.setHeader("Content-Disposition" ,"attachment;filename=\"" + getFileName(data.getFilePath()) + "\""); } //その他の設定を実施 response.setHeader("Cache-Control", "private"); response.setHeader("Pragma", ""); OutputStream out = null; InputStream in = null; try{ //ダウンロード履歴への書き込み DownloadHistory downloadHistory = new DownloadHistory(); downloadHistory.setHistoryId(downloadHistoryMapper.gen_history_sequence()); downloadHistory.setFileDataId(data.getId()); downloadHistory.setDownloadDate(Timestamp.valueOf(LocalDateTime.now())); downloadHistoryMapper.insert(downloadHistory); //ダウンロードファイルへ出力 out = response.getOutputStream(); in = data.getFileObj(); byte[] buff = new byte[1024]; int len = 0; while ((len = in.read(buff, 0, buff.length)) != -1) { out.write(buff, 0, len); } out.flush(); }catch (Exception e){ System.err.println(e); }finally { if(out != null){ try{ out.close(); }catch (IOException e){ System.err.println(e); } } if(in != null){ try{ in.close(); }catch (IOException e){ System.err.println(e); } } } //画面遷移先はnullを指定 return null; } /** * ファイルパスからファイル名を取得する * @param filePath ファイルパス * @return ファイル名 */ private String getFileName(String filePath){ String fileName = ""; if(filePath != null && !"".equals(filePath)){ try{ //ファイル名をUTF-8でエンコードして指定 fileName = URLEncoder.encode(new File(filePath).getName(), "UTF-8"); }catch(Exception e){ System.err.println(e); return ""; } } return fileName; } } |
さらに、ダウンロードエラー時は、ダウンロードエラー画面に遷移する。ダウンロードエラー画面の実装は以下の通り。
1 2 3 4 5 6 7 8 9 10 | <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ダウンロードエラー画面</title> </head> <body> <p><font color="red">ダウンロードエラーが発生しました。</font></p> </body> </html> |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-download-updatedb/demo
要点まとめ
- ファイルダウンロード時の、InputStreamオブジェクト, OutputStreamオブジェクトのclose処理は、例外発生の可能性がある場合は、finally句で実装する必要がある。