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

JavaWeb 文件批量打包下載:ZIP 流式壓縮與響應輸出實戰

     編輯:Java 相關知識

業務場景

教師批量下載學生作業附件:根據作業 ID 查詢所有提交記錄,遍歷附件 URL 下載文件,打包成 ZIP 返回前端下載。

核心流程

  1. 查詢作業詳情與提交列表
  2. 獲取班級學生名單,構建「學生 → 提交記錄」映射
  3. 篩選已提交且有附件的記錄
  4. 創建臨時 ZIP 文件,流式寫入
  5. 將 ZIP 文件流式輸出至 HTTP 響應
  6. 清理臨時文件

完整 Controller 實現

@RestController
@RequestMapping("/assignment")
public class AssignmentController {
    
    @Autowired AssignmentService assignmentService;
    @Autowired MemberService memberService;
    @Autowired AnnounceService announceService;
    
    @GetMapping("/downloadAllHomework")
    public void downloadAllHomework(HttpSession session, HttpServletRequest request,
                                    HttpServletResponse response,
                                    @RequestParam String assignmentId,
                                    @RequestParam int classCode) throws Exception {
        
        Site site = (Site) session.getAttribute("site");
        String siteId = site.getId();
        
        // 1. 獲取作業詳情
        AssignmentDetail detail = assignmentService.getDetail(assignmentId);
        
        // 2. 獲取所有提交記錄
        List submits = assignmentService.getSubmitted(assignmentId);
        List submitIds = submits.stream()
            .map(AssignmentSubmit::getId)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
        
        // 3. 獲取提交詳情 (含附件)
        Map submitMap = new HashMap<>();
        for (String sid : submitIds) {
            AssignmentSubmit s = assignmentService.getSubmitDetail(sid);
            submitMap.put(s.getUserName(), s);
        }
        
        // 4. 獲取班級學生,合併提交信息
        List students = memberService.getStudents(siteId, classCode);
        List exportList = new ArrayList<>();
        for (MemberVO stu : students) {
            AssignmentSubmit s = submitMap.getOrDefault(stu.getId(), new AssignmentSubmit());
            s.setRealName(stu.getName());
            s.setUserName(stu.getId());
            exportList.add(s);
        }
        
        // 5. 篩選有附件的提交
        List withFiles = exportList.stream()
            .filter(s -> s.getAttachIDs() != null && !s.getAttachIDs().isEmpty())
            .collect(Collectors.toList());
        
        // 6. 創建臨時 ZIP 文件
        File zipFile = File.createTempFile("homework_", ".zip");
        zipFile.deleteOnExit(); // JVM 退出時刪除
        
        try (FileOutputStream fos = new FileOutputStream(zipFile);
             ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fos))) {
            
            for (AssignmentSubmit submit : withFiles) {
                for (AttachIDs attach : submit.getAttachIDs()) {
                    try {
                        // 6.1 獲取文件元數據
                        AttachmentMeta meta = announceService.getAttachment(attach.getId());
                        String fileName = meta.getName();
                        String fileUrl = meta.getUrlUpload();
                        
                        // 6.2 構建雲存儲下載 URL
                        String cloudUrl = "https://storage.example.com:8066/ImageService/DownloadFile/"
                            + fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
                        
                        // 6.3 解決 ZIP 中文文件名編碼 (GBK 兼容 Windows)
                        ZipEntry entry = new ZipEntry(
                            new String(fileName.getBytes("GBK"), "ISO-8859-1"));
                        zos.putNextEntry(entry);
                        
                        // 6.4 流式下載寫入 ZIP
                        try (InputStream is = downloadFile(cloudUrl)) {
                            byte[] buffer = new byte[8192];
                            int len;
                            while ((len = is.read(buffer)) > 0) {
                                zos.write(buffer, 0, len);
                            }
                        }
                        zos.closeEntry();
                    } catch (Exception e) {
                        log.error("附件處理失敗: {}", attach.getId(), e);
                    }
                }
            }
            zos.finish(); // 完成壓縮
        }
        
        // 7. 流式輸出 ZIP 文件
        downloadZip(zipFile, response);
    }
    
    private InputStream downloadFile(String url) throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setConnectTimeout(10000);
        conn.setReadTimeout(30000);
        return conn.getInputStream();
    }
    
    private void downloadZip(File file, HttpServletResponse response) throws IOException {
        response.reset();
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", 
            "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
        response.setContentLengthLong(file.length());
        
        try (FileInputStream fis = new FileInputStream(file);
             BufferedInputStream bis = new BufferedInputStream(fis);
             OutputStream os = new BufferedOutputStream(response.getOutputStream())) {
            
            byte[] buffer = new byte[8192];
            int len;
            while ((len = bis.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            os.flush();
        } finally {
            // 異步刪除臨時文件 (響應完成後)
            CompletableFuture.runAsync(() -> {
                try { Files.deleteIfExists(file.toPath()); } catch (IOException ignored) {}
            });
        }
    }
}

關鍵技術點解析

技術點實現細節
流式壓縮`ZipOutputStream` 直接寫入 `FileOutputStream`,無需將所有文件載入記憶體
中文文件名編碼`new String(name.getBytes("GBK"), "ISO-8859-1")` 兼容 Windows 資源管理器預覽
大文件支援8KB 緩衝區分塊讀寫,避免 OOM;`setContentLengthLong` 支持 >2GB
遠程文件下載`HttpURLConnection` 獲取輸入流,直接管道寫入 ZIP,無本地緩存
異常隔離單個附件失敗不影響其他,記錄錯誤日誌繼續處理
臨時文件清理`deleteOnExit()` + 異步 `Files.deleteIfExists()` 雙重保障
響應頭設置`application/zip` + `attachment` 強制瀏覽器下載而非預覽

優化方向

  • 異步預生成:大量文件時,後台任務預生成 ZIP,前端輪詢下載鏈接
  • 斷點續傳:支援 `Range` 請求,大 ZIP 分片下載
  • 對象存儲簽名 URL:替換直接下載為預簽名 URL,減輕應用服務器帶寬
  • 壓縮級別: `zos.setLevel(Deflater.BEST_COMPRESSION)` 或 `BEST_SPEED`
  • 進度回調:WebSocket/Server-Sent Events 推送壓縮/下載進度

前端調用示例

// 普通表單提交 (非 AJAX,因涉及文件下載)
function downloadAllHomework(assignmentId, classCode) {
    const url = `/assignment/downloadAllHomework?assignmentId=${assignmentId}&classCode=${classCode}`;
    window.location.href = url; // 瀏覽器原生下載
}

// 或使用 a 標籤
批量下載作業

常見問題排查

現象原因解決
ZIP 解壓亂碼編碼不匹配Windows 用 GBK,Linux/Mac 用 UTF-8,可嘗試 `ZipEntry.setExtra()` 存 UTF-8 標誌
下載中斷超時/連接重置增加連接/讀取超時,啟用斷點續傳,檢查代理/防火牆
文件名截斷響應頭編碼錯誤`URLEncoder.encode(fileName, "UTF-8")`,避免手動拼接
內存溢出緩衝區過大/文件過多縮小 buffer (4-8KB),分批處理,使用臨時文件而非 ByteArrayOutputStream
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved