Spring Boot でファイルダウンロード API を 302 リダイレクトで実装する方法
Web サービスでファイルをダウンロードさせる場合、通常は ResponseEntity<Resource> にファイルの内容を詰めて返します
しかし、ファイルを別の場所(CDN や外部ストレージなど)でホスティングしている場合は、リダイレクトを使ってクライアントをその URL に誘導することができます
HTTP の 302 ステータスコードはリソースが一時的に移動したことを示し、レスポンスの Location ヘッダーに新しい URL を指定するとブラウザが自動的に再リクエストします
この記事では、Spring Boot を使用してファイルのダウンロード要求に対して 302 リダイレクトを返す API を実装する手順を紹介します
ファイルの保存場所はサーバー内でも外部サービスでも構いません
以下の例では、サーバー上の任意のディレクトリに保存されているファイルをダウンロード用 URL にリダイレクトする実装を示します
302 リダイレクトの概要
HTTP 302 は「一時的な移動」を意味し、ブラウザや HTTP クライアントは Location ヘッダーに指定された URL へ自動的にリクエストを出し直します
この挙動は Spring MVC でもサポートされています
ビュー名を redirect: プレフィックスで返すと、UrlBasedViewResolver が 302 ステータスと Location ヘッダーを含むレスポンスを生成してくれます
また、ResponseEntity と HttpStatus.FOUND(302)を明示的に使って Location ヘッダーを設定することもできます
302 リダイレクトを使う場面の例
- 大きなファイルを別サーバーに配置している場合
- アプリケーションサーバーはダウンロード処理を担当せず、CDN やオブジェクトストレージへの URL に誘導します
- ダウンロード回数を記録したい場合
- 最初のリクエストを受けた際にログを残し、その後リダイレクトすることで統計を取得できます
- 認証/認可チェック後にアクセス先を変更する場合
- 権限に応じて別のファイルにリダイレクトするなど
Spring Boot プロジェクトの準備
ファイルダウンロード API を作るには Spring Web 機能が必要です
Spring Boot では Maven や Gradle など複数のビルドツールが利用できます
ここではそれぞれの設定例を紹介します
Maven での準備
従来どおり Maven プロジェクトであれば、spring-boot-starter-web の依存関係を追加するだけで Web アプリケーションが構築できます
pom.xml への追加例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
この依存関係を追加したらアプリケーションを構築し、@SpringBootApplication を付けたエントリポイントクラスを用意します
Gradle での準備
Gradle プロジェクトでは、java、org.springframework.boot、io.spring.dependency-management の各プラグインを有効にした build.gradle ファイルを作成します
Spring Boot の公式チュートリアルでは、build.gradle でプラグインと Java ツールチェイン、リポジトリを次のように設定する例が示されています
plugins {
id 'java'
// Spring Boot Gradle プラグインを適用
id 'org.springframework.boot' version '4.0.5'
// Spring Boot の BOM による依存関係管理を有効にするプラグイン
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
// Java 17 のツールチェインを使用する設定(例)
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
// Web アプリケーション用スターター。Spring Boot 3 系では spring-boot-starter-web、
// Spring Boot 4 では web starter がモジュール化され spring-boot-starter-webmvc になります【861355298915293†L588-L614】。
implementation 'org.springframework.boot:spring-boot-starter-web'
// テスト用スターター
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
上記のように plugins ブロックで Spring Boot プラグインと依存関係管理プラグインを宣言し、java ブロックでツールチェインを指定することで、Gradle でも簡単に Spring Boot アプリケーションを構築できます
依存関係に spring-boot-starter-web(または Spring Boot 4 の場合は spring-boot-starter-webmvc)を追加することで、Web サーバーと REST 機能が有効になります
テスト用には spring-boot-starter-test を追加し、tasks.named('test') で JUnit Platform を利用する設定を行います
コントローラーの実装
以下に示す FileRedirectController は、要求されたファイル名が存在するかどうかを確認し、ダウンロード用 URL を生成して 302 リダイレクトを返すコントローラーです
package com.example.fileapi.controller;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
@RestController
@RequestMapping("/files")
public class FileRedirectController {
// ダウンロード対象ファイルが保存されているディレクトリ
private final Path storageDir = Paths.get("/var/www/downloads");
/**
* ファイルの所在にリダイレクトするエンドポイント。
* ファイルが存在しない場合は 404 を返します。
*/
@GetMapping("/{fileName:.+}")
public ResponseEntity<Void> downloadFile(@PathVariable String fileName) throws Exception {
// ファイルの存在確認
Path filePath = storageDir.resolve(fileName).normalize();
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
// 実際のファイル配信を担当するエンドポイント(または外部URL)を作成
String fileDownloadUrl = ServletUriComponentsBuilder
.fromCurrentContextPath()
.path("/files/download/")
.path(fileName)
.toUriString();
// Location ヘッダーにダウンロード URL を設定し 302 ステータスで返却
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(fileDownloadUrl));
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
/**
* 実際にファイル内容を返すエンドポイント。
* ここでは単純にファイルをストリームとして返しています。
*/
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<org.springframework.core.io.Resource> serveFile(@PathVariable String fileName) throws Exception {
Path filePath = storageDir.resolve(fileName).normalize();
org.springframework.core.io.Resource resource = new org.springframework.core.io.UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
このコントローラーでは、ファイル名をパスパラメータとして受け取ってファイルの存在を確認し、存在する場合はダウンロード専用エンドポイント /files/download/{fileName} にリダイレクトします
ブラウザは Location ヘッダーに記載された URL に自動的にアクセスし、serveFile メソッドでファイルがストリームとして返されるため、ユーザーは通常のダウンロードダイアログでファイルを保存できます
重要なのは、ResponseEntity<Void> を使って Location ヘッダーを設定し、ステータスコードに HttpStatus.FOUND(302)を指定することです
この方法によって、HTTP レスポンスが 302 となりクライアントがリダイレクトを処理します
プレフィックスを利用した別の書き方
Spring MVC では、文字列でビュー名を返すとビュー解決が行われます
返却するビュー名の先頭に redirect: を付けると、UrlBasedViewResolver がこの名前を特別扱いし、残りの文字列を Location ヘッダーに設定して 302 リダイレクトを返します
単純なリダイレクトなら以下のように記述することもできます
@GetMapping("/download/{fileName:.+}")
public String redirectDownload(@PathVariable String fileName) {
// /files/actual/ にファイルを配信するエンドポイントがあると仮定
return "redirect:/files/actual/" + fileName;
}
この場合、返却値が “redirect:/files/actual/{ファイル名}” なので、Spring MVC が自動的に 302 ステータスと Location ヘッダーを付けてレスポンスを返してくれます
ResponseEntity を使用するよりも記述量は少なくなりますが、ヘッダーの細かい制御はできません
まとめ
- HTTP 302 は一時的なリダイレクトに用いられ、ブラウザは
Locationヘッダーに記載された URL へ自動的に移動します - Spring Boot では、
ResponseEntityとHttpStatus.FOUNDを使って明示的にリダイレクトレスポンスを生成する方法と、ビュー名の頭にredirect:を付けて返す方法の二つがあります - ファイルダウンロード API を実装する際、ファイルの配信処理を別エンドポイントに分け、ダウンロード URL への 302 リダイレクトで誘導する構成にすると、ファイルの保存場所を柔軟に変えたり、ダウンロード前にログを記録したりすることが容易になります
このように、Spring Boot でファイルダウンロード API を実装する際に 302 リダイレクトを活用すると、サーバーの責務を分離しつつユーザーにはスムーズなダウンロード体験を提供できます
ぜひ試してみてください
是非フォローしてください
最新の情報をお伝えします
