核心下載方法
public void downloadFile(File file, String downloadName,
HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (!file.exists()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
return;
}
// 1. 解決中文文件名瀏覽器兼容性
String userAgent = request.getHeader("User-Agent");
String encodedName;
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE / Edge 舊版
encodedName = URLEncoder.encode(downloadName, StandardCharsets.UTF_8.name());
} else {
// 現代瀏覽器 (Chrome, Firefox, Safari, 新 Edge)
encodedName = new String(downloadName.getBytes(StandardCharsets.UTF_8),
StandardCharsets.ISO_8859_1);
}
// 2. 設置響應頭
response.reset();
response.setContentType("application/octet-stream"); // 通用二進制流
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedName + "\""); // 強制下載
response.setContentLengthLong(file.length()); // 支持大文件
// 3. 流式傳輸 (NIO 零拷貝優化可選)
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis, 8192);
OutputStream os = new BufferedOutputStream(response.getOutputStream(), 8192)) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
os.flush();
}
}
關鍵技術點深度解析
1. Content-Disposition 標準與實踐
// RFC 5987 標準 (現代瀏覽器支援)
Content-Disposition: attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6%E5%90%8D.txt
// 兼容寫法 (雙重指定)
Content-Disposition: attachment; filename="fallback.txt"; filename*=UTF-8''%E6%96%87%E4%BB%B6%E5%90%8D.txt
**推薦雙重指定** 確保最大兼容性。
2. 瀏覽器 User-Agent 判斷邏輯
| 瀏覽器 | User-Agent 特徵 | 編碼方式 |
| IE 6-11 | 含 "MSIE" 或 "Trident" | `URLEncoder.encode(name, "UTF-8")` |
| Edge (Legacy) | 含 "Edge" | 同 IE |
| Chrome / Firefox / Safari / Edge (Chromium) | 其他 | RFC 5987 或 ISO-8859-1 轉碼 |
3. 大文件支援要點
- `response.setContentLengthLong(file.length())`:`long` 類型支援 >2GB
- 緩衝區 8KB-64KB 平衡記憶體與系統調用
- **NIO 零拷貝** (Java 8+,文件 → Socket 通道):
try (FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ);
WritableByteChannel bc = Channels.newChannel(response.getOutputStream())) {
fc.transferTo(0, fc.size(), bc); // 內核態直接傳輸,CPU 佔用極低
}
4. 斷點續傳 (Range 請求)
// 請求頭: Range: bytes=0-1023
// 響應頭: Content-Range: bytes 0-1023/2048
// 狀態碼: 206 Partial Content
String rangeHeader = request.getHeader("Range");
if (rangeHeader != null) {
long fileLen = file.length();
String[] ranges = rangeHeader.replace("bytes=", "").split("-");
long start = Long.parseLong(ranges[0]);
long end = ranges.length > 1 && !ranges[1].isEmpty() ?
Long.parseLong(ranges[1]) : fileLen - 1;
if (start >= fileLen || end >= fileLen || start > end) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
response.setHeader("Content-Range", "bytes */" + fileLen);
return;
}
long contentLength = end - start + 1;
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLen);
response.setContentLengthLong(contentLength);
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
raf.seek(start);
byte[] buffer = new byte[8192];
long remaining = contentLength;
OutputStream os = response.getOutputStream();
while (remaining > 0) {
int read = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
if (read == -1) break;
os.write(buffer, 0, read);
remaining -= read;
}
}
return;
}
Spring MVC / Spring Boot Controller 封裝
@GetMapping("/download")
public void download(@RequestParam String fileName,
HttpServletRequest request, HttpServletResponse response) {
// 安全校驗:防止路徑遍歷
String safeName = Paths.get(fileName).getFileName().toString();
File file = new File(uploadDir, safeName);
// 權限校驗 (示例)
if (!hasPermission(file)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
downloadFile(file, fileName, request, response);
}
前端調用方式
1. 普通超鏈接 (最簡單)
<a href="/download?fileName=report.pdf">下載報告</a>
2. AJAX + Blob (需處理二進制響應)
fetch('/download?fileName=report.pdf', { method: 'GET' })
.then(res => res.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'report.pdf';
a.click();
window.URL.revokeObjectURL(url);
});
3. iframe 隱式下載 (不刷新頁面)
function download(url) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 1000);
}
常見問題與解決方案
| 問題 | 原因 | 解決 |
| 下載文件名亂碼 | 編碼方式與瀏覽器不匹配 |
雙重 filename/filename* 或 UA 判斷分支 |
| 大文件下載中斷 | 超時/無斷點續傳 |
啟用 Range 支持、增加代理超時、NIO 零拷貝 |
| 瀏覽器直接打開而非下載 | Content-Type 被識別 |
強制 `application/octet-stream` + `attachment` |
| 文件損壞/大小為 0 | 流未關閉/未 flush |
try-with-resources 確保關閉、os.flush() |
| 路徑遍歷漏洞 | 未校驗 fileName |
`Paths.get(fileName).getFileName()` 只取文件名 |
| 中文路徑文件找不到 | 文件系統編碼 |
確保 JVM `-Dfile.encoding=UTF-8`、文件系統支援 UTF-8 |
最佳實踐總結
- ✅ **統一工具類** `FileDownloadUtil.download(...)` 封裝所有邏輯
- ✅ **安全第一**:路徑校驗、權限校驗、文件類型白名單
- ✅ **大文件優化**:NIO `transferTo`、斷點續傳、分片下載
- ✅ **兼容性**:RFC 5987 + 降級方案、User-Agent 判斷
- ✅ **資源管理**:try-with-resources、緩衝流、異常時確保流關閉
- ✅ **監控指標**:下載次數、流量、耗時、失敗率、熱門文件
- 上一篇文章: JavaWeb 文件上傳入門教程:Commons FileUpload 核心用法與最佳實踐
- 下一篇文章: MyBatis 自動建表與表結構同步:基於註解的模型驅動開發實戰
Copyright ©
程式碼之家 All Rights Reserved