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

JavaWeb 文件下載功能實現:流式傳輸、中文文件名、瀏覽器兼容完整方案

     編輯:Java 相關知識

核心下載方法

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 判斷邏輯

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

常見問題與解決方案

瀏覽器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 轉碼
問題原因解決
下載文件名亂碼編碼方式與瀏覽器不匹配 雙重 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、緩衝流、異常時確保流關閉
  • ✅ **監控指標**:下載次數、流量、耗時、失敗率、熱門文件
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved