今回は、以前実装していたSpring BootのWEB画面上でファイルダウンロード処理に、DB更新処理を追加してみたので、そのサンプルプログラムを共有する。
ファイルダウンロードを行う際、最後にInputStreamオブジェクト, OutputStreamオブジェクトをそれぞれcloseする必要があるが、エラー発生時でも確実にcloseできるようにするには、finally句でこれを実行する必要がある。
例えばプログラム上で、DB更新⇒ファイルダウンロードの順に記載している場合、DB更新処理・ファイルダウンロード処理がほぼ同じタイミングで実行されるため、finally句でclose処理を実行しないと、DB更新処理に失敗した場合にclose処理が実行されなくなってしまう。
今回は、下記「前提条件」に記載した記事に、finally句でclose処理を実行する部分と、ファイルダウンロード前のチェック処理の追加を行ったので、その内容について共有する。
前提条件
下記記事の実装が完了していること。
完成イメージ
ここでは、完成した画面イメージの共有を行う。
Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスした場合の初期表示は以下の通りで、「PDFプレビュー」ボタンを押下すると、以下のように、別画面にPDFプレビュー画面が表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
select * from download_history
なお、ダウンロード履歴テーブルは、本記事で作成するものとする。
また、「ダウンロード」ボタンを押下すると、以下のように、画面下にファイル操作のダイアログが表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
select * from download_history
さらに、一覧に表示されるものの、ファイルダウンロードデータが無い場合は、以下のように、ダウンロードエラー画面が表示される。
なお、上記の場合は、ダウンロード履歴テーブルへのレコード追加はされない。
やってみたこと
ファイル履歴テーブルとそのシーケンスの作成
今回は、ファイル履歴テーブルとそのシーケンスを追加した。実行したSQLは以下の通り。
create table download_history ( history_id number(10) primary key not null, file_data_id number(6) not null, download_date timestamp not null );
create sequence download_sequence INCREMENT BY 1 START WITH 1 MAXVALUE 9999999999
実行後の確認結果は以下の通り。テーブルの確認はdescコマンドで、シーケンスの確認はuser_sequencesテーブルで、それぞれ確認している。
desc DOWNLOAD_HISTORY
select * from user_sequences where sequence_name = 'DOWNLOAD_SEQUENCE'
サンプルプログラムの作成
今回作成したサンプルプログラムの構成は以下の通り。なお、下図の赤枠は、前提条件に記載した記事と変更になったソースコードを示しており、今後記載する。
ダウンロード履歴テーブルにアクセスするためのエンティティクラスは以下の通り。
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文も含めている。
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句で実装している。
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; } }
さらに、ダウンロードエラー時は、ダウンロードエラー画面に遷移する。ダウンロードエラー画面の実装は以下の通り。
<!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句で実装する必要がある。