JSP 直接輸出文件流供下載,下載完成後瀏覽器彈出的下載對話框關閉,但**響應頁面(空白頁)仍然存在**,用戶需手動關閉,體驗差。
Content-Disposition: attachment),瀏覽器識別為下載,彈出保存對話框。window.close()),但此時**響應體已被文件流佔用**,瀏覽器不會渲染後續 HTML/JS。
3. 若在同一頁面(target="_self")提交下載,頁面會跳轉為空白下載響應,無法執行關閉邏輯。
<!-- 列表頁 -->
<form action="download.jsp" method="post" target="_blank">
<input type="hidden" name="filename" value="report.pdf">
<button type="submit">下載</button>
</form>
原理:提交在新標籤頁打開,下載響應佔用新標籤頁,**原列表頁不受影響**。現代瀏覽器下載完成後自動關閉新標籤頁(或保留空白標籤,用戶可忽略)。
<iframe id="downloadFrame" name="downloadFrame" style="display:none;"></iframe>
<form action="download.jsp" method="post" target="downloadFrame">
<input type="hidden" name="filename" value="report.pdf">
<button type="submit">下載</button>
</form>
原理:下載響應加載到隱藏 Iframe,主頁面完全無感。需確保服務端響應頭正確,瀏覽器不會嘗試在 Iframe 中渲染文件流。
// 前端
async function downloadFile(filename) {
const response = await fetch('download.jsp?filename=' + encodeURIComponent(filename), {
method: 'GET',
credentials: 'include' // 攜帶 Cookie/Session
});
if (!response.ok) throw new Error('下載失敗');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename; // 指定下載文件名
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
// 綁定按鈕
document.getElementById('downloadBtn').onclick = () => downloadFile('report.pdf');
<!-- download.jsp 後端調整:支援 AJAX 跨域/同源 -->
<%
String filename = request.getParameter("filename");
File file = new File("C:/repository", filename);
// 安全校驗略...
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" +
java.net.URLEncoder.encode(filename, "UTF-8") + "\"");
response.setContentLengthLong(file.length());
try (FileChannel in = FileChannel.open(file.toPath(), StandardOpenOption.READ);
ReadableByteChannel out = Channels.newChannel(response.getOutputStream())) {
in.transferTo(0, file.length(), out); // 零拷貝
}
%>
優點:無新窗口/Iframe,下載進度可監控(response.body.getReader()),文件名完全由前端控制,體驗最佳。要求瀏覽器支援 Fetch + Blob(IE 需 Polyfill)。
<%@ page import="java.io.*, java.nio.channels.*" contentType="application/octet-stream" %>
<%
// 1. 參數獲取與安全校驗
String filename = request.getParameter("filename");
if (filename == null || filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "非法文件名");
return;
}
String basePath = "C:/repository";
File file = new File(basePath, filename).getCanonicalFile();
File baseDir = new File(basePath).getCanonicalFile();
if (!file.getPath().startsWith(baseDir.getPath()) || !file.isFile()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// 2. 響應頭設置 (關鍵:必須在任何輸出前)
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" +
java.net.URLEncoder.encode(file.getName(), "UTF-8") + "\"");
response.setContentLengthLong(file.length());
response.setHeader("Cache-Control", "private, max-age=0, must-revalidate"); // 防緩存
response.setHeader("Pragma", "no-cache");
// 3. 高效流傳輸 (NIO 零拷貝)
try (FileChannel in = FileChannel.open(file.toPath(), StandardOpenOption.READ);
ReadableByteChannel out = Channels.newChannel(response.getOutputStream())) {
in.transferTo(0, file.length(), out);
}
// 4. 絕對不要在這裡寫任何 HTML/JS/out.write(),響應體已被文件流佔用!
%>
| 錯誤代碼 | 後果 | 修正 |
|---|---|---|
out.println(...) 或 HTML 代碼在流輸出後 | 文件內容被文本污染,下載損壞 | JSP 僅輸出二進制流,移除所有模板內容,或改用 Servlet |
未設置 Content-Length | 瀏覽器無法顯示進度,大文件可能下載不全 | response.setContentLengthLong(file.length()) |
| 文件名中文未編碼 | 下載文件名亂碼 | URLEncoder.encode(name, "UTF-8") |
使用 response.getWriter() 而非 getOutputStream() | 字符流編碼破壞二進制文件 | 強制使用 getOutputStream() / Channels.newChannel() |
| 未關閉流 | 連接洩漏、文件句柄耗盡 | try-with-resources 自動關閉 |
target="_blank" 或 Fetch+Blob:消除「頁面不關閉」問題,提升用戶體驗。Range 請求頭解析,返回 206 Partial Content。