4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

#25 Springでファイルのアップロードを行う

Last updated at Posted at 2022-12-04

#25 Springでファイルのアップロードを行う

今回は画面からアップロードされたファイルを読込み、データベース内に格納し、画面に再表示させます。

前提条件

この記事はSpringの最低限の知識が必要になります。
また、なるべく分かりやすく書くつもりですが、この記事の目的は自分の勉強のアウトプットであるため、所々説明は省略します。

前回まで

前回はSELECT文を用いて特定した行のデータを取得を行いました。

構築環境

  1. 各バージョン
    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

  2. 依存関係
    image.png

成果物

image.png

今回行うこと

今回は以下の流れに沿って進めていきます。

  1. アップロードされたファイルをFormクラス(SignupForm.java)に取り込む
    1. SignupForm.java
    2. MUser.java
    3. SignupController.java
  2. バイト型に変換しテーブル(M_USER)内に格納する
    1. UserService.java
    2. UserServiceImpl.java
  3. base64でエンコードする
  4. 画面上にアイコンを表示する
  5. デフォルトの画像を差し込む
     1. UserService.java
     2. UserServiceImpl.java
     3. SignupController.java
  6. 画面の作成(signup.html)

1. アップロードされたファイルをFormクラス(SignupForm.java)に取り込む

1. SignupForm.java

関係ないフィールドとimport文が多かったので一部省略しました。
アップロードされたファイルをFormクラスに格納するためには型にMultipartFileを用います。

SignupForm.java
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[])として入れなくてはいけません。

MUser.java
package com.example.model;

import java.util.Date;

import lombok.Data;

@Data
public class MUser {

    /* 省略 */

	private byte[] accountIcon;
	private Integer gender;
}

3. SignupController.java

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にコピーするのですが、このままコピーしてもaccountIconnullになってしまいます。
よって、以下の方法を用いてデータベースに格納できる型に変更します。

具体的は方法は次の2. バイト型に変換しデータベース(M_USER)内に格納するで説明したいと思いすが、以下の内容を簡単にまとめると、そのままコピーしただけでは値を取得できないのでaccountIconだけは個別にデータをセットします。

		// アップロード実行処理メソッドの呼び出し
		user.setAccountIcon(userService.uploadFile(multipartFile));

2. バイト型に変換しテーブル(M_USER)内に格納する

では実際にテーブル(M_USER)に格納するための処理を書きます。

1. UserService.java

まずは実際の処理を書く前に抽象メソッドを記述します。

UserServie.java
package com.example.service;

/* 省略 */

import org.springframework.web.multipart.MultipartFile;

public interface UserService {
	
    /* 省略 */
	
	/* アップロード実行処理 */
	public byte[] uploadFile(MultipartFile multipartFile);
}

2. UserServiceImpl.java

UserService.javaに記述した抽象メソッドを実装していきます。

UserServiceImpl.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;
    	}
	}

	/* 省略 */
}

引数にとったmultipartFilegetBytesメソッドを用いてバイト型に変換し、戻り値として返します。
ここで例外が発生した場合try-catch文によりnullを返します。

	/* アップロードの実行処理 */
	@Override
	public byte[] uploadFile(MultipartFile multipartFile) {
    	try {
        	// アップロードファイルをバイト値に変換
    		byte[] bytes = multipartFile.getBytes();
			
    		return bytes;
			
    	} catch (IOException e) {
        	e.printStackTrace();
    		return null;
    	}
	}

よってaccountIconの値にはバイト値にが入ります。

SignupController.java
		// アップロード実行処理メソッドの呼び出し
		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つほどあるそうです。

  1. th:srcなどで画像出力するためのControllerを呼び出し、かつ画像を一意に特定できるパラメータを指定して、Controllerから別途画像のバイナリデータのみをレスポンスするようにする
  2. BASE64形式に変換したものを出力する(画像ファイルが1KB程度の小さいもので利用する)

今回は2つ目の方法を用いて画像を表示させたいと思います。1つ目の方法に関しては現在調べ中

UserDetailController.java
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. 画面上にアイコンを表示する

header.html
<!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に追加したbase64AccountIconimgタグに追加します。 また、ログイン者の名前部分も${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>

画面を確認すると、以下のように名前とアイコンが変更されています。
image.png

ただし、ユーザー登録画面でaccountIconは必須項目ではありません(ファイルのアップロードを必須項目にするには自分でバリデーションを作成しなければいけない)。
そのため、ユーザー登録画面でaccountIconを登録していない状態(accountIcon=null)で3. base64でエンコードするで記述した処理を実行しようとするとthrows Excepitonにより例外が発生してしまいます。
そうならないよう、ユーザー登録画面でaccountIconが登録されなかった場合、ローカルにある画像をアイコンとして差し替えます。

5. デフォルトの画像を差し込む

1. UserService.java

UserService.java
package com.example.service;

import java.io.IOException;

/* 省略 */

public interface UserService {
	
    /* 省略 */    

	/* デフォルト画像をbyte[]型に変更 */
	public byte[] changedByte() throws IOException;
}

2. UserServiceImpl.java

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

SignupController.jva
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)で必要な設定が抜けていました。

signup.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>

最後に

下の画像右上のアイコンを確認すると、今回デフォルトで設定した画像が表示されています。
image.png

また、デフォルト画像を設定する前ログを確認すると、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)
4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?