Spring Bootでエクセルファイルをアップロードして読み込む

**この記事は[フリュー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/furyu)の10日目の記事となります。**
# はじめに
社内で利用するウェブアプリの機能追加として、エクセルファイルを読み込ませて一括処理を行いたいという要望があったので、実装しました。
オンメモリからエクセルファイルを読み込む方法がレアケースなのか、意外にも他に記事として見かけませんでしたので、私が得た情報をアップロードからの流れに沿ってまとめておこうと思います。


# ファイルをアップロードする
フロントエンド、バックエンドそれぞれ説明します。
記載しているソースコードに最低限としており、エラー処理などは入っていませんので実際に利用する際には必要に応じてチェックなど行ってください。

## 環境
以下の環境で動作を確認しています。

| ライブラリ | バージョン |
| ---- | ---- |
| Spring Boot | 3.2.0 |
| Open JDK | 21 |
| JQuery | 3.7.1 |
| Apache POI | 5.2.3 |

その他はSpring Bootプロジェクトデフォルトでの環境となっています。

なお、ブラウザはGoogle Chromeのバージョン120で確認しています。

## フロントエンド
今回はhtmlとJavaScriptを利用してアップロードを実現していきます。
JavaScriptを利用せずSpringの機能を使いアップロードを実現する方法もありますが、こちらはまた別途紹介したいと思います。

以下はファイル選択および、アップロードボタンを配置してアップロード処理をJavaScriptで記載したhtmlファイルになります。
JavaScriptはMultipartFormデータとしてファイルのアップロードを行います。
Multipartのキー名をバックエンドと合わせる必要があり、ここでは`upload_file`という文字列を使用しています。

``` upload.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script type="text/javascript">
        $(function() {
            $('#uploadButton').on('click', function(event) {
				
                var $file = $('#uploadSelectFileBox');

                // FormData オブジェクトを作成
                var formData = new FormData();
                formData.append('upload_file', $file.prop("files")[0]);

                // Ajaxで送信
                $.ajax({
                    // アップロードを処理するAPIへのURL
                    url: 'file/upload',
                    // HTTPメソッド
                    method: 'post',
                    // レスポンスの形式
                    dataType: 'json',
                    // dataに FormDataを指定
                    data: formData,
                    // dataの変換をさせない
                    processData: false,
                    // contentTypeをfalseにすることで適切なContentTypeを指定してくれる
                    contentType: false
                }).done(function(data, textStatus) {
                    // 送信成功

                    // Jsonが返ってきたら以下のようにパースできる
                    var data_stringify = JSON.stringify(data);
                    var data_json = JSON.parse(data_stringify);

                    // Jsonへのアクセスも簡単にできます
                    console.log(data_json.message);

                }).fail(function(XMLHttpRequest, textStatus, errorThrown) {
                    // 失敗時の処理（エラーメッセージなど）
                    console.log(textStatus);
                });

                return false;        
            });
        });
    </script>
</head>
<body>
    <input type="file" name="file" id="uploadSelectFileBox" accept=".xlsx"/>
    <input type="button" id="uploadButton" value="アップロード" />
</body>
</html>
```
こんな感じに表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/7a2b58ed-725a-e1fe-78d5-3e7b04b6783c.png)
これでアップロードボタンを押すと、ファイルがサーバーへ送信されます。
フロントエンドではバックエンドの`file/upload`を呼び出していますので、バックエンド側でこのAPIを用意する必要があります。

## バックエンド
バックエンドのControllerを作ります。
エクセルファイルを読み込むには`Apache POI`ライブラリを利用します。
GradleもしくはMavenの設定ファイルに以下の２つのライブラリを追加します。
※バージョン5.2.3での動作確認を行っています

https://mvnrepository.com/artifact/org.apache.poi/poi/5.2.3

https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml/5.2.3

ここで注意したいのはこれらのライブラリは他のライブラリへの依存関係もありますので、ご自身の環境から必要に応じて上記ページのCompile Dependenciesからバージョンの更新や足りないもの確認してください。

コントローラーの処理は以下になります。
こちらも例によってエラー処理などは端折っていますし、DBへの更新処理を行う場合は安全のために入力値の正当性やトランザクション処理を実装してください。
エクセルのセルへのアクセスもサンプルとして記述しています。

``` FileUploadController.java
package com.upload;

import java.io.ByteArrayInputStream;
import java.util.Date;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Controller;
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.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;


@Controller
@RequestMapping("/file")
public class FileUploadController {


	private class UploadResponse
	{
		public int status = 1;
		public String message = "success";
	}
	
	@PostMapping(value = "/upload")
	@ResponseBody
    public UploadResponse uploadFile( @RequestParam("upload_file") MultipartFile multipartFile ) throws Exception {

		UploadResponse response = new UploadResponse();

		// ファイルが空の場合は異常終了
		if(multipartFile.isEmpty()){
			// 異常終了時の処理
            response.status = 0;
			response.message = "アップロードされたエクセルファイルが存在しなかった";
			return response;
		}

		//バイナリを取得する
		byte[] bytes = multipartFile.getBytes();
		//バイナリからストリームへ変換
		ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

		//エクセルをワークブックインスタンスへ
		Workbook wb = WorkbookFactory.create(inputStream);

		// ほげ１という名前のシートを取得
		Sheet sheet = wb.getSheet("ほげ１");

		// 1行目取得
		Row row = sheet.getRow(0);

		// データがある最後のセル番号取得
		final int cellNum = row.getLastCellNum();
		// セル分ループ
		for (int cellIndex = 0; cellIndex < cellNum; ++cellIndex) {
		    //セル取得
		    Cell cell = row.getCell(cellIndex);

		    //セルの値取得
		    switch (cell.getCellType()) {
		        case STRING:
		            String stringVal = cell.getStringCellValue();
		            // 文字列データの処理
		            break;
		        case NUMERIC:
		            if (DateUtil.isCellDateFormatted(cell)) {
		                Date dateVal = cell.getDateCellValue();
		                // 日付データの処理
		            } else {
		                double numVal = cell.getNumericCellValue();
		                // 数値データの処理
		            }
		            break;
		        default:
		            // その他のデータタイプに対する処理
		        	break;
		    }

		    //必要に応じた処理
		}

		response.message = "正常に完了しました";
		return response;
	}
}
```

実装は以上となります。
ポイントとしては、JavaScript側で指定した`upload_file`を指定してアップロードされたファイルを取得している点と、WorkbookFactory.createへはbyte[]では引数に渡せなかったのでストリームとして`ByteArrayInputStream`を使ってエクセルを開いています。
セルにアクセスするにはセルに設定されている値の種類を取得し、種類によって処理を振り分ける必要があります。

エクセルの操作内容については以下のように他に詳しい記事がたくさんありますので、そちら参考にしていただくのが良いかと思います。

https://qiita.com/yhayashi30/items/e5cbe848a2fb5c9183af

# まとめ
・シンプルなアップロード処理は簡単に実現できます
・`ByteArrayInputStream`を使えばオンメモリでエクセルファイルを開くことができる
・`Apache POI`を使えば簡単にエクセル操作が可能になります

ライブラリがあるおかげで実装量も少なくアップロードからエクセル操作まで実装できました。
次回、Spring BootとThymeleafを利用したアップロード方法も紹介したいと思います。