| 方案 | 核心機制 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 1. 直接鏈接 | <a href="/files/xxx.pdf"> | 極簡、無服務端壓力、支援斷點續傳 | 路徑暴露、無權限控制、無下載統計 | 靜態資源、公開文件、CDN 分發 |
| 2. 流式傳輸 (Response OutputStream) | Servlet/JSP 讀文件寫 response.getOutputStream() | 權限可控、邏輯靈活、支援動態生成 | 佔用請求線程、大文件易 OOM、無斷點續傳 | 受控下載、動態報表、中小文件 |
| 3. Applet 客戶端下載 | Applet 接收 Servlet 流並寫本地磁盤 | 客戶端寫入、可實現進度條、多線程下載 | 需簽名信任、瀏覽器棄用 Java 插件、維護成本極高 | 舊系統維護、特殊客戶端需求(極不推薦新項目) |
| 4. 現代替代:前端 Blob + ObjectURL / Service Worker | Fetch/AXIOS 獲取 Blob,URL.createObjectURL() 觸發下載 | 純前端、支援進度、暫停續傳、大文件分片 | 需瀏覽器支援、大文件內存壓力 | 現代 Web 應用首選 |
<%@ page import="java.io.*, java.net.URLEncoder" %>
<%
// 1. 參數校驗與安全檢查
String filePath = request.getParameter("path"); // 實際應從服務層獲取真實路徑,防止路徑遍歷攻擊
String fileName = request.getParameter("name");
// 安全校驗:規範化路徑、限制在允許目錄下
File file = new File(filePath).getCanonicalFile();
File allowedRoot = new File("/data/downloads").getCanonicalFile();
if (!file.getAbsolutePath().startsWith(allowedRoot.getAbsolutePath()) || !file.exists()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 2. 設置響應頭
String mimeType = getServletContext().getMimeType(file.getName());
if (mimeType == null) mimeType = "application/octet-stream";
response.setContentType(mimeType);
response.setContentLengthLong(file.length());
// Content-Disposition: attachment 強制下載;filename* 支援 UTF-8 (RFC 5987)
String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replace("+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName);
response.setHeader("Cache-Control", "private, max-age=3600");
response.setHeader("Accept-Ranges", "bytes"); // 聲明支援範圍請求(需配合 Range 處理)
// 3. 高效流傳輸 (NIO 零拷貝或緩衝流)
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis, 8192);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
} catch (IOException e) {
log.error("文件下載中斷: {}", fileName, e);
// 客戶端斷開不拋異常,僅記錄
}
%>
../),使用 getCanonicalFile() 對比允許根目錄setContentLengthLong (Servlet 3.1+) 支援 2GB+ 文件filename (舊瀏覽器) + filename* (RFC 5987 標準)Range 請求頭,返回 206 Partial Content,建議用成熟庫 (如 RangeFileInputStream) 或 Nginx X-Accel-Redirect@WebServlet("/download-applet")
public class AppletDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/octet-stream");
File file = new File("/data/files/report.pdf");
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = resp.getOutputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = fis.read(buf)) != -1) os.write(buf, 0, len);
}
}
}
// 核心邏輯:接收流 -> 寫本地文件 -> 可選打開
URL url = new URL("https://host/download-applet?file=report.pdf");
URLConnection conn = url.openConnection();
conn.setRequestProperty("User-Agent", "JavaDownloader/1.0");
try (InputStream is = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(new File(System.getProperty("user.home"), "Downloads/report.pdf"))) {
byte[] buf = new byte[8192];
int len;
while ((len = is.read(buf)) != -1) fos.write(buf, 0, len);
}
// 可選:Desktop.getDesktop().open(file);
// service/api.js
export function downloadFile(url, params, fileName) {
return axios({
url,
method: 'GET',
params,
responseType: 'blob', // 關鍵:接收二進制 Blob
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(response => {
const blob = response.data;
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url); // 釋放內存
});
}
// 進階:大文件分片下載 + 進度條 (使用 axios-on-progress 或 Fetch + ReadableStream)
async function downloadLargeFile(url, fileName) {
const response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + getToken() } });
const total = Number(response.headers.get('Content-Length'));
const reader = response.body.getReader();
const chunks = [];
let loaded = 0;
while (true) {
const {done, value} = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
updateProgress(loaded / total * 100);
}
const blob = new Blob(chunks);
triggerSave(blob, fileName);
}
將文件傳輸卸載給 Nginx/Apache,Java 僅做權限校驗並返回特殊 Header:
// Java Controller
@GetMapping("/secure-download")
public ResponseEntity secureDownload(@RequestParam String fileId) {
FileMeta meta = fileService.checkPermissionAndGetMeta(fileId, currentUser());
if (meta == null) return ResponseEntity.notFound().build();
HttpHeaders headers = new HttpHeaders();
headers.add("X-Accel-Redirect", "/protected-files/" + meta.getStoredPath()); // Nginx 內部路徑
headers.add("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(meta.getOriginalName(), "UTF-8"));
headers.add("Content-Type", meta.getMimeType());
return ResponseEntity.ok().headers(headers).build();
}
// Nginx 配置
location /protected-files/ {
internal; # 禁止直接外部訪問
alias /data/files/;
sendfile on; # 內核零拷貝
tcp_nopush on;
aio threads; # 異步 I/O
}
收益:Java 線程瞬間釋放、零拷貝內核傳輸、自動支援斷點續傳、高併發下載零壓力。