Apache Commons FileUpload 是 Java 文件上傳的經典組件,雖然 Spring MVC 提供了更高層的 MultipartFile 抽象,但理解其底層工作原理對排查問題、適配非 Spring 環境、實現自定義存儲邏輯至關重要。本文從零構建完整上傳流程:依賴引入、前端表單、Servlet 解析、文件/字段區分、輸入流直接寫入數據庫 BLOB 字段,並解決 MySQL 大數據包限制問題。
<dependencies>
<!-- 核心上傳組件 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- IO 工具類依賴 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 數據庫驅動 (以 MySQL 為例) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
<h2>文件上傳測試</h2>
<form action="UploadServlet" method="post" enctype="multipart/form-data">
描述: <input type="text" name="description" /><br/>
文件: <input type="file" name="uploadFile" /><br/>
<input type="submit" value="上傳" />
</form>
</body>
</html>
@WebServlet("/UploadServlet")
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
// 1. 檢查是否為 multipart 請求
if (!ServletFileUpload.isMultipartContent(req)) {
out.println("錯誤: 請求格式必須為 multipart/form-data");
return;
}
// 2. 配置工廠與解析器
DiskFileItemFactory factory = new DiskFileItemFactory();
// 設置臨時文件目錄 (項目外部路徑)
String tempPath = getServletContext().getRealPath("/WEB-INF/temp");
File tempDir = new File(tempPath);
if (!tempDir.exists()) tempDir.mkdirs();
factory.setRepository(tempDir);
factory.setSizeThreshold(1024 * 1024); // 1MB 內存閾值
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8"); // 關鍵:解決中文文件名亂碼
upload.setFileSizeMax(10 * 1024 * 1024); // 10MB
upload.setSizeMax(50 * 1024 * 1024); // 50MB
// 3. 解析請求
try {
@SuppressWarnings("unchecked")
List<FileItem> items = upload.parseRequest(req);
String description = null;
InputStream fileStream = null;
String fileName = null;
String fileType = null;
for (FileItem item : items) {
if (item.isFormField()) {
// 普通表單字段
String fieldName = item.getFieldName();
String value = item.getString("UTF-8");
if ("description".equals(fieldName)) {
description = value;
}
} else {
// 文件字段
String originalName = item.getName();
if (originalName == null || originalName.isEmpty()) continue;
// 處理瀏覽器傳遞的完整路徑 (IE 等老舊瀏覽器)
int idx = originalName.lastIndexOf("\\");
fileName = (idx >= 0) ? originalName.substring(idx + 1) : originalName;
// 獲取擴展名作為類型
int dotIdx = fileName.lastIndexOf('.');
fileType = (dotIdx > 0) ? fileName.substring(dotIdx + 1).toLowerCase() : "";
// 直接獲取輸入流,不落地磁盤
fileStream = item.getInputStream();
}
}
// 4. 入庫處理
if (fileStream != null && fileName != null) {
UploadDAO dao = new UploadDAO();
dao.saveFile(fileStream, fileName, fileType, description);
out.println("✅ 文件 '" + fileName + "' 上傳並入庫成功!描述: " + description);
} else {
out.println("❌ 未接收到有效文件");
}
} catch (FileUploadException e) {
out.println("❌ 解析失敗: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
out.println("❌ 服務器錯誤: " + e.getMessage());
e.printStackTrace();
}
}
}
public class UploadDAO {
// 表結構建議:
// CREATE TABLE t_file (
// id BIGINT PRIMARY KEY AUTO_INCREMENT,
// file_name VARCHAR(255),
// file_type VARCHAR(50),
// file_data LONGBLOB, -- 關鍵:使用 LONGBLOB 支援 4GB
// description TEXT,
// create_time DATETIME DEFAULT CURRENT_TIMESTAMP
// );
public void saveFile(InputStream in, String fileName, String fileType, String description)
throws SQLException {
String sql = "INSERT INTO t_file (file_name, file_type, file_data, description) VALUES (?, ?, ?, ?)";
try (Connection conn = JdbcUtils.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, fileName);
ps.setString(2, fileType);
// 關鍵:setBlob 直接從輸入流讀取,驅動層處理分塊傳輸
ps.setBlob(3, in);
ps.setString(4, description);
ps.executeUpdate();
} catch (SQLException e) {
// 捕獲 "Packet too large" 等錯誤
throw new RuntimeException("數據庫寫入失敗", e);
}
}
}
public class JdbcUtils {
private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
private static final String USER = "root";
private static final String PASS = "123456";
static {
try { Class.forName("com.mysql.cj.jdbc.Driver"); }
catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); }
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASS);
}
public static void close(AutoCloseable... resources) {
for (AutoCloseable r : resources) {
if (r != null) try { r.close(); } catch (Exception ignored) {}
}
}
}
默認 max_allowed_packet = 4MB,上傳大文件會報 Packet too large 錯誤,需調整:
mysql> SET GLOBAL max_allowed_packet = 67108864; -- 64MB
mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
[mysqld]
max_allowed_packet=64M
# 重啟 MySQL 服務生效
jdbc:mysql://localhost:3306/mydb?
useSSL=false&
serverTimezone=UTC&
allowPublicKeyRetrieval=true&
maxAllowedPacket=67108864 <!-- 驅動層也建議設置 -->
| 現象 | 原因 | 解決 |
|---|---|---|
| request.getParameter 為 null | multipart 請求無法用 getParameter 獲取 | 必須通過 FileItem.getString() 獲取普通字段 |
| 中文文件名亂碼 | 未設置 headerEncoding | upload.setHeaderEncoding("UTF-8") |
| 文件名包含完整路徑 (C:\path\file.jpg) | IE/舊版瀏覽器行為 | lastIndexOf("\\") 截取文件名部分 |
| 上傳大文件報錯 Packet too large | MySQL max_allowed_packet 過小 | 調大 MySQL 配置與驅動參數 |
| 臨時文件未清理佔滿磁盤 | 異常中斷未調用 delete() | 使用 try-with-resources、定時清理任務 |
| 數據庫保留字衝突 (如 description) | 列名與關鍵字衝突 | 反引號 \`description\` 或改名 descr |
InputStream → setBlob → DB 零臨時文件,內存佔用極低Commons FileUpload 核心流程:DiskFileItemFactory 創建臨時存儲 → ServletFileUpload 解析 Multipart 請求 → 遍歷 FileItem 區分字段/文件 → 獲取 InputStream 直接業務處理。本文展示了「流式直寫數據庫 BLOB」的輕量級方案,適合中小文件、低併發場景。生產系統建議遷移至 Spring MVC MultipartFile + 對象存儲架構,獲得更好的擴展性、可維護性與雲原生適配能力。