瀏覽器識別 Content-Disposition: attachment; filename=xxx 響應頭,彈出「另存為」對話框,而非在頁面顯示。關鍵步驟:
Content-Disposition、Content-Length、Content-Type@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 文件路徑 (建議配置外部存儲目錄,非 WAR 內部)
String filePath = "/data/uploads/report.pdf";
File file = new File(filePath);
if (!file.exists()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 2. 文件名編碼 (RFC 5987 標準,兼容中文)
String fileName = file.getName();
String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replace("+", "%20");
String contentDisposition = String.format(
"attachment; filename=\"%s\"; filename*=UTF-8''%s",
fileName, encodedName);
// 3. 設置響應頭
resp.reset();
resp.setHeader("Content-Disposition", contentDisposition);
resp.setHeader("Content-Length", String.valueOf(file.length()));
resp.setContentType("application/octet-stream"); // 通用二進制流
// 4. 流式傳輸 (緩衝 8KB,避免大文件 OOM)
try (InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = new BufferedOutputStream(resp.getOutputStream())) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
}
}
}
<!-- 方式 1:超連結 (最簡單) -->
<a href="${pageContext.request.contextPath}/download?fileId=123">下載報表</a>
<!-- 方式 2:JS 觸發 (需帶參數/權限校驗) -->
<button onclick="downloadFile(123)">下載</button>
<script>
function downloadFile(id) {
window.location.href = '${pageContext.request.contextPath}/download?fileId=' + id;
}
</script>
<!-- 方式 3:AJAX + Blob (需處理二進制響應,較複雜,不推薦簡單下載) -->
String referer = req.getHeader("Referer");
if (referer == null || !referer.startsWith("https://yourdomain.com")) {
resp.sendRedirect("/403.html");
return;
}
// 檢查 Range 頭
String range = req.getHeader("Range");
if (range != null) {
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206
// 解析 range: bytes=start-end
// 設置 Content-Range, Content-Length
// 只傳輸指定區間
}
@GetMapping("/download/{id}")
public ResponseEntity download(@PathVariable Long id) {
FileInfo info = fileService.getById(id);
Resource resource = new FileSystemResource(info.getPath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment().filename(info.getName(), StandardCharsets.UTF_8).build().toString())
.contentLength(info.getSize())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 中文文件名亂碼 | 瀏覽器編碼解析不一致 | 雙重編碼 filename + filename* (RFC 5987) |
| 大文件記憶體溢出 | 一次性 readAllBytes | 流式分塊傳輸 8KB 緩衝 |
| 下載文件損壞/為 0 字節 | 流未關閉/未 flush | try-with-resources 確保關閉 |
| AJAX 無法觸發下載 | 瀏覽器安全策略阻攔 | 改用 window.location.href 或 <a> 標籤 |
| WAR 部署路徑失效 | getRealPath 返回 null | 配置外部絕對路徑或資源庫 |
file.getCanonicalPath(),校驗是否在允許目錄下。application/pdf),未知用 application/octet-stream。Java Web 文件下載核心在於正確設置三大響應頭 (Content-Disposition、Content-Length、Content-Type) 並採用緩衝流式傳輸。生產環境需補全:中文文件名雙重編碼、防盜鏈、權限校驗、路徑遍歷防護、大文件異步化、下載審計。Spring Boot 可直接使用 ResponseEntity<Resource> 簡化代碼,Nginx X-Accel-Redirect 可進一步卸載應用服務器帶寬。