ファイルアップロードは退屈に聞こえるかもしれませんが、本番環境で MultipartException: Failed to parse multipart servlet request のデバッグに午後を費やしたことのある人にはそうではありません。
Spring Boot では、ファイルアップロードには MultipartFile、MultipartAutoConfiguration、spring.servlet.multipart.* プロパティが関わり、サーブレットコンテナ上で動作しているという暗黙の前提があります。動作はしますが、多くの隠れた仕組みを抱えています。
Solon は異なるアプローチを取ります。コアクラスは UploadedFile で、余分な設定なしに Web レイヤーに存在し、マルチパート解析のタイミングと一時ファイルのクリーンアップを明示的に制御できます。
基本: UploadedFile
コントローラーメソッドに UploadedFile パラメータが含まれている場合、Solon は自動的にマルチパート解析をトリガーします。アノテーションは不要です。
@Controller
public class FileController {
@Post
@Mapping("/upload")
public String upload(UploadedFile file) {
try {
file.transferTo(new File("/storage/uploads/" + file.name));
return "uploaded: " + file.name + " (" + file.contentSize + " bytes)";
} finally {
file.delete(); // 必ずクリーンアップする
}
}
}
Enter fullscreen mode Exit fullscreen mode
最もよく使用する主なプロパティ:
| プロパティ | 説明 |
|---|---|
file.name |
拡張子を含む完全なファイル名 |
file.extension |
拡張子のみ(例: jpg) |
file.contentType |
MIME タイプ |
file.contentSize |
バイト単位のファイルサイズ |
file.content |
生の InputStream
|
file.contentAsBytes |
byte[] |
file.isEmpty() |
アップロードが空かどうか |
注意点: フレームワークは一時ファイルを自動的にクリーンアップしません。delete() を呼び出さない場合、それらのテンポラリファイルはディスクに残ります。上記の try/finally パターンはオプションではありません。
同じフィールド名での複数ファイル
クライアントが同じフィールド名で複数のファイルを送信する場合、UploadedFile[] を使用します(v2.3.8 以降でサポート):
@Post
@Mapping("/upload/batch")
public void uploadBatch(UploadedFile[] files) {
for (UploadedFile file : files) {
try {
file.transferTo(new File("/storage/" + file.name));
} finally {
file.delete();
}
}
}
Enter fullscreen mode Exit fullscreen mode
フィールド名が一致しない場合
フォームのフィールド名がパラメータ名と異なる場合は、@Param を使用します:
@Post
@Mapping("/avatar")
public void setAvatar(@Param("user_avatar") UploadedFile file) {
// フォームフィールド "user_avatar" を処理
}
Enter fullscreen mode Exit fullscreen mode
混合フォーム: ファイル + 通常フィールド
同じリクエストでファイルとテキストフィールドの両方を受信できます。パラメータインジェクションは両方を自動的に処理します:
@Post
@Mapping("/upload/with-meta")
public void uploadWithMeta(UploadedFile file, String description, int category) {
try {
// file: アップロードされたドキュメント
// description, category: プレーンテキストのフォームフィールド
saveFile(file, description, category);
} finally {
file.delete();
}
}
Enter fullscreen mode Exit fullscreen mode
UploadedFile パラメータがない場合
マルチパートフォームにファイル添付がなく、テキストフィールドのみが含まれる場合があります。その場合、Solon は自動的にマルチパート解析をトリガーしません。マッピングで multipart = true を使用します:
@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
// テキストフィールドのみのマルチパートフォーム
}
Enter fullscreen mode Exit fullscreen mode
より細かい制御が必要な場合は、Context を介して手動でファイルにアクセスすることもできます:
@Post
@Mapping(path = "/upload/manual", multipart = true)
public void uploadManual(String username, Context ctx) {
UploadedFile file = ctx.file("attachment");
UploadedFile[] extras = ctx.files("extras");
// 処理...
}
Enter fullscreen mode Exit fullscreen mode
セキュリティ: autoMultipart の制御
デフォルトでは、autoMultipart は true です。任意のマルチパートリクエストが自動的に解析されます。公開向けサービスでは、クライアントが任意のエンドポイントに大きなファイルを送信して解析オーバーヘッドを引き起こす可能性があります。
ルーターフィルタでこれを強化します:
Solon.start(App.class, args, app -> {
app.router().filter(-1, (ctx, chain) -> {
// アップロードパスでのみマルチパートを解析
ctx.autoMultipart(ctx.path().startsWith("/upload"));
chain.doFilter(ctx);
});
});
Enter fullscreen mode Exit fullscreen mode
集中型のテンポラリファイルクリーンアップ(v2.7.3+)には、フィルタも効果的です:
@Component
public class MultipartCleanupFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} finally {
if (ctx.isMultipartFormData()) {
ctx.filesDelete(); // アップロードされたすべてのテンポラリファイルをクリーンアップ
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
注: このパターンを使用する場合、アップロードハンドラを非同期にしないでください。フィルタはリクエスト完了時に実行されますが、非同期ハンドラはクリーンアップが実行される時点でもまだ動作している可能性があります。
ファイルサイズの設定
# app.yml
server:
request:
maxBodySize: 2mb # リクエストボディの最大サイズ(デフォルト: 2mb)
maxFileSize: 20mb # 単一ファイルの最大サイズ
maxHeaderSize: 8kb # ヘッダーの最大サイズ
fileSizeThreshold: 512kb # このサイズ未満: メモリ; 以上: テンポラリファイル(v3.6.0+)
Enter fullscreen mode Exit fullscreen mode
fileSizeThreshold 設定(v3.6.0 で導入)は、小さなファイルを自動的にメモリに、大きなファイルをディスクにルーティングします。v3.6.0 より前は、強制的なテンポラリファイルモードのために useTempfile: true がありましたが、このフラグは現在非推奨です。
ファイルのダウンロード
ファイルの返却も同様に簡潔です。バイト配列やストリームには DownloadedFile を返し、java.io.File を直接返すこともできます:
@Get
@Mapping("/download/report")
public DownloadedFile downloadReport() {
byte[] pdf = reportService.generatePdf();
return new DownloadedFile("application/pdf", pdf, "report.pdf");
}
@Get
@Mapping("/download/avatar/{userId}")
public File downloadAvatar(@Path String userId) {
return new File("/storage/avatars/" + userId + ".jpg");
}
Enter fullscreen mode Exit fullscreen mode
より細かい制御(キャッシュ、インライン表示、ETag)を行う場合:
@Get
@Mapping("/preview/logo")
public DownloadedFile previewLogo() {
DownloadedFile file = new DownloadedFile(new File("/assets/logo.png"));
file.asAttachment(false); // インライン表示(ダウンロードをトリガーしない)
file.cacheControl(3600); // 1 時間の 304 キャッシュ
file.eTag("logo-v3");
return file;
}
Enter fullscreen mode Exit fullscreen mode
DownloadedFile は HTTP Range もサポートしているため、追加設定なしでビデオストリーミングや大容量ファイルの再開可能なダウンロードに対応します。
追加の依存関係不要
上記のすべては solon-web に含まれており、別途マルチパートスターターや自動設定クラスを探す必要はありません:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
JDK HTTP、Jetty、Undertow、Grizzly、smarthttp など、任意のサーバーアダプタを選択できます。いずれも fileSizeThreshold を介してテンポラリファイルモードをサポートします。
メンタルモデル
Spring との違いは、主に明示性にあります:
-
Spring Boot: サーブレットレイヤーによって
MultipartFileが注入され、MultipartAutoConfigurationがすべてを登録し、spring.servlet.multipart.*が制限を設定し、CommonsMultipartResolverはオプション。 -
Solon:
UploadedFileパラメータ → 解析がトリガーされ、delete()を呼び出すまでテンポラリファイルは残り、autoMultipartによりパスレベルでの制御が可能。設定はserver.request.*に存在。
ほとんどのプロジェクトでは違いはわずかです。重要なのは、解析のタイミング、メモリとディスクのどちらに存在するのか、クリーンアップの責任は誰にあるのかを明確に理解したい場合です。Solon はこれらの質問を自動設定の中に隠すのではなく、明示的にします。
参考:
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.