Apache Commons FileUpload 是 Java Web 開發中處理 multipart/form-data 表單上傳的標準元件,廣泛應用於 Servlet/JSP、Spring MVC 等框架中。本文詳細說明從原理分析到完整實作的各個環節。
預設 application/x-www-form-urlencoded 僅支援鍵值對文字資料,無法處理二進位文件流。
ServletFileUpload.isMultipartContent(request) 驗證內容類型DiskFileItemFactory 建立工廠 (可設定暫存目錄、記憶體緩衝閾值)ServletFileUpload 解析請求,產生 List<FileItem>FileItem:isFormField() 區分一般欄位與文件欄位item.write(file) 持久化 (自動清理暫存檔)<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>文件上傳示例</title>
<meta charset="UTF-8">
</head>
<body>
<h2>多文件上傳測試</h2>
<form action="${pageContext.request.contextPath}/servlet/UploadServlet"
method="post" enctype="multipart/form-data">
姓名:<input type="text" name="username" /><br/>
文件1:<input type="file" name="file1" /><br/>
文件2:<input type="file" name="file2" /><br/>
<input type="submit" value="上傳" />
</form>
</body>
</html>
package com.example.upload;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 配置常數
private static final int MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB 單檔上限
private static final int MAX_TOTAL_SIZE = 6 * 1024 * 1024; // 6MB 總上限
private static final int MEMORY_THRESHOLD = 5 * 1024; // 5KB 記憶體緩衝
private static final String UPLOAD_DIR = "uploads"; // 相對儲存路徑
private static final String TEMP_DIR = "upload_temp"; // 暫存目錄
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 1. 驗證 multipart 請求
if (!ServletFileUpload.isMultipartContent(request)) {
out.write("錯誤:表單必須設定 enctype='multipart/form-data'");
return;
}
// 2. 建立工廠與解析器
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(MEMORY_THRESHOLD);
factory.setRepository(new File(getServletContext().getRealPath("/") + TEMP_DIR));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(MAX_FILE_SIZE);
upload.setSizeMax(MAX_TOTAL_SIZE);
upload.setHeaderEncoding("UTF-8"); // 解決中文檔名亂碼
String realUploadPath = getServletContext().getRealPath("/") + UPLOAD_DIR;
File uploadDir = new File(realUploadPath);
if (!uploadDir.exists()) uploadDir.mkdirs();
try {
// 3. 解析請求
List<FileItem> items = upload.parseRequest(request);
// 4. 處理各欄位
for (FileItem item : items) {
if (item.isFormField()) {
// 一般表單欄位
String fieldName = item.getFieldName();
String fieldValue = item.getString("UTF-8");
System.out.println("表單欄位: " + fieldName + " = " + fieldValue);
} else {
// 文件欄位處理
processUploadedFile(item, realUploadPath, out);
}
}
out.write("<script>alert('上傳成功!'); window.history.back();</script>");
} catch (FileUploadBase.FileSizeLimitExceededException e) {
out.write("<script>alert('單一檔案超過 3MB 限制!'); window.history.back();</script>");
} catch (FileUploadBase.SizeLimitExceededException e) {
out.write("<script>alert('總檔案大小超過 6MB 限制!'); window.history.back();</script>");
} catch (Exception e) {
e.printStackTrace();
out.write("<script>alert('上傳失敗:" + e.getMessage() + "'); window.history.back();</script>");
}
}
/** 處理單一上傳文件 */
private void processUploadedFile(FileItem item, String basePath, PrintWriter out) throws Exception {
String originalName = item.getName();
if (originalName == null || originalName.isEmpty()) {
out.write("<script>alert('未選擇檔案!'); window.history.back();</script>");
return;
}
// 安全檔名處理:UUID + 原始副檔名
String ext = FilenameUtils.getExtension(originalName);
String safeName = UUID.randomUUID().toString() + "." + ext;
// 依日期建立子目錄 (避免單一目錄檔案過多)
String dateDir = new java.text.SimpleDateFormat("yyyyMMdd").format(new Date());
File dateDirFile = new File(basePath, dateDir);
if (!dateDirFile.exists()) dateDirFile.mkdirs();
File destFile = new File(dateDirFile, safeName);
item.write(destFile); // 自動清理暫存檔
System.out.println("檔案已儲存: " + destFile.getAbsolutePath());
// 此處可記錄資料庫:原始檔名、儲存路徑、上傳者、時間、MIME類型等
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>com.example.upload.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/servlet/UploadServlet</url-pattern>
</servlet-mapping>
DiskFileItemFactory.setRepository(File):指定暫存目錄 (預設系統暫存目錄)setSizeThreshold(int):記憶體緩衝區大小,超過則寫入暫存檔 (預設 10KB)item.write(File) 而非手動 IO 流複製,前者會自動刪除暫存檔,避免磁碟洩漏upload.setHeaderEncoding("UTF-8") (Commons FileUpload 1.3.1+ 支援)new String(name.getBytes("ISO-8859-1"), "UTF-8")| 方法 | 作用範圍 | 拋出異常 |
|---|---|---|
| upload.setFileSizeMax() | 單一檔案 | FileSizeLimitExceededException |
| upload.setSizeMax() | 整個請求總和 | SizeLimitExceededException |
需在 parseRequest() 前設定,捕獲異常時請求流已讀取完畢,可直接回應錯誤訊息。
// 僅允許圖片類型
String contentType = item.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("僅允許上傳圖片檔案");
}
// 進階:檢查 Magic Number (檔案頭特徵碼) 防止偽裝副檔名
byte[] header = new byte[8];
try (InputStream in = item.getInputStream()) {
in.read(header);
}
// JPEG: FF D8 FF, PNG: 89 50 4E 47 0D 0A 1A 0A, PDF: 25 50 44 46...
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
@Value("${file.upload.dir:/data/uploads}")
private String uploadDir;
@PostMapping
public Result upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) return Result.error("檔案為空");
// 檔案類型驗證
String contentType = file.getContentType();
if (!Arrays.asList("image/jpeg", "image/png", "application/pdf").contains(contentType)) {
return Result.error("不支援的檔案類型");
}
// 儲存邏輯...
String fileName = UUID.randomUUID() + "." + FilenameUtils.getExtension(file.getOriginalFilename());
Path dest = Paths.get(uploadDir, fileName);
Files.copy(file.getInputStream(), dest);
return Result.success("/files/" + fileName);
}
}
application.yml 配置:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 2KB
Commons FileUpload 提供了穩健的文件上傳處理能力,核心在於正確配置工廠參數、妥善處理暫存檔案、實施嚴格的安全驗證。現代專案建議優先考慮 Spring Boot 整合的 MultipartFile 抽象層,或直接對接雲端對象儲存服務,將檔案儲存邏輯從應用伺服器剝離,提升擴展性與可維護性。