首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JSP 實現文件下載的四種方案完整對比與代碼實戰

     編輯:Java 相關知識

方案對比總覽

方案核心機制優點缺點適用場景
1. 直接鏈接<a href="/files/xxx.pdf">極簡、無服務端壓力、支援斷點續傳路徑暴露、無權限控制、無下載統計靜態資源、公開文件、CDN 分發
2. 流式傳輸 (Response OutputStream)Servlet/JSP 讀文件寫 response.getOutputStream()權限可控、邏輯靈活、支援動態生成佔用請求線程、大文件易 OOM、無斷點續傳受控下載、動態報表、中小文件
3. Applet 客戶端下載Applet 接收 Servlet 流並寫本地磁盤客戶端寫入、可實現進度條、多線程下載需簽名信任、瀏覽器棄用 Java 插件、維護成本極高舊系統維護、特殊客戶端需求(極不推薦新項目)
4. 現代替代:前端 Blob + ObjectURL / Service WorkerFetch/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 標準)
  • 緩衝區大小:8KB-64KB 平衡內存與系統調用開銷
  • 斷點續傳:需解析 Range 請求頭,返回 206 Partial Content,建議用成熟庫 (如 RangeFileInputStream) 或 Nginx X-Accel-Redirect

方案三:Applet 方案(僅供歷史參考,新項目禁用)

Servlet 端

@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);
        }
    }
}

Applet 端 (JNLP 部署)

// 核心邏輯:接收流 -> 寫本地文件 -> 可選打開
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);

方案四:現代前端下載最佳實踐 (Vue/React 通用)

// 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 X-Accel-Redirect / X-Sendfile

將文件傳輸卸載給 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 線程瞬間釋放、零拷貝內核傳輸、自動支援斷點續傳、高併發下載零壓力。

總結選型指南

  • 新項目/現代化改造:前端 Blob 下載 + 後端 Nginx X-Accel-Redirect(最佳體驗與性能)
  • 傳統 JSP/Servlet 維護:流式傳輸方案,加上路徑校驗、緩衝流、Content-Disposition 雙重編碼
  • 大文件/斷點續傳剛需:必須實現 Range 請求處理,或直接走 Nginx 內建支援
  • Applet 方案:歷史包袱,勿用於新開發
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved