基于SpringBoot实现多文件批量下载并打包为ZIP压缩包的完整解决方案
一、功能需求分析
我们要实现的核心功能:
- 前端传入多个文件 ID,后端根据 ID 查询文件的存储路径
- 将这些文件读取并打包成一个 ZIP 压缩包
- 通过 HTTP 响应将 ZIP 包直接下载到客户端
- 处理文件不存在、IO 异常等边界情况
二、核心技术点
- SpringBoot Web:提供 HTTP 接口、处理请求响应
- Java IO/NIO:读取本地文件、操作输出流
- java.util.zip:JDK 原生 ZIP 压缩工具类,无需引入额外依赖
- 跨域处理:解决前后端分离架构下的跨域问题
- 日志记录:记录下载操作日志(可选)
三、代码实现
3.1 实体类:Attachment(文件附件实体)
首先定义文件附件实体类,对应数据库中存储的文件信息:
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;
/** 文件业务id */
@Excel(name = "文件业务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;
/** 创建时间 */
@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 压缩的核心逻辑,负责将多个文件打包到输出流中:
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;
/**
* 文件压缩下载工具类
* 功能:将多个文件压缩到指定输出流中,用于ZIP包下载
*/
public class CompressDownloadUtil {
/**
* 将多个文件压缩到指定输出流中
*
* @param lists 需要压缩的文件列表
* @param outputStream 压缩后的输出流(直接关联HTTP响应输出流)
*/
public static void compressZip(List<Attachment> lists, OutputStream outputStream) {
ZipOutputStream zipOutStream = null;
try {
// 包装成ZIP格式输出流,提升写入效率
zipOutStream = new ZipOutputStream(new BufferedOutputStream(outputStream));
// 设置压缩方法为DEFLATED(默认,有压缩效果)
zipOutStream.setMethod(ZipOutputStream.DEFLATED);
// 循环处理每个文件
for (Attachment file : lists) {
java.io.File localFile = new java.io.File(file.getFilePath());
// 校验文件是否存在,避免空指针或文件找不到异常
if (localFile.exists() && localFile.isFile()) {
try (FileInputStream fileInputStream = new FileInputStream(localFile)) {
// 读取文件字节数据
byte[] data = new byte[(int) localFile.length()];
fileInputStream.read(data);
// 创建ZIP条目(压缩包内的文件名称)
ZipEntry zipEntry = new ZipEntry(file.getFileName());
zipEntry.setSize(localFile.length());
// 将条目写入ZIP流
zipOutStream.putNextEntry(zipEntry);
zipOutStream.write(data);
// 关闭当前条目
zipOutStream.closeEntry();
}
}
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("文件压缩失败:" + e.getMessage());
} finally {
// 关闭流(注意关闭顺序:先关ZIP流,再关基础输出流)
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 {
// 注入文件附件服务(实际项目中通过@Autowired注入)
private AttachmentService attachmentService;
/**
* 多文件下载为ZIP压缩包
* @param fileid 多个文件ID,建议用逗号分隔(如:1,2,3)
* @param request HTTP请求
* @param response HTTP响应
* @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. 根据文件ID查询文件列表
Attachment attachment = new Attachment();
attachment.setFileIds(fileid);
List<Attachment> attachmentList = attachmentService.selectAttachmentList(attachment);
// 2. 校验文件列表是否为空
if (attachmentList.isEmpty()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 3. 设置响应头,告诉浏览器下载文件
// 生成唯一的ZIP包名称(避免重名)
String downloadName = UUID.randomUUID().toString().replaceAll("-", "") + ".zip";
// 解决文件名中文乱码问题
String fileName = new String(downloadName.getBytes("UTF-8"), "ISO8859-1");
// 设置响应头
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 设置响应内容类型为二进制流(通用文件下载类型)
response.setContentType("application/octet-stream");
// 禁用缓存
response.setHeader("Cache-Control", "no-cache");
// 4. 调用工具类压缩文件并写入响应输出流
try {
CompressDownloadUtil.compressZip(attachmentList, response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
四、关键细节说明
4.1 解决文件名中文乱码
String fileName = new String(downloadName.getBytes("UTF-8"), "ISO8859-1");
4.2 流的关闭顺序
在工具类中,关闭流的顺序必须是:先关闭 ZipOutputStream,再关闭基础的 OutputStream。因为 ZipOutputStream 是包装流,关闭包装流时会自动刷新缓冲区,确保所有数据都写入基础流。
4.3 异常处理
- 校验文件是否存在:避免读取不存在的文件导致 IO 异常
- 响应状态码:文件不存在返回 404,服务器异常返回 500,前端可根据状态码给出友好提示
- try-with-resources:在读取文件时使用try (FileInputStream fileInputStream = new FileInputStream(localFile)),自动关闭流,简化代码
4.4 跨域处理
- @CrossOrigin注解解决前后端分离架构下的跨域问题,如果是全局跨域配置,可省略该注解。
五、前端调用示例(Axios)
// 前端下载ZIP包示例
function downloadZip(fileIds) {
// 创建a标签,通过GET请求下载
const a = document.createElement('a');
a.href = `/file/downloadZip/${fileIds}`;
a.download = '文件包.zip'; // 可选,优先使用后端返回的文件名
a.click();
// 移除a标签
a.remove();
}
// 调用示例:下载ID为1,2,3的文件
downloadZip("1,2,3");
六、优化建议
- 大文件处理:本文代码适用于中小文件,若处理大文件,建议使用BufferedInputStream分块读取,避免一次性加载文件到内存导致 OOM。
- 文件名去重:如果压缩包内有重名文件,可在文件名后添加序号(如:test.txt → test (1).txt)。
- 进度条:大文件下载时,可结合 WebSocket 实现下载进度条。
- 权限控制:在接口中添加权限校验,确保只有授权用户才能下载文件。
- 日志完善:除了操作日志,可记录文件下载的大小、耗时、用户等信息,便于问题排查。
七、总结
本文基于 SpringBoot + JDK 原生 ZIP 工具类,实现了多文件批量下载并打包为 ZIP 压缩包的功能,核心思路是:
查询文件列表 → 2. 设置下载响应头 → 3. 读取文件并压缩 → 4. 写入响应输出流。
该方案无需引入额外的压缩依赖,基于 JDK 原生 API 实现,轻量且稳定,适用于大多数 Web 项目的文件下载场景。如果有更复杂的压缩需求(如加密、分卷压缩),可考虑使用 Apache Commons Compress 工具包。
以上就是基于SpringBoot实现多文件批量下载并打包为ZIP压缩包的完整解决方案的详细内容,更多关于SpringBoot多文件下载并打包为ZIP压缩包的资料请关注脚本之家其它相关文章!
相关文章
MyBatis-Plus动态表名使用selectPage方法不生效问题解析与解决方案
MyBatis-Plus是MyBatis的增强工具,动态表名是MyBatis-Plus的一个重要功能之一,一些开发者在使用selectPage方法时可能会遇到动态表名不生效的问题,本文将深入分析这个问题的原因,并提供相应的解决方案,需要的朋友可以参考下2023-12-12
Spring MVC如何使用@RequestParam注解获取参数
这篇文章主要介绍了Spring MVC实现使用@RequestParam注解获取参数方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-10-10


最新评论