Spring BootとAjax通信でファイルアップロード・ダウンロードを行いたい

# # 初めに
Spring Boot とAjax通信を使用したファイルのアップロード・ダウンロード方法について記載しています。
JavaはMavenとThymeleaf使用して作成しています。
JavaScriptはjQueryを使用した場合と普通のJavaScriptを使用した両方のパターンを記載しています。

# 今回作成するもの
## アップロード
これはアップロード画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/8ceaa453-b1e3-315a-f3de-9b99e4100c81.png)

今回アップロードするファイルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/47294805-4642-29e7-8b62-a8f5b0cd2d28.png)

アップロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/ee4c9bb2-479c-764e-0d44-43766b0df1af.png)

ファイルが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/81531e81-9353-89d7-ff34-8868abc79356.png)

アップロードボタンを押下すると確認ダイアログが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/779345f3-93dc-2f4b-c448-925cab0284c5.png)

はいを押下し成功すると成功メッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/82e088d1-6d44-2e44-a155-9cfc35099859.png)

__\src\main\resources\uploadFile__ にファイルがアップロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/67d208f3-6128-2c03-72ef-280fd66aba7b.png)

## ダウンロード
これはダウンロード画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/dd7e5cfb-8609-3c57-1991-a1698d605d0d.png)

ダウンロードボタンを押下すると確認ダイアログが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/de8572d0-55e4-9f85-f0f2-27e04d990a5e.png)

はいを押下するとファイルがダウンロードされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2663952/4e6e1d2f-501d-fa1f-8a43-f00c23f9997e.png)


# 実装
|実装方法|
|---|
|[フロントエンド側の実装](#フロントエンド側の実装)|
|[バックエンド側の実装](#バック側の実装)|

## 注意事項
__Thymeleaf__ を使用するため htmlは下記の __src\main\resources\templates__ 配下に作成します。
__Thymeleaf__ を使用するため Javascriptは下記の　__src\main\resources\static\js__ 配下に作成します。
__Thymeleaf__ を使用するためhtmlタグが違います。注意してください。
```:Thymeleafを使用する
<html xmlns:th="http://www.thymeleaf.org">
```
__Thymeleaf__ を使用するためJavascriptの読み込み方が違います。注意してください。
```:Javascriptの読み込み
<script th:src="@{/js/upload.js}"></script>
<script th:src="@{/js/download.js}"></script>
```

※レイアウト(CSS)は設定してないので見た目はよくないです。

__jQuery__ は下記の __CDN__ を使用しております。

https://releases.jquery.com/

## フロントエンド側の実装
### アップロード
アップロード画面のhtmlとJavascriptは以下のように実装します。
```:upload.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title>Insert title here</title>
	<script src="https://code.jquery.com/jquery-3.6.3.js"
		integrity="sha256-nQLuAZGRRcILA+6dMBOvcRh5Pe310sBpanc6+QBmyVM=" crossorigin="anonymous"></script>
</head>

<body>
	
    <input type="file" id="file">
    <button type="button" id="btn">アップロード</button>
	
	<dialog>
		<p>アップロードしますか？</p>
		<button id="yes">はい</button>
		<button id="no">いいえ</button>
	</dialog>
    <script th:src="@{/js/upload.js}"></script>
</body>

</html>
```
```:upload.js
let dialog = document.querySelector('dialog');
let btn = document.getElementById('btn');
let yes = document.getElementById('yes');
let no = document.getElementById('no');


btn.addEventListener('click', function() { 
    // 開くボタンをクリックした場合の処理
    dialog.showModal();
}, false);

yes.addEventListener('click', function() { 
    // はいボタンをクリックした場合の処理
    // 通常のJavascriptで実装
    sendDataXMLHttpRequest();
    // jQueryで実装　
    //sendDatajQuery();
    dialog.close();

}, false);

no.addEventListener('click', function() { 
    // いいえボタンをクリックした場合の処理    
    dialog.close();
}, false);

/**
 * 普通のJavascriptで実装
 */
function sendDataXMLHttpRequest(){
    // ファイルを取得
    let file = document.getElementById('file').files[0];

    // フォームデータを設定
    let formdata = new FormData();
    formdata.append( "file", file);
     
    let xmlhttp = new XMLHttpRequest();

    xmlhttp.onload = function(){
        alert("成功");
    }

    xmlhttp.onerror = function(){
        alert("失敗");
    }

    xmlhttp.open("POST", "/uploadFile", true);
    xmlhttp.send(formdata);
}

/**
 * jQueryで実装
 */
function sendDatajQuery(){
    // ファイルを取得
    let file = $('#file')[0].files[0];

    // フォームデータを取得
    let formdata = new FormData();
    formdata.append( "file", file);
     
    // POSTでアップロード
	$.ajax({
          url  : "/uploadFile",
          type : "POST",
          data : formdata,
          cache       : false,
          contentType : false,
          processData : false,
          dataType    : "html"
      })
      .done(function(){
          alert("成功");
      })
      .fail(function(){
          alert("失敗");
      });
}
```

### ダウンロード
ダウンロード画面のhtmlとJavascriptは以下のように実装します。
```:download.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title>Insert title here</title>
	<script src="https://code.jquery.com/jquery-3.6.3.js"
		integrity="sha256-nQLuAZGRRcILA+6dMBOvcRh5Pe310sBpanc6+QBmyVM=" crossorigin="anonymous"></script>
</head>

<body>
	
    <button type="button" id="btn">ダウンロード</button>
	
	<dialog>
		<p>ダウンロードしますか？</p>
		<button id="yes">はい</button>
		<button id="no">いいえ</button>
	</dialog>
	<script th:src="@{/js/download.js}"></script>
</body>

</html>
```
```:download.js
let dialog = document.querySelector('dialog');
let btn = document.getElementById('btn');
let yes = document.getElementById('yes');
let no = document.getElementById('no');


btn.addEventListener('click', function() {
	// 開くボタンをクリックした場合の処理
	dialog.showModal();
}, false);

yes.addEventListener('click', function() {
	// はいボタンをクリックした場合の処理
    // 通常のJavascriptで実装
	getDataXMLHttpRequest();
    // jQueryで実装
    // getDatajQuery();
	dialog.close();

}, false);

no.addEventListener('click', function() {
	// いいえボタンをクリックした場合の処理    
	dialog.close();
}, false);

/**
 * 普通のJavascriptで実装
 */
function getDataXMLHttpRequest() {

	let xmlhttp = new XMLHttpRequest();

	xmlhttp.onload = function(data) {
		let filename = "";
		// サーバ側で設定したファイル名を正規表現で取得
		let disposition = xmlhttp.getResponseHeader('Content-Disposition');
		if (disposition && disposition.indexOf('attachment') !== -1) {
			let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
			let matches = filenameRegex.exec(disposition);
			if (matches != null && matches[1]) {
				filename = matches[1].replace(/['"]/g, '');
			}
		}
		let blob = new Blob([data.target.response]);
		let objURL = window.URL.createObjectURL(blob);
		// リンクを生成し、JavaScriptからクリック
		let a = document.createElement("a");
		document.body.appendChild(a);
		a.href = objURL;
		a.download = filename;
		a.click();
	}

	xmlhttp.onerror = function() {
		alert("失敗");
	}

	xmlhttp.open("POST", "/downloadFile", true);
	xmlhttp.send();
}

/**
 * jQueryで実装
 */
function getDatajQuery() {
	// POSTでアップロード
	$.ajax({
		url: "/downloadFile",
		type: "POST",
		cache: false,
		contentType: false,
		processData: false,
		dataType: "html"
	})
		.done(function(data) {
			let fileName = 'test.txt';
			let blob = new Blob([data]);
			let objURL = window.URL.createObjectURL(blob);
			// リンクを生成し、JavaScriptからクリック
			let a = document.createElement("a");
			document.body.appendChild(a);
			a.href = objURL;
			a.download = fileName;
			a.click();
		})
		.fail(function() {
			alert("失敗");
		});
}
```
※jQueryを使用した際のfile名の取得方法がわからなかったので直接指定しています。すみません・・・。

## バックエンド側の実装
### コントローラの実装
html表示の用のコントローラです。
```:GamenController.java
package com.app.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class GamenController {
	/**
	 * upload.htmlを表示するコントローラ
	 * @return upload.html 
	 */
	@GetMapping("/upload")
	public String upload() {
		return "upload";
	}
	
	/**
	 * download.htmlを表示するコントローラ
	 * @return upload.html 
	 */
	@GetMapping("/download")
	public String download() {
		return "download";
	}
}
```
Api呼び出し用のコントローラです。
```:
package com.app.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.app.services.DownloadService;
import com.app.services.UploadService;

import jakarta.servlet.http.HttpServletResponse;

@RestController
public class UploadDownController {
	@Autowired
	UploadService uploadService;
	
	@Autowired
	DownloadService downloadService;
	
	/**
	 * ファイルアップロードAPI
	 * @param file アップロードしたファイル
	 */
	@PostMapping(value = "/uploadFile")
	public void uploadFile(@RequestParam("file") MultipartFile file) {
		uploadService.fileUpload(file);
	}
	
	/**
	 * ファイルアップロードAPI
	 * @param file アップロードしたファイル
	 */
	@PostMapping(value = "/downloadFile")
	public void downloadFile(HttpServletResponse response) {
		downloadService.fileDownload(response);
	}	
}
```

### アップロードサービスの実装
アップロードの実装方法です。
```:UploadService.java
package com.app.services;

import org.springframework.web.multipart.MultipartFile;

public interface UploadService {
	/**
	 * ファイルアップロードサービス
	 * 
	 * @param file アップロードしたファイル
	 */
	public void fileUpload(MultipartFile file);
}
```
```:UploadServiceImpl.java
package com.app.services;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public class UploadServiceImpl implements UploadService {
	
	/**
	 * ファイルをInputStreamに変換し、出力用ファイルオブジェクトを作成してOutputStreamに変換し
	 * InputStreamの内容を読み込んでOutputStreamに出力する。
	 * @param file アップロードしたファイル  
	 */
	@Override
	public void fileUpload(MultipartFile file) {
		// 出力先パス+ファイル名を作成
		String path = "./src/main/resources/uploadFile/" + file.getOriginalFilename();
		
		// ファイルオブジェクトを作成
		File convFile = new File(path);

		try (InputStream inputStream = file.getInputStream();
				OutputStream outputStream = new FileOutputStream(convFile)) {
			int read = 0;
			byte[] bytes = new byte[1024];
			
			// InputStreamの内容を読み込んでOutputStreamに出力する。
			while ((read = inputStream.read(bytes)) != -1) {
				outputStream.write(bytes, 0, read);
			}
		} catch (IOException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}
	}

}
```

### ダウンロードサービスの実装
ダウンロードの実装方法です。
```:DownloadService.java
package com.app.services;

import jakarta.servlet.http.HttpServletResponse;

public interface DownloadService {
	/**
	 * ファイルダウンロード
	 * @param response　レスポンスデータ(ファイルの情報)
	 */
	public void fileDownload(HttpServletResponse response);
}
```
```:DownloadServiceImpl.java
package com.app.services;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.springframework.stereotype.Service;

import jakarta.servlet.http.HttpServletResponse;

@Service
public class DownloadServiceImpl implements DownloadService {
	
	/**
	 * InputStreamでダウンロードしたいファイルを読み込み、OutputStreamでresponseに書き込む
	 * @param response　レスポンスデータ(ファイルの情報)
	 */
	@Override
	public void fileDownload(HttpServletResponse response) {
		try (InputStream inputStream = new FileInputStream("./src/main/resources/uploadFile/test.txt");
				OutputStream outputStream = response.getOutputStream();) {

			byte[] fileByteArray = inputStream.readAllBytes();

			response.setContentType("application/octet-stream");
			response.setHeader("Content-Disposition", "attachment; filename=test.txt");
			response.setContentLength(fileByteArray.length);

			outputStream.write(fileByteArray);
			outputStream.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```
以上です。

下記は今回作成したプロジェクトになります。

https://github.com/T-H9703EnAc/UploadDownload

# 参考
https://ja.getdocs.org/java-how-to-convert-inputstream-to-file-in-java

https://qiita.com/kenta8813/items/d0307c258d61ecebb064

https://stackoverflow.com/questions/40939380/how-to-get-file-name-from-content-disposition

https://ja.getdocs.org/java-how-to-convert-inputstream-to-file-in-java

https://www.javadrive.jp/javascript/form/index8.html

https://javascript.keicode.com/newjs/download-files.php