檔案上傳是 Web 開發的高頻需求。在 Servlet 3.0 引入 @MultipartConfig 之前,開發者需手工解析 multipart/form-data 請求體。本文透過從零構建簡易上傳 Bean,深度剖析 HTTP 檔案上傳請求的原始結構與解析邏輯。
<form action="jsp1.jsp" enctype="multipart/form-data" method="post">
作者: <input type="text" name="author" /><br/>
公司: <input type="text" name="company" /><br/>
選擇檔案: <input type="file" name="filename" /><br/>
<input type="submit" value="上傳" />
</form>
關鍵點:enctype="multipart/form-data" 告知瀏覽器採用多部分編碼,而非預設的 application/x-www-form-urlencoded。
<jsp:useBean id="uploadBean" class="SimpleBean" scope="page" />
<%
uploadBean.doUpload(request);
%>
import java.io.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletInputStream;
public class SimpleBean {
public void doUpload(HttpServletRequest request) throws IOException {
// 將原始請求體完整寫入檔案供分析
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("Demo.out"));
ServletInputStream in = request.getInputStream()) {
int b;
while ((b = in.read()) != -1) {
pw.print((char) b);
}
}
}
}
-----------------------------7d15340138
Content-Disposition: form-data; name="Author"
A. Christie
-----------------------------7d15340138
Content-Disposition: form-data; name="Company"
Abisco
-----------------------------7d15340138
Content-Disposition: form-data; name="Filename"; filename="C:\123\data\abisco.html"
Content-Type: text/html
<html>
<head>
<title>Abisco</title>
</head>
</html>
-----------------------------7d15340138--
----------------------------- + 隨機數字--{boundary} 開頭--{boundary}-- (尾部雙連字號)--{boundary}
Content-Disposition: form-data; name="{fieldName}"
{fieldValue}
--{boundary}
Content-Disposition: form-data; name="{fieldName}"; filename="{clientPath}"
Content-Type: {mimeType}
{fileBinaryContent}
Content-Type 標頭提取 boundary 參數--{boundary} 分割各部分Content-Disposition、Content-Typefilename 存在與否判斷:一般欄位 vs 檔案| 挑戰 | 解決方案 |
|---|---|
| 大檔案記憶體溢出 | 臨界值超過時落地磁碟,使用 FileItemFactory 機制 |
| 邊界匹配效能 | Boyce-Moore/KMP 算法優化邊界搜尋 |
| 字元編碼處理 | 標頭用 ISO-8859-1,欄位值用請求編碼 (UTF-8) |
| 檔名安全性 | 提取純檔名 new File(filename).getName(),防止路徑遍歷 |
| 惡意請求防護 | 限制總大小、單檔大小、檔案類型白名單 |
@MultipartConfig + request.getPart() / request.getParts()MultipartFile 介面public class FileUploadBean {
private static final int MEMORY_THRESHOLD = 1024 * 1024; // 1MB
private static final long MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
private static final long MAX_REQUEST_SIZE = 100 * 1024 * 1024; // 100MB
public Map<String, Object> parseRequest(HttpServletRequest request)
throws IOException, ServletException {
// 1. 驗證 Content-Type
String contentType = request.getContentType();
if (contentType == null || !contentType.startsWith("multipart/")) {
throw new ServletException("非 multipart 請求");
}
// 2. 使用 Commons FileUpload (推薦)
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(MEMORY_THRESHOLD);
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(MAX_FILE_SIZE);
upload.setSizeMax(MAX_REQUEST_SIZE);
upload.setHeaderEncoding("UTF-8");
// 3. 解析並分類
Map<String, Object> result = new HashMap<>();
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
// 一般欄位
result.put(item.getFieldName(), item.getString("UTF-8"));
} else {
// 檔案欄位
String fileName = new File(item.getName()).getName();
File savedFile = new File(UPLOAD_DIR, generateUniqueName(fileName));
item.write(savedFile);
result.put(item.getFieldName(), savedFile);
}
}
return result;
}
}
從手工解析原始請求流到採用成熟框架,理解 Multipart 協議細節是排查上傳異常、實現自定義需求(如斷點續傳、流式處理、病毒掃描整合)的必經之路。