文件下載是 Web 開發高頻功能。本文系統整理 Java Servlet 實現下載的四種核心方式,涵蓋本地檔、網路檔、支援在線預覽 (inline) 與純附件下載 (attachment),並補充關鍵 Header 設定、編碼處理、大檔流式傳輸與斷點續傳基礎知識。
| Header | 用途 | 範例值 |
|---|---|---|
| Content-Type | MIME 類型 | application/octet-stream (強制下載) / image/jpeg (預覽) |
| Content-Disposition | 瀏覽器處理方式 | attachment; filename="test.pdf" / inline; filename="test.pdf" |
| Content-Length | 檔案大小 (bytes) | 102400 |
| Accept-Ranges | 支援斷點續傳 | bytes |
| Content-Range | 斷點續傳範圍 | bytes 0-1023/102400 |
public void download(HttpServletResponse response, String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 檔名編碼:RFC 5987 格式,相容中文
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);
response.setHeader("Content-Length", String.valueOf(file.length()));
response.setHeader("Accept-Ranges", "bytes");
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush();
}
}
public void downloadFlexible(HttpServletResponse response, String filePath, boolean inline)
throws IOException {
File file = new File(filePath);
if (!file.exists()) { response.sendError(404); return; }
String mime = URLConnection.guessContentTypeFromName(file.getName());
if (mime == null) mime = "application/octet-stream";
response.reset();
response.setContentType(mime);
String disposition = inline ? "inline" : "attachment";
String encoded = URLEncoder.encode(file.getName(), "UTF-8").replace("+", "%20");
response.setHeader("Content-Disposition", disposition + "; filename*=UTF-8''" + encoded);
response.setContentLengthLong(file.length());
try (InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = response.getOutputStream()) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) out.write(buf, 0, len);
}
}
public void downloadRemote(HttpServletResponse response, String remoteUrl, String saveAs)
throws IOException {
URL url = new URL(remoteUrl);
try (InputStream in = url.openStream();
OutputStream out = response.getOutputStream()) {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + saveAs);
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) out.write(buf, 0, len);
}
}
@WebServlet("/async-download")
public class AsyncDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
String filePath = req.getParameter("path");
File file = new File(filePath);
if (!file.exists()) { resp.sendError(404); return; }
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
resp.setContentLengthLong(file.length());
// 開啟非阻塞模式
ServletOutputStream out = resp.getOutputStream();
out.setWriteListener(new WriteListener() {
private final FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
private final ByteBuffer buffer = ByteBuffer.allocate(8192);
@Override public void onWritePossible() throws IOException {
while (out.isReady()) {
int read = channel.read(buffer);
if (read == -1) {
out.write(buffer.array(), 0, buffer.position());
out.close();
channel.close();
return;
}
buffer.flip();
out.write(buffer.array(), 0, buffer.limit());
buffer.clear();
}
}
@Override public void onError(Throwable t) { t.printStackTrace(); }
});
}
}
filename*=UTF-8''encodedName (RFC 5987),避免 IE/Edge/舊版瀏覽器中文亂碼。Files.readAllBytes() 或 byte[] buffer = new byte[(int)file.length()] 導致 OOM。Range Header,回傳 206 Partial Content + Content-Range,建議成熟專案直接用 HttpRange 庫或 Nginx X-Accel-Redirect 卸載。filePath 防止目錄遍歷 (如 ../../etc/passwd),建議用預設下載目錄 + 檔名映射表。掌握 Content-Disposition 兩種模式、filename* 編碼、流式傳輸、斷點續傳 Header,即能應對 90% 下載場景。生產環境建議:小檔用標準流式、大檔配合 Nginx sendfile / X-Accel-Redirect 卸載應用伺服器、敏感檔案加簽名 URL 限時下載。