Spring MVCでファイルアップロードを実装!初心者でもわかるMultipartFileの使い方
新人
「先輩、JavaのSpring Bootを使って、ユーザーが自分のパソコンにある画像やファイルをサーバーに送る機能をサイトに作りたんですけど、どうすればいいですか?」
先輩
「それは『ファイルアップロード』という機能だね。Spring MVCという仕組みを使えば、意外と簡単に実装できるよ。」
新人
「ファイルって、普通のテキストデータとは送り方が違うんですか?」
先輩
「そうだね。バイナリデータという形式で送る必要があるんだ。Springでは『MultipartFile』という便利なインターフェースが用意されているから、まずはその基本構造から学んでいこう!」
1. Spring MVCでファイルアップロードを実装する基本構造
JavaのWeb開発フレームワークであるSpring Boot(Spring MVC)を使って、ブラウザからサーバーへファイルを送信する機能を実装する方法を解説します。ファイルアップロードは、SNSのプロフィール画像設定や、ドキュメントの共有システムなど、多くのWebアプリで必須となる機能です。
まず、全体的な流れを把握しましょう。ファイルアップロードを実現するには、大きく分けて「HTML(画面)」と「Controller(サーバー側の処理)」の2つを準備する必要があります。
画面側の準備(HTML/JSP)
HTMLでファイルを送信する際には、通常の入力フォームとは異なる設定が2つ必要です。
- 1. formタグのメソッドを method="POST" にする。
- 2. enctype="multipart/form-data" という属性を追加する。
- 3. input type="file" を使用してファイル選択ボタンを作る。
この「enctype(エンクタイプ)」という属性は、データの種類をブラウザに教えるためのものです。これがないと、ファイルの中身ではなく「ファイル名」という単なる文字だけがサーバーに届いてしまいます。
サーバー側の準備(Java)
サーバー側では、コントローラーのメソッドの引数に MultipartFile を指定します。Springが自動的に、送られてきたファイルの情報をこの変数の中に詰め込んでくれます。
それでは、具体的なプログラムコードを見ていきましょう。まずは、ファイルを1つだけアップロードするシンプルな例です。
<!-- upload.jsp (送信画面) -->
<form action="/upload" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="fileInput" class="form-label">アップロードするファイルを選択してください</label>
<input type="file" name="uploadFile" id="fileInput" class="form-control">
</div>
<button type="submit" class="btn btn-primary">送信</button>
</form>
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.ui.Model;
@Controller
public class FileUploadController {
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("uploadFile") MultipartFile file, Model model) {
// ファイルが空でないかチェック
if (file.isEmpty()) {
model.addAttribute("message", "ファイルを選択してください。");
return "result";
}
// ファイル名を取得
String fileName = file.getOriginalFilename();
model.addAttribute("message", fileName + " のアップロードに成功しました!");
return "result";
}
}
このコードでは、HTMLの input name="uploadFile" と、Java側の @RequestParam("uploadFile") の名前を一致させることが非常に重要です。これが一致していないと、Springはどのデータを変数に入れればいいか分からず、エラーになってしまいます。
2. MultipartFileインターフェースの役割と仕組み
ここで、プログラムの中で登場した MultipartFile というものについて詳しく解説します。プログラミングに慣れていない方にとって、「インターフェース」という言葉は難しく感じるかもしれませんが、ここでは「便利な道具箱」だと考えてください。
MultipartFile は、アップロードされたファイルの内容(中身)だけでなく、そのファイルに関する色々な情報を取り出すための機能を備えています。例えば、以下のような情報を取得できます。
| メソッド名 | 役割(何ができるか) |
|---|---|
getOriginalFilename() |
ユーザーのパソコン上での元のファイル名(例:photo.jpg)を取得します。 |
getContentType() |
ファイルの種類(MIMEタイプ)を確認します(例:image/png など)。 |
getSize() |
ファイルの大きさをバイト(byte)単位で取得します。 |
getBytes() |
ファイルの中身をデータとして直接取り出します。 |
isEmpty() |
ファイルが空かどうか、または選択されていないかを判定します。 |
transferTo(File dest) |
指定した保存先にファイルを書き込みます。 |
例えば、セキュリティ対策として「画像ファイル以外は受け付けたくない」という場合は、getContentType() を使ってチェックを行います。また、「ファイルサイズが大きすぎてサーバーの容量を圧迫しないか」を getSize() で確認することもできます。
次に、複数のファイルを一度にアップロードする場合の応用的なコードを見てみましょう。リスト(List)を使って複数のファイルを一括で受け取ることができます。
@PostMapping("/multi-upload")
public String handleMultiUpload(@RequestParam("files") MultipartFile[] files, Model model) {
int count = 0;
StringBuilder fileNames = new StringBuilder();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
// 各ファイルの情報を処理
fileNames.append(file.getOriginalFilename()).append(", ");
count++;
}
}
model.addAttribute("message", count + "個のファイルを処理しました: " + fileNames.toString());
return "result";
}
この MultipartFile[] (配列)という書き方を使うことで、1つのボタンで複数の画像を選択して送ってきた場合でも、ループ処理(繰り返し処理)を使って1つずつ順番に保存やチェックを行うことが可能になります。
3. application.propertiesでのアップロードサイズ制限の設定
実は、Spring Bootには標準で「一度に送れるファイルのサイズ制限」がかかっています。デフォルトでは、1つのファイルが1MB、1回のリクエスト全体で10MBまでという非常に小さな値に設定されています。
最近のスマートフォンで撮影した写真は1枚で5MBを超えることも珍しくありません。そのため、初期設定のままだと MaxUploadSizeExceededException というエラーが発生し、アップロードに失敗してしまいます。
この制限を変更するには、プロジェクトの設定ファイルである application.properties に設定を書き加えます。このファイルは、アプリ全体のルールを決めるための「設定ノート」のような役割をしています。
設定キーワード
設定する項目は主に2つあります。
spring.servlet.multipart.max-file-size: 1ファイルあたりの最大サイズ
spring.servlet.multipart.max-request-size: 1回のアクション(複数のファイル合計)での最大サイズ
具体的な記述例は以下の通りです。単位は「KB(キロバイト)」「MB(メガバイト)」「GB(ギガバイト)」などが使えます。
# 1ファイルの上限を10MBに設定
spring.servlet.multipart.max-file-size=10MB
# 1回のリクエスト(合計)の上限を50MBに設定
spring.servlet.multipart.max-request-size=50MB
なぜこのような制限があるのでしょうか?それは「嫌がらせ(サイバー攻撃)」を防ぐためです。もし制限がなければ、悪意のあるユーザーが何百GBという巨大なファイルを送り続け、サーバーのハードディスクをパンクさせてしまうかもしれません。自分のアプリが扱うデータの種類に合わせて、適切なサイズを検討しましょう。
また、ファイルのアップロード場所についても注意が必要です。プログラムの内部に保存してしまうと、アプリを更新して再起動した際にファイルが消えてしまうことがあります。実務では、サーバー内の特定のフォルダを指定するか、クラウドストレージ(AWS S3など)に保存するのが一般的です。
プログラミング未経験の方は、まずは自分のパソコンの中でファイルが移動し、名前が表示されるところまでを確認してみてください。それができれば、世界中のユーザーから写真や動画を受け取るアプリの第一歩を踏み出したことになります!
4. Thymeleafでマルチパートフォーム(enctype)を作成する方法
Spring BootでのWeb開発において、現在最も主流なテンプレートエンジンが Thymeleaf(タイムリーフ) です。前章では一般的なHTML構造を学びましたが、ここではThymeleaf独自の書き方(th属性)を用いた、より実践的なファイルアップロードフォームの構築方法を詳しく解説します。
ファイルアップロードを実現するために最も重要なポイントは、formタグに対して enctype="multipart/form-data" を明示的に指定することです。この属性を指定しない場合、ブラウザはファイルの実体(バイナリデータ)を送信せず、ファイル名という単なる文字列のみを送信してしまいます。サーバー側でファイルを受け取るためには、この「マルチパート」形式での送信が不可欠です。
Thymeleafによる実装例
以下に、Thymeleafを使用した標準的なアップロード画面のコードを示します。Bootstrap 5のクラスを適用して、見た目も整えた状態にしています。
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="bi bi-cloud-upload"></i> ファイル選択</h4>
</div>
<div class="card-body">
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="formFile" class="form-label">アップロードする画像を選択</label>
<input class="form-control" type="file" id="formFile" name="uploadFile" accept="image/*">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">サーバーへ送信</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
このコードにおける重要な要素を整理します。まず、th:action="@{/upload}" です。Thymeleafのリンク式を使うことで、アプリケーションのコンテキストパスを考慮した正しいURLが生成されます。次に、method="post" です。ファイルデータは容量が大きくなるため、URLにデータを含めるGETメソッドではなく、必ずPOSTメソッドを使用します。
そして、最も肝心なのが enctype="multipart/form-data" です。これがあることで、HTTPリクエストのボディ部分が複数のパート(マルチパート)に分割され、テキストデータとバイナリデータが混在した状態で正しくサーバーに届けられます。また、input タグに accept="image/*" を付与することで、ブラウザのファイル選択ダイアログで画像ファイルのみを表示させるという、ユーザー利便性を高める工夫も行っています。
さらに、Thymeleafではオブジェクトとフォームをバインドさせる th:object を併用することもありますが、単純なファイルアップロードの場合は、上記のように name="uploadFile" という属性名を指定し、コントローラー側でその名前を受け取る形が最もシンプルで理解しやすい構成となります。
5. Controllerでのファイル受信処理と保存先ディレクトリの指定
画面から送信されたファイルは、サーバー側の Controller(コントローラー) で受け取ります。Spring MVCでは、引数に MultipartFile を定義するだけで、複雑な解析処理をフレームワークが肩代わりしてくれます。しかし、受け取っただけではファイルはメモリ上や一時ディレクトリに存在するだけなので、永続的に保存するためにはサーバー内の特定のフォルダに書き出す処理が必要です。
保存処理の実装ポイント
ファイルを保存する際には、以下のステップが必要になります。
- 1. 保存先の絶対パスを決定する。
- 2. 保存先ディレクトリが存在しない場合は作成する。
- 3.
transferToメソッドを使用して、ファイルを物理的なディスクに書き込む。
以下のサンプルコードでは、プロジェクトの実行ルート直下に「upload-dir」という名前のフォルダを作成し、そこにファイルを保存する一連の流れを記述しています。
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.ui.Model;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileSaveController {
// 保存先ディレクトリのパス(相対パスを絶対パスに変換)
private final String UPLOAD_DIR = "upload-dir/";
@PostMapping("/upload")
public String uploadFile(@RequestParam("uploadFile") MultipartFile file, Model model) {
if (file.isEmpty()) {
model.addAttribute("error", "ファイルが選択されていません。");
return "upload-view";
}
try {
// 1. 保存先のディレクトリを準備
File directory = new File(UPLOAD_DIR);
if (!directory.exists()) {
directory.mkdirs(); // フォルダがなければ作成
}
// 2. 元のファイル名を取得してパスを生成
String fileName = file.getOriginalFilename();
Path path = Paths.get(UPLOAD_DIR + fileName);
// 3. ファイルを保存先に書き出し
file.transferTo(path.toFile());
model.addAttribute("message", "保存完了: " + fileName);
model.addAttribute("filePath", path.toAbsolutePath().toString());
} catch (IOException e) {
e.printStackTrace();
model.addAttribute("error", "システムエラーが発生しました。");
}
return "result";
}
}
実務上の注意点として、file.getOriginalFilename() をそのまま使うのは、セキュリティ面や運用面でリスクがあります。例えば、異なるユーザーが同じ「image.jpg」という名前のファイルをアップロードした場合、後から送られたファイルで上書きされてしまいます。これを防ぐためには、ファイル名の先頭にタイムスタンプを付与したり、UUID(重複しないランダムな文字列)を生成してファイル名に設定する手法が一般的です。
また、保存先をプログラム内の「src/main/resources/static」の中に設定したくなるかもしれませんが、これは推奨されません。Spring BootアプリがJAR形式でパッケージ化されると、その内部は読み取り専用になり、動的にファイルを書き込むことができなくなるからです。必ずサーバーのOS上の独立したディレクトリ(例:/var/app/uploads など)を指定するようにしましょう。
保存ディレクトリのパス設定は、プログラム内に直接書く(ハードコーディングする)のではなく、application.properties から読み込むようにすると、開発環境と本番環境で保存先を切り替えやすくなり、よりプロフェッショナルなコードになります。
6. エラーハンドリング:ファイル未選択やサイズ超過への対応
ファイルアップロード機能において、正常系の処理と同じくらい重要なのが エラーハンドリング です。ユーザーは常に開発者が想定した通りの操作をしてくれるとは限りません。ファイルを選択せずに送信ボタンを押したり、スマートフォンの超高画質な動画ファイル(数百MB)をいきなり送ってきたりすることもあります。
未選択チェックと空ファイル対策
最も頻繁に発生するのが「ファイルの未選択」です。前述の MultipartFile オブジェクトには isEmpty() メソッドが用意されています。これを使用することで、ファイルが選択されていない、あるいは中身が0バイトである場合に即座に検知し、適切なメッセージを画面に返すことができます。
ファイルサイズ超過(MaxUploadSizeExceededException)の制御
Spring Bootのデフォルト制限を超えたファイルが送信された場合、Controllerのメソッドが実行されるよりも前の段階で MaxUploadSizeExceededException という例外が発生します。この例外を適切にキャッチしないと、ユーザーにはブラウザ固有の無機質なエラー画面が表示されてしまい、不親切な印象を与えます。
これを解決するために、@ControllerAdvice を使ったグローバルなエラーハンドリングを導入しましょう。これにより、アプリ内のどこでサイズ超過が起きても、共通の綺麗なエラー画面を表示させることができます。
package com.example.demo.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@ControllerAdvice
public class GlobalExceptionHandler {
// ファイルサイズ上限を超えた時の処理
@ExceptionHandler(MaxUploadSizeExceededException.class)
public String handleMaxSizeException(MaxUploadSizeExceededException exc, RedirectAttributes redirectAttributes) {
// フラッシュスコープにエラーメッセージを格納
redirectAttributes.addFlashAttribute("error", "ファイルサイズが大きすぎます!上限は10MBです。");
// アップロード画面にリダイレクト
return "redirect:/upload-view";
}
}
この @ControllerAdvice は、アプリケーション全体の「見守り役」のような存在です。特定のコントローラーに依存せず、発生した例外を横取りして処理してくれます。ここでは RedirectAttributes を使用して、リダイレクト先の画面に一度だけ表示されるメッセージ(フラッシュ属性)を渡しています。これにより、ユーザーは「なぜ失敗したのか」を理解し、再度適切なサイズのファイルを選び直すことができます。
さらに高度な対策として、クライアント側(JavaScript)での事前チェックも有効です。送信ボタンが押された瞬間にファイルの size プロパティを確認し、上限を超えていれば通信を発生させずに警告を出すことで、サーバーの負荷を軽減しつつユーザー体験を向上させることができます。サーバー側とクライアント側の両面でガードを固めることが、堅牢なシステム開発の鍵となります。
最後に、ファイル形式のバリデーションについても触れておきます。getContentType() を利用して、「image/jpeg」や「image/png」のみを許可し、実行形式のファイル(.exe)などは拒否するように設定することはセキュリティ上極めて重要です。悪意のあるファイルがサーバー内で実行されるリスクを最小限に抑えるよう、常に意識しましょう。
7. 実装時に注意すべきセキュリティ対策(ファイル名や拡張子の検証)
ファイルアップロード機能を一般公開する際に、最も注意を払わなければならないのがセキュリティです。悪意のあるユーザーがサーバーを攻撃するための足がかりとして、不正なファイルを送り込む可能性があるからです。安全なアプリケーションを構築するために、開発者が実施すべき重要な検証処理について解説します。
ファイル名のサニタイジングと書き換え
ユーザーがアップロードしたファイル名をそのままサーバーのファイルシステムで使用することは非常に危険です。例えば、ファイル名に「../../etc/passwd」といった相対パスを含めることで、サーバー内の重要なシステムファイルを上書きしようとする「ディレクトリトラバーサル攻撃」を受けるリスクがあります。
この対策として、元のファイル名(OriginalFilename)は画面表示などの参照用にとどめ、サーバーに保存する際はシステム側で自動生成したユニークなIDを使用することを強く推奨します。Javaでは UUID クラスを使用することで、重複の可能性が極めて低いランダムな文字列を簡単に生成できます。
拡張子とコンテンツタイプの二重チェック
「画像のみ許可する」という仕様の場合、拡張子が「.jpg」や「.png」であることを確認するだけでは不十分です。悪意のあるユーザーは、中身が実行可能なプログラムであるにもかかわらず、拡張子だけを偽装して送信してくることがあります。そのため、拡張子のチェックに加えて、 MultipartFile の getContentType() メソッドによるMIMEタイプの確認を併用してください。
package com.example.demo.util;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class FileValidator {
// 許可する拡張子のリスト
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "gif");
// 許可するMIMEタイプのリスト
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList("image/jpeg", "image/png", "image/gif");
public static String getSafeFileName(MultipartFile file) throws IllegalArgumentException {
String originalName = file.getOriginalFilename();
if (originalName == null || !originalName.contains(".")) {
throw new IllegalArgumentException("不正なファイル形式です。");
}
// 1. 拡張子の抽出とチェック
String extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(extension)) {
throw new IllegalArgumentException("許可されていない拡張子です。");
}
// 2. MIMEタイプのチェック
String contentType = file.getContentType();
if (contentType == null || !ALLOWED_MIME_TYPES.contains(contentType)) {
throw new IllegalArgumentException("不正なコンテンツタイプです。");
}
// 3. 安全な新しいファイル名を生成(UUID + 拡張子)
return UUID.randomUUID().toString() + "." + extension;
}
}
このように、検証ロジックを独立させたユーティリティクラスを作成することで、コントローラー側のコードをスッキリと保ちつつ、堅牢なガードを構築できます。また、保存先のディレクトリについては、Webサーバーのドキュメントルート(外部から直接アクセスできる場所)の外に配置し、プログラムを介してのみアクセスを許可することで、アップロードされたプログラムを直接実行されるリスクを回避できます。
8. 複数ファイルの一括アップロードに対応する実装ポイント
実務においては、複数の写真をアルバムに一括登録したり、複数の添付資料を一度に送信したりする要件が頻繁に発生します。Spring MVCでは、単一のファイルアップロードを拡張する形で、配列やリストを用いて複数のファイルをスマートに扱うことができます。
HTML側での複数選択対応
HTMLの input type="file" タグに multiple 属性を追加するだけで、ブラウザのファイル選択ダイアログで複数のファイルを同時に選択できるようになります。この際、 name 属性は単一ファイルの場合と同じ名前で問題ありません。
<form th:action="@{/upload/multiple}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">複数のファイルを選択(Shiftキーで複数選択可)</label>
<input type="file" name="uploadFiles" class="form-control" multiple>
</div>
<button type="submit" class="btn btn-success">一括アップロード開始</button>
</form>
サーバー側でのループ処理と一括保存
サーバー側のコントローラーでは、引数を MultipartFile[] (配列)または List<MultipartFile> の形式で受け取ります。受け取った後は、拡張for文などを使用してループを回し、一つ一つのファイルに対してバリデーションと保存処理を実行します。
@PostMapping("/upload/multiple")
public String handleMultipleUpload(@RequestParam("uploadFiles") MultipartFile[] files, Model model) {
List<String> successMessages = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
continue; // 空のデータはスキップ
}
try {
// セキュリティチェック済みのファイル名を取得
String safeName = FileValidator.getSafeFileName(file);
Path targetPath = Paths.get("C:/uploads/").resolve(safeName);
// 物理保存
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
successMessages.add(file.getOriginalFilename() + " を保存しました。");
} catch (Exception e) {
errorMessages.add(file.getOriginalFilename() + " の保存に失敗しました: " + e.getMessage());
}
}
model.addAttribute("successList", successMessages);
model.addAttribute("errorList", errorMessages);
return "upload-result";
}
一括処理を行う際の注意点として、一部のファイルだけが保存に失敗した場合の挙動を検討しておく必要があります。全てをエラーにするのか、成功したものだけ保存して結果を表示するのか、ビジネス要件に合わせて実装してください。また、大量のファイルを一度に送るとサーバーのメモリを圧迫するため、リクエスト全体のサイズ制限(max-request-size)を application.properties で適切に調整することを忘れないようにしましょう。
9. Spring MVC + Thymeleaf アップロード実装の総まとめ
ここまで、Spring MVCとThymeleafを組み合わせたファイルアップロードの基礎から応用、そしてセキュリティ対策までを網羅してきました。ファイルアップロードは単にデータを送るだけの機能に見えますが、実際にはWeb技術の多くの要素が凝縮されています。
実装のステップを振り返ると、まずクライアント側ではHTMLのフォーム形式を正しく設定し、 multipart/form-data という形式でバイナリデータを送る準備を整えます。Thymeleafの便利な属性を活用することで、URL解決や動的なメッセージ表示が容易になります。サーバー側では、Spring MVCが提供する MultipartFile インターフェースをコントローラーの引数に設定するだけで、データの受け取りが完了します。この抽象化こそがSpringの強力な魅力であり、開発者は「データをどう受け取るか」ではなく「受け取ったデータをどう扱うか」というロジックに集中できるのです。
また、実務レベルの開発で不可欠なのが、設定ファイルによる制限の管理とエラーハンドリングです。 application.properties でサイズ上限を定め、 @ControllerAdvice で例外をキャッチしてユーザーに優しく通知する構造は、プロフェッショナルなアプリケーションの標準的な姿です。さらに、ファイル名のサニタイジングやMIMEタイプの検証といったセキュリティ対策を施すことで、初めて「公開しても恥ずかしくない、安全な機能」となります。
ファイルアップロード機能をマスターした後は、アップロードされた画像をリサイズしてサムネイルを作成したり、クラウドストレージであるAmazon S3やGoogle Cloud Storageにデータを転送したりといった、さらに高度なステップへ挑戦してみてください。Javaの NIO (New I/O) パッケージや外部ライブラリを組み合わせることで、ファイル操作の可能性は無限に広がります。まずはシンプルな一単一ファイルのアップロードを完璧に理解し、そこから徐々に機能を拡張していくことが、Webエンジニアとしての着実な成長への近道となるでしょう。この記事で学んだ知識を土台にして、ぜひ自分だけの便利なWebツールを開発してみてください。