基于SpringBoot实现多文件批量下载并打包为ZIP压缩包的完整解决方案

 更新时间:2026年02月03日 09:33:15   作者:qq_29757467  
在日常的 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(文件附件实体)

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

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

六、优化建议

  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 工具包。

以上就是基于SpringBoot实现多文件批量下载并打包为ZIP压缩包的完整解决方案的详细内容,更多关于SpringBoot多文件下载并打包为ZIP压缩包的资料请关注脚本之家其它相关文章!

相关文章

  • 分析ThreadLocal内存泄漏问题

    分析ThreadLocal内存泄漏问题

    ThreadLocal的作用是提供线程内的局部变量,这种变量在线程生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度,但是如果滥用ThreadLocal可能会导致内存泄漏,所以本文将为大家分析ThreadLocal内存泄漏问题
    2023-07-07
  • MyBatis-Plus动态表名使用selectPage方法不生效问题解析与解决方案

    MyBatis-Plus动态表名使用selectPage方法不生效问题解析与解决方案

    MyBatis-Plus是MyBatis的增强工具,动态表名是MyBatis-Plus的一个重要功能之一,一些开发者在使用selectPage方法时可能会遇到动态表名不生效的问题,本文将深入分析这个问题的原因,并提供相应的解决方案,需要的朋友可以参考下
    2023-12-12
  • 分布式Netty源码分析概览

    分布式Netty源码分析概览

    这篇文章主要为大家介绍了分布式Netty源码分析概览,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03
  • java spring mvc处理器映射器介绍

    java spring mvc处理器映射器介绍

    这篇文章主要介绍了java spring mvc处理器映射器,文章围绕equestMapping解析映射介绍展开源码内容,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-03-03
  • 为什么Java项目中别用!=null做判空

    为什么Java项目中别用!=null做判空

    本文主要介绍了为什么Java项目中别用!=null做判空,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • java求数组第二大元素示例

    java求数组第二大元素示例

    这篇文章主要介绍了java求数组第二大元素示例,需要的朋友可以参考下
    2014-04-04
  • springSecurity实现简单的登录功能

    springSecurity实现简单的登录功能

    这篇文章主要为大家详细介绍了springSecurity实现简单的登录功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • Spring MVC如何使用@RequestParam注解获取参数

    Spring MVC如何使用@RequestParam注解获取参数

    这篇文章主要介绍了Spring MVC实现使用@RequestParam注解获取参数方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • 聊聊单线程的Redis为何会快到飞起

    聊聊单线程的Redis为何会快到飞起

    Redis想必大家都或多或少听过吧,我们在工作学习中通常用它来作为缓存使用,既然是作为缓存,大家的第一反应肯定是:这家伙很快
    2022-02-02
  • Spring JPA联表查询之OneToMany源码解析

    Spring JPA联表查询之OneToMany源码解析

    这篇文章主要为大家介绍了Spring JPA联表查询之OneToMany源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04

最新评论