今回は、以前実装していた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句で実装する必要がある。





