檔案下載兩種場景
- 靜態資源直連:檔案位於 Web 公開目錄,瀏覽器直接請求 URL 下載 (如
/files/report.pdf)。
- 動態串流下載:檔案儲存於非公開路徑、需權限控管、或即時生成 (報表、Excel),需透過
HttpServletResponse.getOutputStream() 輸出。
通用下載工具類 DownloadUtils
public class DownloadUtils {
private static final String DEFAULT_ENCODING = "UTF-8";
private static final int BUFFER_SIZE = 8192;
// 核心方法:InputStream -> Response
public static void download(HttpServletResponse response, InputStream inputStream,
String fileName, String encoding) throws IOException {
if (inputStream == null || fileName == null) return;
response.setContentType("application/octet-stream;charset=" + encoding);
response.setCharacterEncoding(encoding);
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(fileName, encoding));
try (BufferedInputStream bis = new BufferedInputStream(inputStream);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush();
}
}
// 重載:File -> Response
public static void download(HttpServletResponse response, File file, String fileName) throws IOException {
if (file == null || !file.exists() || file.isDirectory()) return;
if (fileName == null) fileName = file.getName();
try (FileInputStream fis = new FileInputStream(file)) {
download(response, fis, fileName, DEFAULT_ENCODING);
}
}
// 重載:路徑字串 -> Response
public static void download(HttpServletResponse response, String filePath, String fileName) throws IOException {
download(response, new File(filePath), fileName);
}
// 設定全域編碼
public static void setDefaultEncoding(String encoding) { /* ... */ }
}
Controller / Servlet 整合範例
Spring MVC Controller
@Controller
@RequestMapping("/file")
public class FileController {
@GetMapping("/download")
public void download(@RequestParam String path,
@RequestParam(required = false) String fileName,
HttpServletResponse response) throws IOException {
DownloadUtils.download(response, path, fileName);
}
@GetMapping("/export-excel")
public void exportExcel(HttpServletResponse response) throws IOException {
// 即時生成 Excel 串流
ByteArrayOutputStream baos = excelService.generateExcel();
DownloadUtils.download(response, new ByteArrayInputStream(baos.toByteArray()),
"報表_" + LocalDate.now() + ".xlsx", "UTF-8");
}
}
傳統 Servlet
@WebServlet("/download/servlet")
public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String filePath = "C:/data/export.zip";
DownloadUtils.download(resp, filePath);
}
}
防盜鏈下載範例 (圖片)
@WebServlet("/secure/image")
public class SecureImageServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String referer = req.getHeader("Referer");
if (referer == null || !referer.startsWith("https://mysite.com")) {
resp.sendRedirect("/error/hotlink.html");
return;
}
String fileName = URLEncoder.encode("內部圖片.jpg", "UTF-8");
resp.setHeader("Content-Disposition", "attachment;filename=" + fileName);
resp.setContentType("image/jpeg");
try (InputStream in = getServletContext().getResourceAsStream("/images/internal.jpg");
OutputStream out = resp.getOutputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) out.write(buf, 0, len);
}
}
}
關鍵技術細節
| 項目 | 說明 |
| Content-Type | application/octet-stream 強制下載;已知類型可設定具體 MIME (如 application/pdf) |
| Content-Disposition | attachment 觸發下載;inline 瀏覽器嘗試直接開啟 |
| 中文檔名編碼 | RFC 5987: filename*=UTF-8''%E6%96%87%E4%BB%B6.txt;相容舊瀏覽器用 ISO-8859-1 |
| 緩衝區大小 | 4KB-8KB 平衡記憶體與系統呼叫次數 |
| 大檔案支援 | 使用 RandomAccessFile + Range 實現斷點續傳 (見文章 236447) |
最佳實踐清單
- 輸出流務必在
finally 或 try-with-resources 中關閉。
- 檔案不存在時回傳 404,而非空回應。
- 敏感檔案下載前驗證權限 (Spring Security
@PreAuthorize 或 Servlet Filter)。
- 生產環境建議由 Nginx
X-Accel-Redirect 或 Apache mod_xsendfile 直接服務靜態檔案,減輕 Java 進程負載。
- 即時生成報表時,使用
ByteArrayOutputStream 或臨時檔案避免記憶體溢位。