依賴準備
<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>
上傳 Servlet 實現
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
private static final long MAX_SIZE = 20 * 1024 * 1024; // 20MB
private static final String ALLOWED_EXTS = ".jpg|.jpeg|.gif|.bmp|.png|";
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
boolean success = processUpload(req);
req.getRequestDispatcher("/upload.jsp").forward(req, resp);
}
private boolean processUpload(HttpServletRequest req) {
String savePath = getServletContext().getRealPath("/upload");
File dir = new File(savePath);
if (!dir.exists()) dir.mkdirs();
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(dir);
factory.setSizeThreshold(1024 * 1024); // 1MB 內存閾值
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(MAX_SIZE);
upload.setHeaderEncoding("UTF-8");
try {
List<FileItem> items = upload.parseRequest(req);
for (FileItem item : items) {
String fieldName = item.getFieldName();
if (item.isFormField()) {
// 普通表單字段
String value = item.getString("UTF-8");
req.setAttribute(fieldName, value);
} else {
// 文件字段
String fileName = item.getName();
if (fileName == null || fileName.trim().isEmpty()) continue;
// 安全處理:僅取文件名部分
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
String ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (!ALLOWED_EXTS.contains(ext + "|")) {
req.setAttribute("message", "禁止上傳類型: " + ext);
return false;
}
// 生成唯一文件名防覆蓋
String newName = System.currentTimeMillis() + ext;
req.setAttribute(fieldName, fileName);
req.setAttribute(fieldName + "Server", newName);
// 流式寫入
try (InputStream in = item.getInputStream();
OutputStream out = new FileOutputStream(new File(dir, newName))) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len);
}
}
}
req.setAttribute("message", "上傳成功");
return true;
} catch (FileUploadException e) {
req.setAttribute("message", "文件過大或解析失敗");
return false;
} catch (Exception e) {
req.setAttribute("message", "上傳異常: " + e.getMessage());
return false;
}
}
}
下載 Servlet 實現
@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String fileName = req.getParameter("fileName"); // 存儲名
String origName = req.getParameter("origName"); // 原始名
if (fileName == null || origName == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
processDownload(fileName, origName, req, resp);
}
private void processDownload(String fileName, String origName,
HttpServletRequest req, HttpServletResponse resp) {
String path = getServletContext().getRealPath("/upload");
File file = new File(path, fileName);
if (!file.exists()) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.reset();
resp.setContentType("application/octet-stream");
resp.setContentLengthLong(file.length());
// RFC 5987 編碼兼容中文文件名
String encodedName = StandardCharsets.UTF_8.encode(origName).toString()
.replaceAll("[^\\p{ASCII}]", "");
resp.setHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + encodedName);
try (FileInputStream in = new FileInputStream(file);
OutputStream out = resp.getOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len);
out.flush();
} catch (IOException e) {
log.error("下載失敗", e);
}
}
}
前端 JSP 頁面
<%@ page contentType="text/html;charset=UTF-8" %>
<form action="upload" method="post" enctype="multipart/form-data">
賬號: <input type="text" name="account"><br/>
文件: <input type="file" name="upfile"><br/>
<input type="submit" value="上傳">
</form>
<c:if test="${not empty message}">
<div style="color:red">${message}</div>
</c:if>
<h3>下載示例</h3>
<a href="download?fileName=12345.jpg&origName=風景.jpg">下載風景.jpg</a>
關鍵最佳實踐
| 場景 | 做法 |
| 大文件內存溢出 | DiskFileItemFactory 設閾值 + 臨時目錄,超閾值落盤 |
| 文件名衝突 | 時間戳/UUID + 原後綴重命名 |
| 中文文件名下載亂碼 | Content-Disposition: filename*=UTF-8''encoded |
| 惡意文件上傳 | 白名單後綴 + 內容檢測 + 存儲目錄禁執行權限 |
| 路徑遍歷攻擊 | 文件名僅保留 baseName,拼接安全路徑 |
| 併發上傳性能 | 異步處理 + 對象存儲 (OSS/S3) 直傳 |
現代化替代方案
- Spring Boot:
MultipartFile + @RequestParam 自動綁定,無需手動解析
- Servlet 3.0+:
@MultipartConfig + request.getPart() 原生 API
- 大文件直傳:前端簽名直傳 OSS,後端僅驗證回調