文件上傳是 Web 開發高頻需求。Servlet 3.0 前需借助第三方庫,Apache Commons FileUpload 是經典選擇。本文提供兩個版本完整實現:基礎版(單文件/多文件/表單混合上傳)與 進階版(流式寫入+實時進度顯示),並解析核心 API 與生產環境關鍵配置。
<dependencies>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version> <!-- 最新穩定版 -->
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
package com.zhanghaobo.fileupload;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 創建磁盤工廠:配置臨時文件目錄、內存閾值
DiskFileItemFactory factory = new DiskFileItemFactory();
String realPath = req.getServletContext().getRealPath("/upload"); // 獲取 Web 絕對路徑
factory.setRepository(new File(realPath)); // 臨時文件存放目錄
factory.setSizeThreshold(1024 * 1024); // 1MB 內存閾值,超過寫磁盤
// 2. 創建上傳處理器
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8"); // 解決中文文件名亂碼
upload.setFileSizeMax(100 * 1024 * 1024); // 單文件最大 100MB
upload.setSizeMax(500 * 1024 * 1024); // 總請求最大 500MB
try {
// 3. 解析請求,獲取 FileItem 列表
List list = upload.parseRequest(req);
for (FileItem item : list) {
String fieldName = item.getFieldName();
if (item.isFormField()) {
// 普通表單字段
String value = item.getString("UTF-8");
req.setAttribute(fieldName, value);
} else {
// 文件字段
String originalName = item.getName();
// 處理瀏覽器差異:IE 包含完整路徑,Chrome/FF 僅文件名
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
req.setAttribute(fieldName, fileName);
// 4. 寫入目標文件(FileItem 提供便捷 write 方法)
File targetFile = new File(realPath, fileName);
item.write(targetFile); // 內部自動處理內存/臨時文件轉移
}
}
} catch (Exception e) {
e.printStackTrace();
req.setAttribute("error", "上傳失敗: " + e.getMessage());
}
// 5. 轉發結果頁
req.getRequestDispatcher("showResult.jsp").forward(req, resp);
}
}
| 類/方法 | 作用 | 關鍵點 |
|---|---|---|
| DiskFileItemFactory | FileItem 創建工廠 | setRepository(臨時目錄)、setSizeThreshold(內存閾值) |
| ServletFileUpload | 請求解析核心 | parseRequest() 返回 List |
| FileItem | 表單項抽象(文件/普通字段) | isFormField() 區分類型、getString(encoding) 讀取文本、write(File) 保存文件 |
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
DiskFileItemFactory factory = new DiskFileItemFactory();
String realPath = req.getServletContext().getRealPath("/upload");
factory.setRepository(new File(realPath));
factory.setSizeThreshold(1024 * 1024);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");
try {
List list = upload.parseRequest(req);
for (FileItem item : list) {
String fieldName = item.getFieldName();
if (item.isFormField()) {
String value = item.getString("UTF-8");
req.setAttribute(fieldName, value);
} else {
String originalName = item.getName();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
req.setAttribute(fieldName, fileName);
// === 進階:手動流式寫入 + 進度計算 ===
try (InputStream is = item.getInputStream();
OutputStream os = new FileOutputStream(new File(realPath, fileName))) {
long totalSize = item.getSize(); // 文件總大小
byte[] buffer = new byte[4096]; // 4KB 緩衝區
long uploaded = 0;
int len;
DecimalFormat df = new DecimalFormat("#.00");
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
uploaded += len;
// 實時輸出進度(生產環境可推送 WebSocket/前端輪詢)
System.out.printf("總大小: %d 字節 | 進度: %s%% | 已上傳: %d 字節%n",
totalSize, df.format((double) uploaded / totalSize * 100), uploaded);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
req.getRequestDispatcher("showResult.jsp").forward(req, resp);
}
}
item.getInputStream() 獲取輸入流,手動控制緩衝區寫入item.getSize() 獲取總大小,累加 read() 返回值計算實時進度item.write() 內部已處理臨時文件清理,手動流式寫入需自行確保異常時清理臨時文件<form action="upload" method="post" enctype="multipart/form-data">
<input type="text" name="username" placeholder="用戶名" /><br/>
<input type="file" name="file1" /><br/>
<input type="file" name="file2" /><br/>
<input type="submit" value="上傳" />
</form>
三要素:method="post"、enctype="multipart/form-data"、input type="file"
| 配置項 | 推薦值 | 說明 |
|---|---|---|
| 臨時目錄 | /tmp/upload 或 專用磁盤分區 | 避免佔用系統盤、支持大文件、定期清理策略 |
| 內存閾值 | 1-5 MB | 過小頻繁寫磁盤,過大佔用 Heap,根據平均文件大小調優 |
| 最大文件大小 | 業務實際上限 (如 100MB) | 防止惡意大文件攻擊、OOM |
| 最大請求大小 | 單文件上限 × 並發文件數 | 防止多文件同時上傳撐爆內存 |
| 文件名衝突處理 | UUID + 原始後綴 / 時間戳 + 隨機數 | 避免覆蓋、支持同名文件並存 |
| 文件類型校驗 | MIME 類型 + 魔數檢測 + 後綴白名單 | 防止腳本上傳、偽裝文件攻擊 |
| 權限控制 | 上傳目錄禁用執行權限 (chmod -x) | 防止上傳 WebShell 執行 |
| 病毒掃描 | 集成 ClamAV / 商業反病毒 API | 高安全級別系統必備 |
Servlet 3.0 (Tomcat 7+, Jetty 8+) 內置文件上傳支持,無需第三方庫:
@WebServlet("/upload")
@MultipartConfig(
location = "/tmp/upload", // 臨時文件目錄
fileSizeThreshold = 1024 * 1024, // 內存閾值 1MB
maxFileSize = 100 * 1024 * 1024, // 100MB
maxRequestSize = 500 * 1024 * 1024 // 500MB
)
public class NativeUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 獲取 Part(對應 FileItem)
Part filePart = req.getPart("file1");
String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();
// 保存文件
filePart.write(Paths.get("/upload", fileName).toString());
// 獲取普通字段
String username = req.getParameter("username");
}
}
遷移建議:新項目優先使用 Servlet 3.0 原生 API,減少依賴;維護舊項目或需精細控制上傳流程(如進度、斷點續傳)繼續用 Commons FileUpload。
文件上傳看似簡單,實則涉及內存管理、磁盤 I/O、安全防護、用戶體驗等多維工程考量。掌握 Commons FileUpload 核心流程(工廠→解析器→FileItem→流處理),理解內存閾值與臨時文件機制,具備流式寫入進度計算能力,並建立生產環境配置清單,才能構建健壯、安全、用戶友好的上傳功能。建議團隊封裝通用 FileUploadUtils 工具類,統一異常處理、文件命名、安全校驗、進度回調,實現「業務代碼零感知上傳」。