檔案上傳聽起來很無聊,直到你花了一個下午在生產環境中除錯 MultipartException: Failed to parse multipart servlet request。
在 Spring Boot 中,檔案上傳涉及 MultipartFile、MultipartAutoConfiguration、spring.servlet.multipart.* 屬性,以及一個無聲的假設——你正在 servlet 容器上執行。它能運作,但帶有許多隱藏的機制。
Solon 採取不同的做法。核心類別是 UploadedFile,它位於網頁層,無需額外設定,而且你可以明確控制何時進行 multipart 解析,以及何時清理暫存檔案。
基礎:UploadedFile
當控制器方法包含 UploadedFile 參數時,Solon 會自動觸發 multipart 解析——無需註解:
@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 參數時
有時 multipart 表單只有文字欄位——沒有檔案附件。此時 Solon 不會自動觸發 multipart 解析。請在 mapping 上使用 multipart = true:
@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
// 僅含文字欄位的 multipart 表單
}
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——任何傳入的 multipart 請求都會被自動解析。在面向大眾的服務上,這意味著用戶端可以向任何端點傳送大型檔案並觸發解析開銷。
使用路由過濾器收緊此設定:
Solon.start(App.class, args, app -> {
app.router().filter(-1, (ctx, chain) -> {
// 僅對上傳路徑解析 multipart
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 中——無需獨立的 multipart starter,也無需尋找自動設定類別:
<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由 servlet 層注入;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.