基于SpringBoot實(shí)現(xiàn)多文件批量下載并打包為ZIP壓縮包的完整解決方案
一、功能需求分析
我們要實(shí)現(xiàn)的核心功能:
- 前端傳入多個(gè)文件 ID,后端根據(jù) ID 查詢文件的存儲路徑
- 將這些文件讀取并打包成一個(gè) ZIP 壓縮包
- 通過 HTTP 響應(yīng)將 ZIP 包直接下載到客戶端
- 處理文件不存在、IO 異常等邊界情況
二、核心技術(shù)點(diǎn)
- SpringBoot Web:提供 HTTP 接口、處理請求響應(yīng)
- Java IO/NIO:讀取本地文件、操作輸出流
- java.util.zip:JDK 原生 ZIP 壓縮工具類,無需引入額外依賴
- 跨域處理:解決前后端分離架構(gòu)下的跨域問題
- 日志記錄:記錄下載操作日志(可選)
三、代碼實(shí)現(xiàn)
3.1 實(shí)體類:Attachment(文件附件實(shí)體)
首先定義文件附件實(shí)體類,對應(yīng)數(shù)據(jù)庫中存儲的文件信息:
package com.itl.project.common.attachment.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.itl.framework.aspectj.lang.annotation.Excel;
import com.itl.framework.web.domain.BaseEntity;
import java.util.Date;
/**
* 文件信息對象 sys_attachment
*
* @author itl
*/
@Data
public class Attachment extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 文件主鍵 */
private Long fileId;
/** 文件業(yè)務(wù)id */
@Excel(name = "文件業(yè)務(wù)id")
@TableField(exist = false)
private String fileBusinessId;
/** 文件key */
@Excel(name = "文件key")
private String fileKey;
/** 文件名稱 */
@Excel(name = "文件名稱")
private String fileName;
/** mime類型 */
@Excel(name = "mime類型")
private String mimeType;
/** 文件大小 */
@Excel(name = "文件大小")
private Long fileSize;
/** 文件路徑 */
@Excel(name = "文件路徑")
private String filePath;
/** 文件地址 */
@Excel(name = "文件地址")
private String fileUrl;
/** 文件后綴 */
@Excel(name = "文件后綴")
private String suffix;
/** 是否存在 */
private boolean exists = false;
/** 創(chuàng)建時(shí)間 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 版本號
*/
private String version;
/**
* 文件備注
*/
private String comment;
@TableField(exist = false)
private String fileIds;
}
3.2 核心工具類:CompressDownloadUtil(ZIP 壓縮工具)
封裝 ZIP 壓縮的核心邏輯,負(fù)責(zé)將多個(gè)文件打包到輸出流中:
package com.itl.common.utils;
import com.itl.project.common.attachment.domain.Attachment;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 文件壓縮下載工具類
* 功能:將多個(gè)文件壓縮到指定輸出流中,用于ZIP包下載
*/
public class CompressDownloadUtil {
/**
* 將多個(gè)文件壓縮到指定輸出流中
*
* @param lists 需要壓縮的文件列表
* @param outputStream 壓縮后的輸出流(直接關(guān)聯(lián)HTTP響應(yīng)輸出流)
*/
public static void compressZip(List<Attachment> lists, OutputStream outputStream) {
ZipOutputStream zipOutStream = null;
try {
// 包裝成ZIP格式輸出流,提升寫入效率
zipOutStream = new ZipOutputStream(new BufferedOutputStream(outputStream));
// 設(shè)置壓縮方法為DEFLATED(默認(rèn),有壓縮效果)
zipOutStream.setMethod(ZipOutputStream.DEFLATED);
// 循環(huán)處理每個(gè)文件
for (Attachment file : lists) {
java.io.File localFile = new java.io.File(file.getFilePath());
// 校驗(yàn)文件是否存在,避免空指針或文件找不到異常
if (localFile.exists() && localFile.isFile()) {
try (FileInputStream fileInputStream = new FileInputStream(localFile)) {
// 讀取文件字節(jié)數(shù)據(jù)
byte[] data = new byte[(int) localFile.length()];
fileInputStream.read(data);
// 創(chuàng)建ZIP條目(壓縮包內(nèi)的文件名稱)
ZipEntry zipEntry = new ZipEntry(file.getFileName());
zipEntry.setSize(localFile.length());
// 將條目寫入ZIP流
zipOutStream.putNextEntry(zipEntry);
zipOutStream.write(data);
// 關(guān)閉當(dāng)前條目
zipOutStream.closeEntry();
}
}
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("文件壓縮失敗:" + e.getMessage());
} finally {
// 關(guān)閉流(注意關(guān)閉順序:先關(guān)ZIP流,再關(guān)基礎(chǔ)輸出流)
try {
if (Objects.nonNull(zipOutStream)) {
zipOutStream.flush();
zipOutStream.close();
}
if (Objects.nonNull(outputStream)) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.3 控制器:文件下載接口
import com.itl.common.utils.CompressDownloadUtil;
import com.itl.project.common.attachment.domain.Attachment;
import com.itl.project.common.attachment.service.AttachmentService;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.UUID;
/**
* 文件下載控制器
*/
@RestController
@RequestMapping("/file")
public class FileDownloadController {
// 注入文件附件服務(wù)(實(shí)際項(xiàng)目中通過@Autowired注入)
private AttachmentService attachmentService;
/**
* 多文件下載為ZIP壓縮包
* @param fileid 多個(gè)文件ID,建議用逗號分隔(如:1,2,3)
* @param request HTTP請求
* @param response HTTP響應(yīng)
* @throws UnsupportedEncodingException 編碼異常
*/
@Log(title = "多文件下載", businessType = BusinessType.DOWNLOAD)
@CrossOrigin // 解決跨域問題(前后端分離必加)
@GetMapping("/downloadZip/{fileid}")
public void downloadZip(@PathVariable("fileid") String fileid,
HttpServletRequest request,
HttpServletResponse response) throws UnsupportedEncodingException {
// 1. 根據(jù)文件ID查詢文件列表
Attachment attachment = new Attachment();
attachment.setFileIds(fileid);
List<Attachment> attachmentList = attachmentService.selectAttachmentList(attachment);
// 2. 校驗(yàn)文件列表是否為空
if (attachmentList.isEmpty()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 3. 設(shè)置響應(yīng)頭,告訴瀏覽器下載文件
// 生成唯一的ZIP包名稱(避免重名)
String downloadName = UUID.randomUUID().toString().replaceAll("-", "") + ".zip";
// 解決文件名中文亂碼問題
String fileName = new String(downloadName.getBytes("UTF-8"), "ISO8859-1");
// 設(shè)置響應(yīng)頭
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 設(shè)置響應(yīng)內(nèi)容類型為二進(jìn)制流(通用文件下載類型)
response.setContentType("application/octet-stream");
// 禁用緩存
response.setHeader("Cache-Control", "no-cache");
// 4. 調(diào)用工具類壓縮文件并寫入響應(yīng)輸出流
try {
CompressDownloadUtil.compressZip(attachmentList, response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
四、關(guān)鍵細(xì)節(jié)說明
4.1 解決文件名中文亂碼
String fileName = new String(downloadName.getBytes("UTF-8"), "ISO8859-1");
4.2 流的關(guān)閉順序
在工具類中,關(guān)閉流的順序必須是:先關(guān)閉 ZipOutputStream,再關(guān)閉基礎(chǔ)的 OutputStream。因?yàn)?ZipOutputStream 是包裝流,關(guān)閉包裝流時(shí)會自動刷新緩沖區(qū),確保所有數(shù)據(jù)都寫入基礎(chǔ)流。
4.3 異常處理
- 校驗(yàn)文件是否存在:避免讀取不存在的文件導(dǎo)致 IO 異常
- 響應(yīng)狀態(tài)碼:文件不存在返回 404,服務(wù)器異常返回 500,前端可根據(jù)狀態(tài)碼給出友好提示
- try-with-resources:在讀取文件時(shí)使用try (FileInputStream fileInputStream = new FileInputStream(localFile)),自動關(guān)閉流,簡化代碼
4.4 跨域處理
- @CrossOrigin注解解決前后端分離架構(gòu)下的跨域問題,如果是全局跨域配置,可省略該注解。
五、前端調(diào)用示例(Axios)
// 前端下載ZIP包示例
function downloadZip(fileIds) {
// 創(chuàng)建a標(biāo)簽,通過GET請求下載
const a = document.createElement('a');
a.href = `/file/downloadZip/${fileIds}`;
a.download = '文件包.zip'; // 可選,優(yōu)先使用后端返回的文件名
a.click();
// 移除a標(biāo)簽
a.remove();
}
// 調(diào)用示例:下載ID為1,2,3的文件
downloadZip("1,2,3");
六、優(yōu)化建議
- 大文件處理:本文代碼適用于中小文件,若處理大文件,建議使用BufferedInputStream分塊讀取,避免一次性加載文件到內(nèi)存導(dǎo)致 OOM。
- 文件名去重:如果壓縮包內(nèi)有重名文件,可在文件名后添加序號(如:test.txt → test (1).txt)。
- 進(jìn)度條:大文件下載時(shí),可結(jié)合 WebSocket 實(shí)現(xiàn)下載進(jìn)度條。
- 權(quán)限控制:在接口中添加權(quán)限校驗(yàn),確保只有授權(quán)用戶才能下載文件。
- 日志完善:除了操作日志,可記錄文件下載的大小、耗時(shí)、用戶等信息,便于問題排查。
七、總結(jié)
本文基于 SpringBoot + JDK 原生 ZIP 工具類,實(shí)現(xiàn)了多文件批量下載并打包為 ZIP 壓縮包的功能,核心思路是:
查詢文件列表 → 2. 設(shè)置下載響應(yīng)頭 → 3. 讀取文件并壓縮 → 4. 寫入響應(yīng)輸出流。
該方案無需引入額外的壓縮依賴,基于 JDK 原生 API 實(shí)現(xiàn),輕量且穩(wěn)定,適用于大多數(shù) Web 項(xiàng)目的文件下載場景。如果有更復(fù)雜的壓縮需求(如加密、分卷壓縮),可考慮使用 Apache Commons Compress 工具包。
以上就是基于SpringBoot實(shí)現(xiàn)多文件批量下載并打包為ZIP壓縮包的完整解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot多文件下載并打包為ZIP壓縮包的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Cloud入門系列服務(wù)提供者總結(jié)
這篇文章主要介紹了Spring Cloud入門系列之服務(wù)提供者總結(jié),服務(wù)提供者使用Eureka Client組件創(chuàng)建 ,創(chuàng)建完成以后修改某文件,具體操作方法及實(shí)例代碼跟隨小編一起看看吧2021-06-06
SpringBoot全局異常處理之解決404/500錯(cuò)誤
在搭建項(xiàng)目框架的時(shí)候用的是springboot,想統(tǒng)一處理異常,但是發(fā)現(xiàn)404的錯(cuò)誤總是捕捉不到,總是返回的是springBoot自帶的錯(cuò)誤結(jié)果信息,這篇文章主要給大家介紹了關(guān)于SpringBoot全局異常處理之解決404/500錯(cuò)誤的相關(guān)資料,需要的朋友可以參考下2023-11-11
SpringBoot因?yàn)閖ackson版本問題啟動失敗的解決
SpringBoot啟動失敗解決Jackson版本沖突,通過調(diào)整pom.xml中的Jackson依賴版本,確保與SpringBoot兼容,避免啟動時(shí)的嵌入容器初始化異常2026-05-05
Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法
下面小編就為大家分享一篇Java文件讀取寫入后 md5值不變的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對大家有所幫助2017-11-11
SpringCloud Config使用本地倉庫及map注入
這篇文章主要介紹了SpringCloud Config使用本地倉庫及map注入,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
SpringBoot定時(shí)任務(wù)@Scheduled使用方式
本文主要介紹了SpringBoot中的定時(shí)任務(wù)的實(shí)現(xiàn)方法,通過@EnableScheduling開啟定時(shí)任務(wù),使用@Scheduled注解可以設(shè)置定時(shí)任務(wù)的觸發(fā)方式,文中詳細(xì)介紹了corn參數(shù)的使用方法和各種通配符的含義,并給出了示例代碼2026-04-04
java源碼解析之String類的compareTo(String otherString)方法
這篇文章主要給大家介紹了關(guān)于java源碼解析之String類的compareTo(String otherString)方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09
實(shí)現(xiàn)quartz定時(shí)器及quartz定時(shí)器原理介紹
Quartz是一個(gè)大名鼎鼎的Java版開源定時(shí)調(diào)度器,功能強(qiáng)悍,使用方便,下面我們看看如何使用它2013-12-12
Spring-全面詳解(學(xué)習(xí)總結(jié))
這篇文章主要介紹了詳解Spring框架入門,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望能給你帶來幫助2021-07-07

