【实战教程】SpringBoot 实现多文件批量下载并打包为 ZIP 压缩包

在日常的 Web 开发中,文件下载是非常常见的功能需求,而多文件批量下载并打包为 ZIP 压缩包 更是高频场景(比如批量下载合同、图片、报表等)。本文将基于 SpringBoot 框架,手把手教你实现这一功能,从核心思路到完整代码,让你快速掌握。

一、功能需求分析

我们要实现的核心功能:

  1. 前端传入多个文件 ID,后端根据 ID 查询文件的存储路径
  2. 将这些文件读取并打包成一个 ZIP 压缩包
  3. 通过 HTTP 响应将 ZIP 包直接下载到客户端
  4. 处理文件不存在、IO 异常等边界情况

二、核心技术点

  • SpringBoot Web:提供 HTTP 接口、处理请求响应
  • Java IO/NIO:读取本地文件、操作输出流
  • java.util.zip:JDK 原生 ZIP 压缩工具类,无需引入额外依赖
  • 跨域处理:解决前后端分离架构下的跨域问题
  • 日志记录:记录下载操作日志(可选)

三、代码实现

3.1 实体类:Attachment(文件附件实体)

首先定义文件附件实体类,对应数据库中存储的文件信息:

bash 复制代码
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 压缩的核心逻辑,负责将多个文件打包到输出流中:

bash 复制代码
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 控制器:文件下载接口

bash 复制代码
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 解决文件名中文乱码

bash 复制代码
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)

bash 复制代码
// 前端下载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");

六、优化建议

  1. 大文件处理:本文代码适用于中小文件,若处理大文件,建议使用BufferedInputStream分块读取,避免一次性加载文件到内存导致 OOM。
  2. 文件名去重:如果压缩包内有重名文件,可在文件名后添加序号(如:test.txt → test (1).txt)。
  3. 进度条:大文件下载时,可结合 WebSocket 实现下载进度条。
  4. 权限控制:在接口中添加权限校验,确保只有授权用户才能下载文件。
  5. 日志完善:除了操作日志,可记录文件下载的大小、耗时、用户等信息,便于问题排查。

七、总结

本文基于 SpringBoot + JDK 原生 ZIP 工具类,实现了多文件批量下载并打包为 ZIP 压缩包的功能,核心思路是:

查询文件列表 → 2. 设置下载响应头 → 3. 读取文件并压缩 → 4. 写入响应输出流。

该方案无需引入额外的压缩依赖,基于 JDK 原生 API 实现,轻量且稳定,适用于大多数 Web 项目的文件下载场景。如果有更复杂的压缩需求(如加密、分卷压缩),可考虑使用 Apache Commons Compress 工具包。

希望本文对你有所帮助,如有问题欢迎在评论区交流~

相关推荐
神奇小汤圆16 小时前
SpringBoot实现微信登录,SoEasy!
后端
你住过的屋檐16 小时前
【Java】虚拟线程详解
java·开发语言
逍遥德16 小时前
Maven教程.02-基础-pom.xml 使用标签大全
java·后端·maven·软件构建
甲枫叶16 小时前
【claude热点资讯】Claude Code 更新:手机遥控电脑开发,Remote Control 功能上线
java·人工智能·智能手机·产品经理·ai编程
额,不知道写啥。16 小时前
P5354 [Ynoi Easy Round 2017] 由乃的 OJ
java·开发语言·算法
神奇小汤圆16 小时前
为什么Java里面,Service层不直接返回Result对象?
后端
让我上个超影吧17 小时前
消息队列——RabbitMQ(高级)
java·rabbitmq
Charlie_lll17 小时前
Redis脑裂问题处理——基于min-replicas-to-write配置
redis·后端
得物技术17 小时前
Sentinel Java客户端限流原理解析|得物技术
java·后端·架构
PM老周17 小时前
2026年软硬件一体化项目管理软件怎么选?多款工具对比测评
java·安全·硬件工程·团队开发·个人开发