アプリ全体の目的と処理フロー

今回解説するアプリは、Spring Bootで画像アップロード機能を実装したサンプルです。

JavaやSpring Bootの研修を約3か月終えている方であれば、Controller、Thymeleaf、Model、GET・POST、フォーム送信といった基本は一度学んでいるはずです。

そこでこの記事では、Spring Bootの基礎説明を長くするのではなく、「画像アップロード処理がどのように流れているのか」に重点を置いて解説します。

今回のアプリの全体像は、次の通りです。

順番処理内容担当ファイル
1トップページにアクセスするImageUploadController.java
2画像アップロード画面を表示するupload.html
3画像ファイルを選択して送信するupload.html
4Controllerが画像ファイルを受け取るImageUploadController.java
5uploadsフォルダへ画像を保存するImageUploadController.java
6保存したファイル名を画面へ渡すImageUploadController.java
7成功画面で画像を表示するsuccess.html
8/images/ から uploads フォルダを参照できるようにするWebConfig.java

画像アップロード機能では、「保存する処理」と「表示する処理」を分けて考えることが大切です。

画像を保存するだけなら、ControllerでMultipartFileを受け取り、指定したフォルダに書き出せば実現できます。

しかし、保存した画像をブラウザで表示するには、別の設定が必要です。

たとえるなら、画像保存は「倉庫に荷物を入れる作業」です。一方、画像表示は「倉庫まで行ける道を作る作業」です。

倉庫に荷物を置いただけでは、お客さんは中身を見られませんよね。画像ファイルも同じです。uploadsフォルダに保存しただけでは、ブラウザから直接表示できません。

今回のアプリでは、WebConfig.javaで「/images/ というURLにアクセスしたら、uploadsフォルダを見に行く」という設定を追加しています。

まず、設定ファイルの全文を見ておきましょう。

spring.application.name=0609

spring.thymeleaf.cache=false
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

spring.application.name は、アプリケーション名を指定しています。

spring.thymeleaf.cache=false は、Thymeleafテンプレートのキャッシュを無効にする設定です。開発中にHTMLを修正したとき、変更内容を確認しやすくなります。

spring.servlet.multipart.max-file-size=10MB は、アップロードできる1ファイルあたりの最大サイズです。

spring.servlet.multipart.max-request-size=10MB は、リクエスト全体の最大サイズです。

画像アップロードでは、ファイルサイズ制限が重要です!

制限がないと、大きすぎるファイルを送られたときにサーバーへ負荷がかかります。水筒に入る量が決まっているように、Webアプリでも受け取れるデータ量を決めておく必要があります。

画像アップロード画面の作り方

画像アップロード画面は、upload.htmlで作られています。

このファイルでは、ユーザーが画像を選択し、送信ボタンを押せるようにしています。

まずは、upload.htmlの全文を見てください。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>画像アップロード</title>
</head>
<body>
    <h2>画像アップロードサンプル</h2>
    <p th:if="${message}" th:text="${message}" style="color: red;"></p>
    <form th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <input type="file" name="imageFile" accept="image/*">
        <button type="submit">送信</button>
    </form>
</body>
</html>

この画面で一番重要なのは、formタグです。

<form th:action="@{/upload}" method="post" enctype="multipart/form-data">

th:action="@{/upload}" は、フォームの送信先を /upload にする指定です。

method="post" は、HTTPのPOSTメソッドで送信する指定です。

画像ファイルをサーバー側へ保存する処理なので、GETではなくPOSTを使っています。

そして、特に重要なのが enctype="multipart/form-data" です。

enctype は、フォームデータをどの形式で送るかを指定する属性です。

通常のテキストだけなら特別な指定がなくても送れます。しかし、画像のようなファイルを送る場合は multipart/form-data が必要です。

たとえるなら、普通の文字だけのフォーム送信は「はがき」を送るようなものです。一方、画像ファイルの送信は「封筒に写真を入れて送る」ようなものです。

写真を送るなら、はがきではなく封筒が必要ですよね。

multipart/form-data は、ファイルを送るための封筒のような役割を持っています。

次に、ファイル選択欄です。

<input type="file" name="imageFile" accept="image/*">

type="file" によって、ブラウザにファイル選択ボタンが表示されます。

name="imageFile" は、Controller側でファイルを受け取るときの名前です。

Controller側では、次のように受け取ります。

@RequestParam("imageFile") MultipartFile imageFile

HTML側の name と、Java側の @RequestParam の名前が一致している点が重要です。

HTML側Java側
name="imageFile"@RequestParam("imageFile")

名前が一致していないと、Springはどのデータをどの引数に入れればよいのか判断できません。

荷物に宛名が書かれていないと、配達員が届け先に迷ってしまいますよね。同じように、フォームのname属性は「このデータは誰宛てか」を示す大切な名前です。

accept="image/*" は、画像ファイルを選びやすくする指定です。

ただし、accept属性はブラウザ側の入力補助です。サーバー側の厳密なチェックではありません。

つまり、accept="image/*" を書いたからといって、画像以外のファイルを完全に防げるわけではありません。実務では、Controller側でも拡張子やMIMEタイプを確認する必要があります。

エラーメッセージを表示しているのは、次の部分です。

<p th:if="${message}" th:text="${message}" style="color: red;"></p>

th:if は、条件を満たす場合だけHTML要素を表示するThymeleafの属性です。

message が存在するときだけ、pタグが表示されます。

th:text は、指定した値を画面上に表示する属性です。

Controller側で次のように値を渡すと、画面にメッセージが表示されます。

model.addAttribute("message", "ファイルが選択されていません。");

Modelは、ControllerからViewへデータを渡す入れ物です。

たとえるなら、ControllerからHTMLテンプレートへ渡す「連絡メモ」のようなものです。

Controllerで画像を受け取る処理

画像アップロード処理の中心になるファイルが、ImageUploadController.javaです。

このファイルでは、画面表示、ファイル受け取り、保存処理、成功画面への遷移まで行っています。

まずは、ImageUploadController.javaの全文を見てみましょう。

package com.example.demo;

import java.io.File;
import java.io.IOException;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class ImageUploadController {

    private final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/";

    @GetMapping("/")
    public String index() {
    	System.out.println("hello");
        return "upload";
    }

    @PostMapping("/upload")
    public String uploadImage(@RequestParam("imageFile") MultipartFile imageFile, Model model) {
        if (imageFile.isEmpty()) {
            model.addAttribute("message", "ファイルが選択されていません。");
            return "upload";
        }
        try {
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            String fileName = imageFile.getOriginalFilename();
            String savePath = UPLOAD_DIR + fileName;
            File destination = new File(savePath);
            imageFile.transferTo(destination);
            model.addAttribute("fileName", fileName);
        } catch (IOException exception) {
            exception.printStackTrace();
            model.addAttribute("message", "アップロードに失敗しました。");
            return "upload";
        }
        return "success";
    }
}

このControllerには、大きく分けて2つのメソッドがありますが画像ファイルを受け取る以下のメソッドがポイントです。

@PostMapping("/upload")
public String uploadImage(@RequestParam("imageFile") MultipartFile imageFile, Model model) {

@PostMapping("/upload") は、/upload にPOSTリクエストが送られたときに呼び出されます。

upload.html側では、次のように送信先を指定していました。

<form th:action="@{/upload}" method="post" enctype="multipart/form-data">

つまり、upload.htmlのフォーム送信と、ImageUploadController.javaの@PostMapping("/upload") がつながっています。

@RequestParam("imageFile") は、リクエストから imageFile という名前のデータを取り出す指定です。

MultipartFile は、Spring MVCでアップロードファイルを扱うためのインターフェースです。

MultipartFileを使うと、次のような操作ができます。

メソッド役割
isEmptyファイルが空かどうかを確認する
getOriginalFilenameアップロードされた元のファイル名を取得する
transferToファイルを指定した場所へ保存する
getSizeファイルサイズを取得する
getContentTypeMIMEタイプを取得する

ファイル未選択時の処理はこちらです。

        if (imageFile.isEmpty()) {
            model.addAttribute("message", "ファイルが選択されていません。");
            return "upload";
        }

isEmpty は、アップロードされたファイルが空かどうかを確認します。

ファイルが選択されていない場合、message をModelに入れて upload.html を再表示します。

失敗したときに、ただエラーにするのではなく、ユーザーへ理由を伝える設計になっているわけですね。

研修後の段階では、成功時だけでなく失敗時の処理にも注目してください。

プログラムの品質は、エラーが起きたときに見えます!

ファイル保存処理の中身

もう一度、保存処理に関係する部分を中心に見てみましょう。

        try {
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            String fileName = imageFile.getOriginalFilename();
            String savePath = UPLOAD_DIR + fileName;
            File destination = new File(savePath);
            imageFile.transferTo(destination);
            model.addAttribute("fileName", fileName);
        } catch (IOException exception) {
            exception.printStackTrace();
            model.addAttribute("message", "アップロードに失敗しました。");
            return "upload";
        

まず、保存先のパスは次の定数で定義されています。

private final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/";

System.getProperty("user.dir") は、アプリケーションを実行している現在のディレクトリを取得します。

たとえば、プロジェクトを次の場所で実行しているとします。

/Users/sample/workspace/0609

その場合、UPLOAD_DIR は次のようになります。

/Users/sample/workspace/0609/uploads/

つまり、今回のアプリでは、プロジェクト直下に uploads フォルダを作成し、その中に画像を保存します。

次に、保存先フォルダを表すFileオブジェクトを作っています。

File uploadDir = new File(UPLOAD_DIR);

Fileクラスは、Javaでファイルやディレクトリを扱うためのクラスです。

名前はFileですが、ファイルだけでなくフォルダも表現できます。

次に、uploadsフォルダが存在するかどうかを確認しています。

            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

exists は、対象のファイルやフォルダが存在するかを確認するメソッドです。

存在しない場合は、mkdirs でフォルダを作成します。

mkdirs は、途中のフォルダも含めて作成できるメソッドです。

mkdir と mkdirs は似ていますが、少し違います。

メソッド特徴
mkdir指定したフォルダだけ作成する
mkdirs親フォルダも含めて作成する

たとえるなら、mkdir は「部室だけ用意する」イメージです。

mkdirs は「校舎、廊下、部室までまとめて使える状態にする」イメージです。

次に、アップロードされた元のファイル名を取得しています。

String fileName = imageFile.getOriginalFilename();

getOriginalFilename は、ユーザーがアップロードしたファイルの元の名前を取得します。

たとえば、ユーザーが touchtype.jpg という画像をアップロードした場合、fileName には touchtype.jpg が入ります。

その後、保存先パスを組み立てています。

String savePath = UPLOAD_DIR + fileName;
File destination = new File(savePath);

UPLOAD_DIR が /Users/sample/workspace/0609/uploads/ で、fileName が touchtype.jpg なら、savePath は次のようになります。

/Users/sample/workspace/0609/uploads/touchtype.jpg

最後に、transferToでファイルを保存しています。

imageFile.transferTo(destination);

transferTo は、MultipartFileの中身を指定した保存先へ書き出すメソッドです。

つまり、ブラウザから送られてきた画像データを、サーバー側のuploadsフォルダへ実体ファイルとして保存しています。

保存後は、ファイル名をModelへ追加しています。

model.addAttribute("fileName", fileName);

このfileNameは、success.htmlで画像を表示するときに使われます。

ここで大切なのは、保存先パスと表示用URLは別物だという点です。

種類
実際の保存場所/Users/sample/workspace/0609/uploads/touchtype.jpg
ブラウザから見るURL/images/touchtype.jpg

ファイルを保存しただけでは、ブラウザから表示できません。

保存した画像を表示するためには、/images/touchtype.jpg というURLで uploads/touchtype.jpg を見に行けるようにする必要があります。

この対応付けを行うのが、次章で解説するWebConfig.javaです。

第7章 保存した画像をブラウザで表示する仕組み

画像をuploadsフォルダに保存しても、そのままではブラウザから表示できません。

Spring Bootが自動で公開する静的リソースフォルダではないからです。

Spring Bootで標準的に公開される静的リソースの置き場所には、次のようなものがあります。

フォルダ用途
src/main/resources/staticCSS、JavaScript、画像など
src/main/resources/public公開用の静的ファイル
src/main/resources/resources静的リソース
src/main/resources/META-INF/resourcesWebJarsなどで使われることがあります

今回のuploadsフォルダは、プロジェクト直下に作成されます。

そのため、WebConfig.javaでURLと実際の保存場所を対応づけています。

まず、WebConfig.javaの全文を見てみましょう。

package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String uploadDir = System.getProperty("user.dir") + "/uploads/";
        registry.addResourceHandler("/images/**")
                .addResourceLocations("file:" + uploadDir);
    }
}

@Configuration は、このクラスがSpringの設定クラスであることを示すアノテーションです。

Controllerではなく、Spring MVCの動きをカスタマイズするためのクラスですね。

WebMvcConfigurer は、Spring MVCの設定を追加・変更するためのインターフェースです。

今回のように、独自の静的リソース公開設定を追加したい場合に使います。

重要なのは、次の部分です。

registry.addResourceHandler("/images/**")
.addResourceLocations("file:" + uploadDir);

addResourceHandler("/images/**") は、ブラウザから /images/ で始まるURLにアクセスされたときのルールを追加しています。

/images/** の ** は、後ろに続く任意のパスを表します。

たとえば、次のようなURLが対象になります。

/images/touchtype.jpg
/images/sample.png
/images/profile/user01.jpg

addResourceLocations("file:" + uploadDir) は、実際にどのフォルダからファイルを探すかを指定しています。

file: を付けている点が大切です。

file: は、クラスパス上ではなく、ファイルシステム上の場所を参照する指定です。

たとえば、uploadDir が次の値だったとします。

/Users/sample/workspace/0609/uploads/

この場合、ブラウザから /images/touchtype.jpg にアクセスすると、Spring MVCは次のファイルを探します。

/Users/sample/workspace/0609/uploads/touchtype.jpg

つまり、対応関係は次のようになります。

ブラウザのURL実際の保存場所
/images/touchtype.jpgプロジェクト直下の uploads/touchtype.jpg
/images/sample.pngプロジェクト直下の uploads/sample.png
/images/profile.jpgプロジェクト直下の uploads/profile.jpg

次に、成功画面であるsuccess.htmlの全文を見てみましょう。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>アップロード完了</title>
</head>
<body>
    <h2>画像のアップロードに成功しました</h2>
    <p>表示確認:</p>
    <img th:src="@{'/images/' + ${fileName}}" alt="アップロード画像" style="max-width: 500px;">
    <br><br>
    <a href="/">戻る</a>
</body>
</html>

画像表示で重要なのは、次の部分です。

<img th:src="@{'/images/' + ${fileName}}" alt="アップロード画像" style="max-width: 500px;">

th:src は、Thymeleafで画像のsrc属性を動的に作るための属性です。

Controller側では、次のようにfileNameをModelへ追加していました。

model.addAttribute("fileName", fileName);

たとえば、fileName が touchtype.jpg だった場合、Thymeleafは次のようなHTMLを生成します。

<img src="/images/touchtype.jpg" alt="アップロード画像" style="max-width: 500px;">

そして、/images/touchtype.jpg へのアクセスは、WebConfig.javaによって uploads/touchtype.jpg に対応づけられます。

この流れを整理すると、次のようになります。

担当処理
ImageUploadController.javafileNameをModelに入れる
success.html/images/ + fileName で画像URLを作る
WebConfig.java/images/** をuploadsフォルダに対応づける
ブラウザ生成されたURLへアクセスして画像を表示する

画像アップロード機能で詰まりやすいポイントは、保存処理よりも表示設定です。

「ファイルはuploadsフォルダにあるのに、画面に表示されない!」

そんなときは、次の3点を確認してください。

確認ポイント内容
保存先uploadsフォルダに画像が存在するか
画像URL/images/ファイル名 の形になっているか
ResourceHandler/images/** とuploadsフォルダが対応しているか

保存、URL生成、公開設定。

この3つがそろって初めて、ブラウザ上に画像が表示されます。

第8章 実務で使うための改善ポイント

今回のコードは、画像アップロードの基本を理解するには十分です。

ただし、実務でそのまま使うには注意が必要です。

ファイルアップロード機能は、ユーザーが任意のファイルをサーバーへ送れる機能だからです。

たとえるなら、家の玄関に「荷物を自由に置いてください」と書くようなものです。きれいな写真だけが届くとは限りません。大きすぎるファイル、画像ではないファイル、危険な名前のファイルが届く可能性もあります。

主な改善ポイントは、次の通りです。

改善ポイント目的
ファイル名をUUIDにする同名ファイルによる上書きを防ぐ
拡張子をチェックする許可した画像形式だけ保存する
MIMEタイプを確認するファイル種別を確認する
保存先を設定ファイルに出す環境ごとに保存場所を変えやすくする
Pathを使ってパスを組み立てるOS差異やパス連結ミスを減らす
例外処理を整理する障害調査とユーザー通知を分けやすくする

実務では、さらに次の観点も必要になります。

観点内容
認証誰でもアップロードできてよいのか
認可他人の画像を見られてよいのか
容量制限ユーザーごとの保存容量を制限するか
ファイル削除不要になった画像をどう削除するか
ログ誰がいつ何をアップロードしたか記録するか
保存先ローカル保存か、クラウドストレージか
公開範囲画像URLを知っていれば誰でも見られる状態でよいのか

しかし、今回は研修中ですのでそこまでの配慮は不要です。

今回のサンプルは、画像アップロードの基本構造を学ぶには十分です。

まずは、今回のコードで「アップロード、保存、表示」の流れを確実に理解してください。

最後までお読みいただきありがとうございます。