@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String filePath = "/var/data/report.pdf"; // 實際路徑
File file = new File(filePath);
if (!file.exists()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 1. 設定回應 Header
resp.reset(); // 重置緩衝區
resp.setContentType("application/octet-stream"); // 通用二進制流
resp.setContentLengthLong(file.length()); // 支援大檔案
// 2. 處理檔案名編碼 (RFC 5987 / RFC 6266)
String fileName = file.getName();
String encodedName = encodeFileName(fileName, req.getHeader("User-Agent"));
resp.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName);
// 3. 高效傳輸 (NIO 零拷貝 Java 8+)
try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
java.io.OutputStream out = resp.getOutputStream()) {
// 直接通道傳輸,避免 JVM 堆記憶體拷貝
long transferred = 0;
long size = file.length();
while (transferred < size) {
transferred += channel.transferTo(transferred, size - transferred,
((java.nio.channels.WritableByteChannel) out));
}
out.flush();
}
}
/** 跨瀏覽器檔案名編碼 */
private String encodeFileName(String fileName, String userAgent) throws UnsupportedEncodingException {
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
.replaceAll("\\+", "%20"); // 空格編碼為 %20 而非 +
// 舊版 IE/Edge 特殊處理 (可選)
if (userAgent != null && userAgent.contains("MSIE")) {
return encoded;
}
return encoded; // 現代瀏覽器支援 filename*=UTF-8''
}
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> download(@PathVariable String id) {
File file = storageService.getFile(id);
if (file == null || !file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName)
.body(resource);
}
filename="...":舊標準,僅支援 ASCII/ISO-8859-1,中文會亂碼filename*=UTF-8''...:RFC 5987/6266 標準,現代瀏覽器 (Chrome, Firefox, Edge, Safari) 完美支援 UTF-8attachment; filename="fallback"; filename*=UTF-8''real| 方案 | 記憶體 | CPU | 適用場景 |
|---|---|---|---|
| byte[] 讀入記憶體 | 高 (OOM 風險) | 低 | < 10MB 小檔 |
| BufferedInputStream + BufferedOutputStream | 低 (緩衝區) | 中 | 通用中型檔 |
| FileChannel.transferTo() (零拷貝) | 極低 | 極低 | > 100MB 大檔、高併發 |
| HttpServletResponse.sendFile() (Servlet 5.0+) | 容器優化 | 容器優化 | Tomcat 10+/Jetty 11+ |
生產環境大檔下載建議實作 HTTP Range 請求:
// 簡易邏輯
String range = req.getHeader("Range"); // bytes=0-1023
if (range != null) {
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206
resp.setHeader("Accept-Ranges", "bytes");
resp.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + total);
// 只傳輸指定區間
}
filePath 在允許目錄下,使用 file.getCanonicalPath().startsWith(allowedDir)public class DownloadAction extends ActionSupport {
private String fileName;
private InputStream inputStream;
public String execute() {
File file = new File("/data/" + fileName);
inputStream = new FileInputStream(file);
return SUCCESS;
}
public InputStream getInputStream() { return inputStream; }
// struts.xml 配置 result type="stream"
}
Content-Type、Content-Length、Content-Dispositionfilename* 標準FileChannel.transferTo() 零拷貝或容器 sendFileResponseEntity<Resource> 最簡潔