#25 Springでファイルのアップロードを行う
今回は画面からアップロードされたファイルを読込み、データベース内に格納し、画面に再表示させます。
前提条件
この記事はSpringの最低限の知識が必要になります。
また、なるべく分かりやすく書くつもりですが、この記事の目的は自分の勉強のアウトプットであるため、所々説明は省略します。
前回まで
前回はSELECT文を用いて特定した行のデータを取得を行いました。
構築環境
-
各バージョン
Spring Boot ver 2.7.5
mybatis-spring-boot-starter ver 2.2.2
Model Mapper ver 3.1.0
jquery ver 3.6.1
bootstrap ver 5.2.2
webjars-locator ver 0.46
thymeleaf-layout-dialect ver 3.0.0
成果物
今回行うこと
今回は以下の流れに沿って進めていきます。
- アップロードされたファイルをFormクラス(SignupForm.java)に取り込む
- SignupForm.java
- MUser.java
- SignupController.java
- バイト型に変換しテーブル(M_USER)内に格納する
- UserService.java
- UserServiceImpl.java
- base64でエンコードする
- 画面上にアイコンを表示する
- デフォルトの画像を差し込む
1. UserService.java
2. UserServiceImpl.java
3. SignupController.java - 画面の作成(signup.html)
1. アップロードされたファイルをFormクラス(SignupForm.java)に取り込む
1. SignupForm.java
関係ないフィールドとimport文が多かったので一部省略しました。
アップロードされたファイルをFormクラスに格納するためには型にMultipartFile
を用います。
package com.example.form;
/* 省略 */
import org.springframework.web.multipart.MultipartFile;
@Data
public class SignupForm {
/* 省略 */
private MultipartFile accountIcon;
private Integer gender;
}
2. MUser.java
今回画像データを格納するためのテーブル(M_USER
)のカラムACCOUNT_ICON
のデータ型はBLOB
型です。
このBLOB型にはデータをバイト型(byte[]
)として入れなくてはいけません。
package com.example.model;
import java.util.Date;
import lombok.Data;
@Data
public class MUser {
/* 省略 */
private byte[] accountIcon;
private Integer gender;
}
3. SignupController.java
package com.example.controller;
/* 省略 */
@Controller
@RequestMapping("/user")
@Slf4j
public class SignupController {
@Autowired
private UserService userService;
@Autowired
private ModelMapper modelMapper;
/* ユーザー登録画面を表示 */
@GetMapping("/signup")
public String getSignup(Model model, Locale locale, @ModelAttribute SignupForm form) {
/* 省略 */
}
/* ユーザー登録処理 */
@PostMapping("/signup")
public String postSignup(Model model, Locale locale,
@Validated(GroupOrder.class) @ModelAttribute SignupForm form,
BindingResult bindingResult) {
// 入力チェック
if(bindingResult.hasErrors()) {
// エラーが発生したので登録画面に戻る
return getSignup(model, locale, form);
}
log.info(form.toString());
// フォームに渡されたアップロードファイルを取得
MultipartFile multipartFile = form.getAccountIcon();
// formをMUserクラスに変換
MUser user = modelMapper.map(form, MUser.class);
// アップロード実行処理メソッドの呼び出し
user.setAccountIcon(userService.uploadFile(multipartFile));
log.info(user.toString());
// ユーザ登録
userService.signUp(user);
// ログイン画面にリダイレクト
return "redirect:/login";
}
}
画面から受け取ったデータをFormクラスに格納する際に以下のコードを用いて個別にaccountIcon
のデータを取得し、変数multipartFile
内に格納しておきます。
// フォームに渡されたアップロードファイルを取得
MultipartFile multipartFile = form.getAccountIcon();
modelMapper.map
メソッドを用いてFormクラスに格納されているデータをエンティティのMUser
にコピーするのですが、このままコピーしてもaccountIcon
はnull
になってしまいます。
よって、以下の方法を用いてデータベースに格納できる型に変更します。
具体的は方法は次の2. バイト型に変換しデータベース(M_USER)内に格納する
で説明したいと思いすが、以下の内容を簡単にまとめると、そのままコピーしただけでは値を取得できないのでaccountIcon
だけは個別にデータをセットします。
// アップロード実行処理メソッドの呼び出し
user.setAccountIcon(userService.uploadFile(multipartFile));
2. バイト型に変換しテーブル(M_USER)内に格納する
では実際にテーブル(M_USER)に格納するための処理を書きます。
1. UserService.java
まずは実際の処理を書く前に抽象メソッドを記述します。
package com.example.service;
/* 省略 */
import org.springframework.web.multipart.MultipartFile;
public interface UserService {
/* 省略 */
/* アップロード実行処理 */
public byte[] uploadFile(MultipartFile multipartFile);
}
2. UserServiceImpl.java
UserService.java
に記述した抽象メソッドを実装していきます。
package com.example.service.impl;
/* 省略 */
@Service
public class UserServiceImpl implements UserService {
/* messages.propertiesのDIを注入 */
@Autowired
private MessageSource messagesource;
/* レポジトリー(UserMapper.java)のDIを注入 */
@Autowired
private UserMapper mapper;
/* 省略 */
/* データの挿入 */
@Override
public void signUp(MUser user) {
mapper.insertOne(user);
}
/* アップロードの実行処理 */
@Override
public byte[] uploadFile(MultipartFile multipartFile) {
try {
// アップロードファイルをバイト値に変換
byte[] bytes = multipartFile.getBytes();
return bytes;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/* 省略 */
}
引数にとったmultipartFile
をgetBytes
メソッドを用いてバイト型に変換し、戻り値として返します。
ここで例外が発生した場合try-catch
文によりnullを返します。
/* アップロードの実行処理 */
@Override
public byte[] uploadFile(MultipartFile multipartFile) {
try {
// アップロードファイルをバイト値に変換
byte[] bytes = multipartFile.getBytes();
return bytes;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
よってaccountIcon
の値にはバイト値にが入ります。
// アップロード実行処理メソッドの呼び出し
user.setAccountIcon(userService.uploadFile(multipartFile));
ちなみにlog.info(user.toString())
によりMUser
内に格納されたデータを確認するとaccountIconにはバイト値が格納されていることが分かります(量が多いので一部省略)。
MUser(userId=Aichi@example.com,
phoneNumber=0525522111,
postalNumber1=450,
postalNumber2=8711,
address=愛知県名古屋市中村区名駅4丁目7番1号 ,
userName=ToyotaPuriusu,
password=Karora_2022,
birthday1=null,
birthday2=null, age=null,
accountIcon=[-119, 80, 78, 71, 13, ・・・, 78, 68, -82, 66, 96, -126],
gender=null)
参考サイト
3. base64でエンコードする
次は取得した画像を表示するための処理を記述していきます。バイト値としてテーブルに格納したファイルはそのままでは画面に出力することができません。そのため画面に表示できるよう変更する必要があるのですが、やり方としては2つほどあるそうです。
- th:srcなどで画像出力するためのControllerを呼び出し、かつ画像を一意に特定できるパラメータを指定して、Controllerから別途画像のバイナリデータのみをレスポンスするようにする
- BASE64形式に変換したものを出力する(画像ファイルが1KB程度の小さいもので利用する)
今回は2つ目の方法を用いて画像を表示させたいと思います。1つ目の方法に関しては現在調べ中
package com.example.controller;
import org.apache.tomcat.util.codec.binary.Base64;
/* 省略 */
@Controller
@RequestMapping("/user")
@Slf4j
public class UserDetailController {
@Autowired
private UserService userservice;
@Autowired
private ModelMapper modelMapper;
/* ユーザーの詳細情報を表示 */
@GetMapping("/detail/{userId:.+}")
public String getUser(UserDetailForm form, Model model, @PathVariable("userId") String UserId) throws Exception {
// ユーザーを1件取得
MUser user = userservice.getOneMUser(UserId);
log.info(user.toString());
StringBuffer data = new StringBuffer();
// base64にエンコードしたものを文字列に変更
String base64 = new String(Base64.encodeBase64(user.getAccountIcon()),"ASCII");
// 拡張子をjpegと指定
// <img ht:src="">で指定できる形にする
data.append("data:image/jpeg;base64,");
data.append(base64);
model.addAttribute("base64AccountIcon",data.toString());
// MUserをformに変換
form = modelMapper.map(user, UserDetailForm.class);
log.info(form.toString());
// Modelに登録
model.addAttribute("userDetailForm", form);
// ユーザー詳細情報を表示
return "user/detail";
}
}
参考サイト
4. 画面上にアイコンを表示する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<head>
</head>
<body>
<nav layout:fragment="header" class="navbar navbar-expand-lg navbar-light bg-primary color fixed-top fs-4">
<div class="container-fluid">
<a class="navbar-brand fa fa-house" th:href="@{#}"> Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" th:href="@{#}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{#}">Service</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{#}">Comapany</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{#}">Access</a>
</li>
</ul>
<ul class="navbar-nav align-items-center">
<li class="nav-item align-middle text-white" th:text="'こんにちは' + ${userDetailForm.userName} + 'さん'"></li>
<li class="nav-item">
<a class="nav-link" th:href="@{/login/login}">
<img th:src="${base64AccountIcon}" alt="アイコン画像" th:width="50px" th:height="50px">
</a>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>
先ほどmodelに追加したbase64AccountIcon
をimg
タグに追加します。 また、ログイン者の名前部分も${userDetailForm.userName}
に変更することにより、テーブルから取得した値にします。
<ul class="navbar-nav align-items-center">
<li class="nav-item align-middle text-white" th:text="'こんにちは' + ${userDetailForm.userName} + 'さん'"></li>
<li class="nav-item">
<a class="nav-link" th:href="@{/login/login}">
<img th:src="${base64AccountIcon}" alt="アイコン画像" th:width="50px" th:height="50px">
</a>
</li>
</ul>
画面を確認すると、以下のように名前とアイコンが変更されています。
ただし、ユーザー登録画面でaccountIcon
は必須項目ではありません(ファイルのアップロードを必須項目にするには自分でバリデーションを作成しなければいけない)。
そのため、ユーザー登録画面でaccountIcon
を登録していない状態(accountIcon=null)で3. base64でエンコードする
で記述した処理を実行しようとするとthrows Excepiton
により例外が発生してしまいます。
そうならないよう、ユーザー登録画面でaccountIcon
が登録されなかった場合、ローカルにある画像をアイコンとして差し替えます。
5. デフォルトの画像を差し込む
1. UserService.java
package com.example.service;
import java.io.IOException;
/* 省略 */
public interface UserService {
/* 省略 */
/* デフォルト画像をbyte[]型に変更 */
public byte[] changedByte() throws IOException;
}
2. UserServiceImpl.java
package com.example.service.impl;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.example.model.MUser;
import com.example.repository.UserMapper;
import com.example.service.UserService;
@Service
public class UserServiceImpl implements UserService {
/* 省略 */
@Override
public byte[] changedByte() throws IOException {
// パスの設定
String filePath = "C:/pleiades/2022-06/workspace/SpringOracleSample/src/main/resources/static/img/default.png";
File file = new File(filePath);
// バイト型に変更
byte[] bytes = Files.readAllBytes(file.toPath());
return bytes;
}
}
File file = new File(filePath)
を用いて使用するファイルを読み込みます。その後、readAllBytes
メソッドを用いて指定したファイルをバイト値に変換します。
参考サイト
これ以外にもバイト値への変換方法は何個かあるようです。
3. SignupController.java
package com.example.controller;
import org.apache.tomcat.util.codec.binary.Base64;
import org.modelmapper.ModelMapper;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.form.UserDetailForm;
import com.example.model.MUser;
import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
@Controller
@RequestMapping("/user")
@Slf4j
public class UserDetailController {
@Autowired
private UserService userservice;
@Autowired
private ModelMapper modelMapper;
/* ユーザーの詳細情報を表示 */
@GetMapping("/detail/{userId:.+}")
public String getUser(UserDetailForm form, Model model, @PathVariable("userId") String UserId) throws Exception {
// ユーザーを1件取得
MUser user = userservice.getOneMUser(UserId);
log.info(user.toString());
// accountIconがnullの場合
if(user.getAccountIcon() == null) {
byte[] bytes = userservice.changedByte();
user.setAccountIcon(bytes);
}
StringBuffer stringBuffer = new StringBuffer();
// base64にエンコードしたものを文字列に変更
String base64 = new String(Base64.encodeBase64(user.getAccountIcon()),"ASCII");
// 拡張子をjpegと指定
// <img ht:src="">で指定できる形にする
stringBuffer.append("data:image/jpeg;base64,");
stringBuffer.append(base64);
model.addAttribute("base64AccountIcon",stringBuffer.toString());
// MUserをformに変換
form = modelMapper.map(user, UserDetailForm.class);
log.info(form.toString());
// Modelに登録
model.addAttribute("userDetailForm", form);
// ユーザー詳細情報を表示
return "user/detail";
}
}
if(user.getAccountIcon() == null)
によりaccountIconが設定されていない場合のみ先ほどバイト値に変換したデフォルト画像をMUser
にセットします。
// accountIconがnullの場合
if(user.getAccountIcon() == null) {
byte[] bytes = userservice.changedByte();
user.setAccountIcon(bytes);
}
6. 画面の作成(signup.html)
2023/01/17現在追加で書いています。
ファイルのアップロードを行うために画面(HTML)で必要な設定が抜けていました。
<!-- enctype="multipart/form-data" ← これがformタグに付ける必要あり!!! -->
<form id="signup-form" method="post" th:action="@{/user/signup}" class=form-signup th:object="${signupForm}" enctype="multipart/form-data">
<!-- 省略 -->
<!-- アカウント画像 -->
<div class="form-group">
<label for="formFile" class="form-label" th:text="#{accountIcon}"></label>
<input type="file" class="form-control" th:field="*{accountIcon}" th:errorclass="is-invalid">
<div class="invalid-feedback" th:errors="*{accountIcon}"></div>
<!-- 省略 -->
<!-- 登録ボタン -->
<input type="submit" th:value="#{usreRegistration}" class="btn btn-primary w-100 mt-3">
</form>
最後に
下の画像右上のアイコンを確認すると、今回デフォルトで設定した画像が表示されています。
また、デフォルト画像を設定する前ログを確認すると、acountIcon=null
になっていることが確認できます。
よって、デフォルト画像が上手く表示されていることが分かりました。
MUser(userId=Tokyo@xxx.co.jp,
phoneNumber=0353211111,
postalNumber1=163,
postalNumber2=8001,
address=東京都新宿区西新宿二丁目8番1号,
userName=TokyoTocho,
password=Tokyo_0353211111,
birthday1=null,
birthday2=Mon Apr 01 00:00:00 JST 1991,
age=31,
accountIcon=null,
gender=1)