画像アップロード

フォームからファイルをアップロードしてプロジェクト内に保存

画像ファイルをプロジェクト内に保存する機能を実装します。
「テキストファイル操作」の単元と類似の記述も多くなっていますので、見比べてください。

Formクラス、DTO

Formクラス

Formクラスはテキストファイルとほぼ同じです。
【/com/cmps/spring/form/ImgForm.java】

package com.cmps.spring.form;

import java.io.Serializable;
import org.springframework.web.multipart.MultipartFile;

import lombok.Data;

@Data
public class ImgForm implements Serializable {

	//imgファイル
	private MultipartFile file;
}

DTOクラス

プロジェクト配下の画像ファイルは、CSSファイルと同じようにリンク式@{ } で読み込めるので、そこに記述するためのパスを設定しています。
【/com/cmps/spring/dto/ImgDto.java】

package com.cmps.spring.dto;

import lombok.Data;

@Data
public class ImgDto {

	//ファイル名
	private String name;
	
	//ファイルパス
	private String path;
}

View

画像をアップロードするフォームと、アップロード済みの画像を一覧表示するためのViewです。
【src/main/resources/templates/img/index.html】

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>画像アップロード</title>
	<link rel="stylesheet" th:href="@{/css/manual.css}">
</head>
<body>
	<h1>画像アップロード(プロジェクト内)</h1>
	<div th:if="${resultMessage}" class="messageBlock"><!-- 完了メッセージ -->
		<p th:text="${resultMessage}" class="messageBlock_text"></p>
	</div>
	<div class="block">
		<h2>画像アップロード</h2>
		<form method="post" th:action="@{/imgup/upload}" enctype="multipart/form-data"><!-- 画像のアップロード フォーム -->
			<input type="file" name="file" accept="image/*">
			<input type="submit" value="アップロード">
		</form>
		<th:block th:if="${imgForm != null}" th:object="${imgForm}"><!-- エラー表示 -->
			<p th:if="${#fields.hasErrors('file')}" th:errors="*{file}" style="color:red"></p>
		</th:block>
	</div>
	<div class="block">
		<h2>保存した画像の表示</h2>
		<ul class="upload_list" th:if="${list}"><!-- 画像の一覧表示 -->
			<li class="upload_item" th:each="file : ${list}" th:object="${file}">
				<p th:text="*{name}"></p>
				<img th:src="@{*{path}}">
			</li>
		</ul>
	</div>
	<script>
		//画像の再読み込みのため、ページをリロードさせるjavascriptの記述
		//window.nameを出力(確認用)
		console.log("window.name :" + window.name);
		//再読み込みの処理
		if(window.name != "once") {
			setTimeout(function () {
				location.reload();
				window.name = "once";
			}, 3000);
			
		} else {
			window.name = "";
		}
	</script>
</body>
</html>

コード内のコメント文に沿って解説していきます。

<!– 画像のアップロード フォーム –>

こちらはテキストファイルとほとんど同じで、formタグにはenctype=”multipart/form-data”を設定しています。(ファイル送信するために必須)
また、inputタグには accept="image/*" を記述し、画像のみ受け付けるようにしています。

<!– 画像の一覧表示 –>

Controller側から受け取ったlistをth:eachで処理しています。
フィールド変数pathには、staticフォルダ下にある/images/~のパスを受け取っており(Controller側の処理)、
それを更にリンク式@{}で処理することで、srcタグで読み込めるようにしています。

//画像の再読み込みのため、ページをリロードさせるjavascriptの記述

javascriptの記述です。ここでは記述内容の詳細については割愛しますが、内容としてはアクセスした3秒後画面を再読み込みするという内容です。
ファイルをアップロードするメソッドから、初期表示画面へリダイレクトした際に、即座に画像が反映されないケースがあるため、ここでは応急処置として記述しています。

Controller

【/com/cmps/spring/controller/ImgController.java】


package com.cmps.spring.controller;

import java.io.File;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.cmps.spring.dto.ImgDto;
import com.cmps.spring.form.ImgForm;

@Controller
@RequestMapping("/imgup")
public class ImgController {

	/**
	 * 初期画面表示
	 * @param model Model
	 * @param resultMessage String
	 * @return
	 */
	@GetMapping("")
	public String index(Model model,
			@ModelAttribute ImgForm imgForm,
			@ModelAttribute("resultMessage") String resultMessage) {

		//Viewに渡すList ImgDtoオブジェクトに必要な情報を詰めて渡す
		List<ImgDto> list = new ArrayList<ImgDto>();

		//ディレクトリパスを指定してFileオブジェクトを生成
		File dir = new File("src/main/resources/static/images/");
		//listFilesメソッドを使用して一覧(Fileクラス型配列)を取得する
		File[] fileList = dir.listFiles();

		try {
			for (File file : fileList) {
				//オブジェクト生成
				ImgDto dto = new ImgDto();
				//名前を格納
				dto.setName(file.getName());

				//ファイルのパスをStringで取得して格納
				//直接取得できるのは絶対パスsrc\main\resources\static\images\~ だが、
				//thymeleafのリンク式に指定して表示できるのはstatic配下のパスであるため、不要な部分を除去する
				String absolutePath = file.toString();//toString()はObject型に用意されているメソッド
				String path = absolutePath.replace("src\\main\\resources\\static", "");

				//DTOオブジェクトにcontentを格納
				dto.setPath(path);
				//リストに追加
				list.add(dto);
			}

		} catch (Exception e) {
			e.printStackTrace();
		}

		model.addAttribute("list", list);
		return "img/index";
	}

	/**
	 * ファイルアップロード resources/static/imagesに保存する
	 * @param model Model
	 * @param redirectAttributes String
	 * @return
	 */
	@PostMapping("/upload")
	public String upload(Model model,
			@Validated ImgForm imgForm, BindingResult result,
			RedirectAttributes redirectAttributes) {

		//バリデーションエラー返却
		if (result.hasErrors()) {
			return index(model, imgForm, "");
		}

		//書き込み対象ファイル
		Path path = Path.of("src/main/resources/static/images/", imgForm.getFile().getOriginalFilename());

		try {
			//ファイルの保存
			imgForm.getFile().transferTo(path);

		} catch (Exception e) {
			e.printStackTrace();
			redirectAttributes.addFlashAttribute("resultMessage", "ファイルの保存に失敗しました。");
			return "redirect:/imgup";
		}

		// redirectAttributesに登録
		redirectAttributes.addFlashAttribute("resultMessage", "画像ファイルのアップロードが完了しました。");
		return "redirect:/imgup";
	}
}

indexメソッド(初期画面表示)

テキストファイル操作の単元と異なるのは、画像パス取得の部分です。

file.toString()でファイルのパスを取得していますが、この時点では”src\main\resources\static”から始まるパスになっています。
しかしリンク式で処理できるのは、staticより下のパスになりますので、前半の”src\main\resources\static”を除去するためにreplace()メソッドを使用しています。
 (例) src\main\resources\static\images\image.jpg →加工後 \images\image.jpg

uploadメソッド(ファイルアップロード・保存)

ここではFileクラスのtransferToメソッドを使用して、プロジェクト内に保存しています。

画像をバイナリ変換してDBに保存する

バイナリ(binary)とは、コンピュータが扱うデータの記述形式のひとつで、2進法(0と1の2値)で表現されたデータ形式です。
画像や動画など、文字や改行などの特殊文字コード以外の情報を含んでいるファイルがバイナリファイルにあたります。
参考:【図解】バイナリとテキスト(ascii)の違いと利点,判別 ~fileとNWプロトコルでの扱い~ | SEの道標
Javaでバイナリデータを扱う時、byte配列 byte[]を使用します。

ただし、画像やファイルなどのデータの転送や保存には、
バイナリデータのまま用いるのではなく、Base64形式の文字列に変換する方法をよく用います。ASCII文字列に変換することで扱いやすくなるのです。
また、Base64にエンコードされた画像はウェブページに直接埋め込むことができます。(imgタグのsrc属性に指定することが可能)
その他、注意点としてはBase64エンコードすることでデータサイズが約33%増加します。

一般に、異なる形式に変換することを「エンコード」、元の形式に戻すことを「デコード」といいます。

実際に実装していきます。

テーブル、Entity、Repository

テーブル

以下のカラムを持つ「images」テーブルを使用します。
バイナリデータを格納するカラムは、「BLOB型」を選択します。
BLOBとは、Binary Large Objectの略で、バイナリデータを保存するためのデータ型です。

imagesテーブル

カラム名データ型長さデフォルト値
idINT自動採番
dataBLOB
nameVARCHAR2100
contentTypeVARCHAR220

Entity

idカラムのsequenceName およびSEQUENCEの作成も合わせて実施してください。
【/com/cmps/spring/entity/Image.java】

package com.cmps.spring.entity;

import java.io.Serializable;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@Data
@Table(name = "images")
public class Image implements Serializable {

	//ID
	@Id
	@SequenceGenerator(name = "IMG_SEQUENCE_GENERATOR", schema = "sample", sequenceName = "IMAGE_SEQ01", allocationSize = 1)
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "IMG_SEQUENCE_GENERATOR")
	private Integer id;

	//BLOBデータ
	@Column
	private byte[] data;
	
	//ファイル名
	@Column
	private String name;
	
	//拡張子
	@Column
	private String contentType;
	
	//引数ありコンストラクタ
	public Image(byte[] data, String name, String contentType) {
		this.data = data;
		this.name = name;
		this.contentType = contentType;		
	}
}

BLOBデータを格納する予定のdataカラムは、前述の説明の通りbyte[]型になっています。

Repository

各自で作成してください。

Form、DTOクラス

Formクラスは、前半で作成したImgFormを共用します。

DTOクラス

【/com/cmps/spring/dto/ImgDbDto.java】

package com.cmps.spring.dto;

import java.util.Base64;
import com.cmps.spring.entity.Image;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Data
public class ImgDbDto {

	//ファイル名
	private String name;

	//エンコードしたデータ(Base64形式)
	private String encodeData;

	//コンテントタイプ
	private String contentType;

	//引数ありコンストラクタ(Imageを受け取る)
	public ImgDbDto(Image image) {
		this.name = image.getName();
		this.contentType = image.getContentType();
		this.encodeData = Base64.getEncoder().encodeToString(image.getData());
	}

	/**
	 * imgタグのsrc属性に記述するテキストを返す
	 * @return String
	 */
	public String getSrcText() {
		return "data:" + this.contentType + "; base64," + this.encodeData;
	}
}

このDTOクラスでのメンバ変数は3つ(name、encodeData、contentType)です。
このメンバ変数に対して、少し特殊なコンストラクタを用意しています。
・引数にImageのEntityを受け取る
・name、contentTypeはImageのメンバ変数をgetterで取得して格納
・encodeDataは、バイナリデータimage.getData() を引数にとり、Base64クラスのメソッドを利用してBase64形式にエンコードした文字列に変換しています。
この処理はController(service)側に実装することももちろん可能ですが、今回は専用の処理としてコンストラクタ内に用意しています。

getSrcText()メソッドは、実際にimgタグのsrc属性に直接挿入できる形に文字列を生成します。
imgタグに下記のように指定するとBase64形式の画像を表示できます。(imgタグ内でデコードしてくれる)
<img src=”data : image/png ; base64, 【Base64文字列】” />

(実際にgetSrcText()メソッドの返り値を挿入した場合の例)
検証ツール(デベロッパーツール)で確認するとHTML上で下記のように表示されています。

View

画像をアップロードするフォームと、アップロード済みの画像を一覧表示するためのViewです。
【src/main/resources/templates/img/indexDB.html】

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>画像アップロード(DB)</title>
<link rel="stylesheet" th:href="@{/css/manual.css}">
</head>
<body>
<h1>画像アップロード(DB)</h1>
<div th:if="${resultMessage}" class="messageBlock"><!-- 完了メッセージ -->
<p th:text="${resultMessage}" class="messageBlock_text"></p>
</div>
<div class="block">
<h2>画像をバイナリ変換してDBに保存</h2>
<form method="post" th:action="@{/imgupDB/upload}" enctype="multipart/form-data"><!-- 画像のアップロード フォーム -->
<input type="file" name="file" accept="image/*">
<input type="submit" value="DBにアップロード">
</form>
<th:block th:if="${imgForm != null}" th:object="${imgForm}"><!-- エラー表示 -->
<p th:if="${#fields.hasErrors('file')}" th:errors="*{file}" style="color:red"></p>
</th:block>
</div>
<div class="block">
<h2 class="db_h2">保存した画像の表示(DBから)</h2>
<ul class="upload_list" th:if="${list}"><!-- 画像の一覧表示 -->
<li class="upload_item" th:each="item : ${list}" th:object="${item}">
<p th:text="*{name}"></p>
<img th:src="*{srcText}">
</li>
</ul>
</div>
</body>
</html>


<!– 画像の一覧表示 –>

imgタグのsrc属性に指定したいパスは、前述した通りDTOクラスのgetSrcText()メソッドで生成しているので、thymeleaf上では直接呼び出すのみです。

Controller

【/com/cmps/spring/controller/ImgDbController.java】

package com.cmps.spring.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.cmps.spring.dto.ImgDbDto;
import com.cmps.spring.entity.Image;
import com.cmps.spring.form.ImgForm;
import com.cmps.spring.service.ImageService;

@Controller
@RequestMapping("/imgupDB")
public class ImgDbController {

	@Autowired
	ImageService imageService;

	/**
	 * 画像アップロードDB版 初期表示画面
	 * @param model Modl
	 * @param imgForm ImgForm
	 * @param resultMessage String
	 * @return
	 */
	@GetMapping("")
	public String index(Model model,
			@ModelAttribute ImgForm imgForm,
			@ModelAttribute("resultMessage") String resultMessage) {

		//Viewに渡すList ImgDbDtoオブジェクトに必要な情報を詰めて渡す
		List<ImgDbDto> list = new ArrayList<ImgDbDto>();

		//DBから全件取得
		List<Image> imageList = imageService.findAll();

		for (Image item : imageList) {
			//オブジェクト生成(コンストラクタで値を格納)
			ImgDbDto dto = new ImgDbDto(item);
			//リストに追加
			list.add(dto);
		}

		model.addAttribute("list", list);
		return "img/indexDB";
	}

	/**
	 * 画像アップロード DBに保存する
	 * @param model Model
	 * @param redirectAttributes String
	 * @return
	 */
	@PostMapping("/upload")
	public String upload(Model model,
			@Validated ImgForm imgForm, BindingResult result,
			RedirectAttributes redirectAttributes) {

		//バリデーションエラー返却
		if (result.hasErrors()) {
			return index(model, imgForm, "");
		}

		try {
			//エンティティの生成、値のセット(コンストラクタ)
			Image imageEntity = new Image(
					imgForm.getFile().getBytes(),
					imgForm.getFile().getOriginalFilename(),
					imgForm.getFile().getContentType());

			//登録処理
			imageService.saveEntity(imageEntity);

		} catch (Exception e) {
			e.printStackTrace();
			redirectAttributes.addFlashAttribute("resultMessage", "ファイルの保存に失敗しました。");
			return "redirect:/imgupDB";
		}

		redirectAttributes.addFlashAttribute("resultMessage", "画像ファイルのアップロードが完了しました。");
		return "redirect:/imgupDB";
	}
}

Service

【/com/cmps/spring/service/ImageService.java】

package com.cmps.spring.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.cmps.spring.entity.Image;
import com.cmps.spring.repository.ImageRepository;

@Service
public class ImageService {
	@Autowired
	ImageRepository imageRepository;

	/**
	 * 受け取ったEntityをDBに登録する処理
	 * @param imageEntity Image
	 */
	public void saveEntity(Image imageEntity) {
		imageRepository.save(imageEntity);
	}

	/**
	 * 全件取得
	 * @return
	 */
	public List<Image> findAll() {
		return imageRepository.findAll();
	}
}

実際には、BLOB型で画像ファイルを保存することには、いくつかのデメリットがあります。
大規模データの場合にはパフォーマンスの低下・クエリの負荷、DBの肥大化、DBへの依存 等・・

そのため、例えば外部ストレージ(オブジェクトストレージやファイルシステム)に保存し、データベースにはメタデータのみ格納する設計をすることもあります。
もしくは、Amazon S3などであれば直接URLの呼び出しができ、システム自体に負荷がかかりません。

参考:#25 Springでファイルのアップロードを行う #Java – Qiita

練習問題

問1: 上述の機能を実装し、挙動を確認してください。

問2: 以下のバリデーションをFormクラスに追加してください。
・ファイルが添付されていることを確認する
・ファイルサイズが200kB以下であることを確認する
・拡張子チェック

問3: DBに保存する機能で、受け取ったファイル名の後ろに日付を足して保存するように改変してください。
(image.pngであれば「image2025-03-28.png」)

練習問題のヒント
問2
テキストアップロードの単元の内容を用います。

問3
・ファイル名と拡張子をそれぞれ取り出すには、StringのsplitメソッドやrepalceAllメソッド等でも可能です。
(正規表現で「.」を表すにはエスケープが必要です:”\\.”)

タイトルとURLをコピーしました