SpringBoot实现多种来源的Zip多层目录打包下载

 更新时间:2025年04月30日 10:51:40   作者:颇有几分姿色  
这篇文章主要为大家详细介绍了SpringBoot如何实现多种来源的 Zip 多层目录打包下载,包括本地文件和HTTP混合,感兴趣的小伙伴可以跟随小编一起学习一下

需要将一批文件(可能分布在不同目录、不同来源)打包成Zip格式,按目录结构导出给用户下载。

1. 核心思路

支持将本地服务器上的文件(如/data/upload/xxx.jpg)打包进Zip,保持原有目录结构。

支持通过HTTP下载远程文件写入Zip。

所有写入Zip的目录名、文件名均需安全处理。

统一使用流式IO,适合大文件/大量文件导出,防止内存溢出。

目录下无文件时写入empty.txt标识。

2. 代码实现

2.1 工具类:本地&HTTP两种方式写入Zip

package com.example.xiaoshitou.utils;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:34
 **/
public class ZipDownloadUtils {


    private static final String SUFFIX_ZIP = ".zip";
    private static final String UNNAMED = "未命名";
    /**
     * 安全处理文件名/目录名
     * @param name
     * @return
     */
    public static String safeName(String name) {
        if (name == null) return "null";
        return name.replaceAll("[\\\\/:*?\"<>|]", "_");
    }

    /**
     * HTTP下载写入Zip
     * @param zipOut
     * @param fileUrl
     * @param zipEntryName
     * @throws IOException
     */
    public static void writeHttpFileToZip(ZipArchiveOutputStream zipOut, String fileUrl, String zipEntryName) throws IOException {
        ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);
        zipOut.putArchiveEntry(entry);
        try (InputStream in = openHttpStream(fileUrl, 8000, 20000)) {
            byte[] buffer = new byte[4096];
            int len;
            while ((len = in.read(buffer)) != -1) {
                zipOut.write(buffer, 0, len);
            }
        } catch (Exception e) {
            zipOut.write(("下载失败: " + fileUrl).getBytes(StandardCharsets.UTF_8));
        }
        zipOut.closeArchiveEntry();
    }

    /**
     * 本地文件写入Zip
     * @param zipOut
     * @param localFilePath
     * @param zipEntryName
     * @throws IOException
     */
    public static void writeLocalFileToZip(ZipArchiveOutputStream zipOut, String localFilePath, String zipEntryName) throws IOException {
        File file = new File(localFilePath);
        if (!file.exists() || file.isDirectory()) {
            writeTextToZip(zipOut, zipEntryName + "_empty.txt", "文件不存在或是目录: " + localFilePath);
            return;
        }

        ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);
        zipOut.putArchiveEntry(entry);
        try (InputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[4096];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                zipOut.write(buffer, 0, len);
            }
        }
        zipOut.closeArchiveEntry();
    }

    /**
     * 写入文本文件到Zip(如empty.txt)
     * @param zipOut
     * @param zipEntryName
     * @param content
     * @throws IOException
     */
    public static void writeTextToZip(ZipArchiveOutputStream zipOut, String zipEntryName, String content) throws IOException {
        ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);
        zipOut.putArchiveEntry(entry);
        zipOut.write(content.getBytes(StandardCharsets.UTF_8));
        zipOut.closeArchiveEntry();
    }

    /**
     * 打开HTTP文件流
     * @param url
     * @param connectTimeout
     * @param readTimeout
     * @return
     * @throws IOException
     */
    public static InputStream openHttpStream(String url, int connectTimeout, int readTimeout) throws IOException {
        URLConnection conn = new URL(url).openConnection();
        conn.setConnectTimeout(connectTimeout);
        conn.setReadTimeout(readTimeout);
        return conn.getInputStream();
    }

    /**
     * 从url获取文件名
     * @param url
     * @return
     * @throws IOException
     */
    public static String getFileName(String url)  {
        return url.substring(url.lastIndexOf('/')+1);
    }

    /**
     * 设置response
     * @param request
     * @param response
     * @param fileName
     * @throws UnsupportedEncodingException
     */
    public static void setResponse(HttpServletRequest request, HttpServletResponse response, String fileName) throws UnsupportedEncodingException {
        if (!StringUtils.hasText(fileName)) {
            fileName = LocalDate.now() + UNNAMED;
        }
        if (!fileName.endsWith(SUFFIX_ZIP)) {
            fileName = fileName + SUFFIX_ZIP;
        }
        response.setHeader("Connection", "close");
        response.setHeader("Content-Type", "application/octet-stream;charset=UTF-8");
        String filename = encodeFileName(request, fileName);
        response.setHeader("Content-Disposition", "attachment;filename=" + filename);
    }

    /**
     * 文件名在不同浏览器兼容处理
     * @param request 请求信息
     * @param fileName 文件名
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String encodeFileName(HttpServletRequest request, String fileName) throws UnsupportedEncodingException {
        String userAgent = request.getHeader("USER-AGENT");
        // 火狐浏览器
        if (userAgent.contains("Firefox") || userAgent.contains("firefox")) {
            fileName = new String(fileName.getBytes(), "ISO8859-1");
        } else {
            // 其他浏览器
            fileName = URLEncoder.encode(fileName, "UTF-8");
        }
        return fileName;
    }
}

2.2 Controller 示例:按本地目录结构批量导出

假设有如下导出结构:

用户A/
    身份证/
        xxx.jpg (本地)
        xxx.png (本地)
    头像/
        xxx.jpg (HTTP)
用户B/
    empty.txt

模拟数据结构:

zipGroup:

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.List;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:36
 **/
@Data
@AllArgsConstructor
public class ZipGroup {
    /**
     * 用户名、文件名
     */
    private String dirName;
    private List<ZipSubDir> subDirs;
}

zipGroupDir:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:37
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipSubDir {
    /**
     * 子目录
     */
    private String subDirName;
    private List<ZipFileRef> fileRefs;
}

ZipFileRef:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:38
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipFileRef {
    /**
     * 文件名
     */
    private String name;
    /**
     * 本地路径
     */
    private String localPath;
    /**
     * http路径
     */
    private String httpUrl;
}

Controller通用代码:

package com.example.xiaoshitou.controller;

import com.example.xiaoshitou.service.ZipService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:50
 **/
@RestController
@RequestMapping("/zip")
@AllArgsConstructor
public class ZipController {

    private final ZipService zipService;

    /**
     *  打包下载
     * @param response
     */
    @GetMapping("/download")
    public void downloadZip(HttpServletRequest request, HttpServletResponse response) {
        zipService.downloadZip(request,response);
    }
}

Service 层代码:

package com.example.xiaoshitou.service.impl;

import com.example.xiaoshitou.entity.ZipFileRef;
import com.example.xiaoshitou.entity.ZipGroup;
import com.example.xiaoshitou.entity.ZipSubDir;
import com.example.xiaoshitou.service.ZipService;
import com.example.xiaoshitou.utils.ZipDownloadUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.zip.Deflater;

/***
 * @title
 * @author shijiangyong
 * @date 2025/4/28 16:43
 **/
@Slf4j
@Service
public class ZipServiceImpl implements ZipService {
    @Override
    public void downloadZip(HttpServletRequest request, HttpServletResponse response) {
        // ==== 示例数据 ====
        List<ZipGroup> data = Arrays.asList(
                new ZipGroup("小明", Arrays.asList(
                        new ZipSubDir("身份证(本地)", Arrays.asList(
                                new ZipFileRef("","E:/software/test/1.png",""),
                                new ZipFileRef("","E:/software/test/2.png","")
                        )),
                        new ZipSubDir("头像(http)", Arrays.asList(
                                // 百度随便找的
                                new ZipFileRef("","","https://pic4.zhimg.com/v2-4d9e9f936b9968f53be22b594aafa74f_r.jpg")
                        ))
                )),
                new ZipGroup("小敏", Collections.emptyList())
        );

        try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
             ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) {
            String fileName = "资料打包_" + System.currentTimeMillis() + ".zip";
            ZipDownloadUtils.setResponse(request,response, fileName);
            // 快速压缩
            zipOut.setLevel(Deflater.BEST_SPEED);
            for (ZipGroup group : data) {
                String groupDir = ZipDownloadUtils.safeName(group.getDirName()) + "/";
                List<ZipSubDir> subDirs = group.getSubDirs();
                if (subDirs == null || subDirs.isEmpty()) {
                    groupDir = ZipDownloadUtils.safeName(group.getDirName()) + "(无资料)/";
                    ZipDownloadUtils.writeTextToZip(zipOut, groupDir + "empty.txt", "该目录无任何资料");
                    continue;
                }
                for (ZipSubDir subDir : subDirs) {
                    String subDirPath = groupDir + ZipDownloadUtils.safeName(subDir.getSubDirName()) + "/";
                    List<ZipFileRef> fileRefs = subDir.getFileRefs();
                    if (fileRefs == null || fileRefs.isEmpty()) {
                        subDirPath = groupDir + ZipDownloadUtils.safeName(subDir.getSubDirName()) + "(empty)/";
                        ZipDownloadUtils.writeTextToZip(zipOut, subDirPath + "empty.txt", "该类型无资料");
                        continue;
                    }
                    for (ZipFileRef fileRef : fileRefs) {
                        if (fileRef.getLocalPath() != null && !fileRef.getLocalPath().isEmpty()) {
                            String name = ZipDownloadUtils.getFileName(fileRef.getLocalPath());
                            fileRef.setName(name);
                            ZipDownloadUtils.writeLocalFileToZip(zipOut, fileRef.getLocalPath(), subDirPath + ZipDownloadUtils.safeName(fileRef.getName()));
                        } else if (fileRef.getHttpUrl() != null && !fileRef.getHttpUrl().isEmpty()) {
                            String name = ZipDownloadUtils.getFileName(fileRef.getHttpUrl());
                            fileRef.setName(name);
                            ZipDownloadUtils.writeHttpFileToZip(zipOut, fileRef.getHttpUrl(), subDirPath + ZipDownloadUtils.safeName(fileRef.getName()));
                        }
                    }
                }
            }
            zipOut.finish();
            zipOut.flush();
            response.flushBuffer();
        } catch (Exception e) {
            throw new RuntimeException("打包下载失败", e);
        }
    }
}

3. 常见问题及安全建议

防路径穿越(Zip Slip):所有目录/文件名务必用safeName过滤特殊字符

大文件/大批量:建议分页、分批处理

空目录写入:统一写empty.txt标识空目录

本地文件不存在:Zip包内写入提示信息

HTTP下载失败:Zip包内写入“下载失败”提示

避免泄露服务器绝对路径:仅在日志中记录本地路径,Zip内不暴露

权限校验:实际生产需验证用户是否有权访问指定文件

4. 总结

这里介绍了如何从本地服务器路径和HTTP混合读取文件并Zip打包下载,目录结构灵活可控。可根据实际需求扩展更多来源类型(如数据库、对象存储等)。

到此这篇关于SpringBoot实现多种来源的Zip多层目录打包下载的文章就介绍到这了,更多相关SpringBoot多来源Zip打包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅析Spring IOC bean为什么默认是单例

    浅析Spring IOC bean为什么默认是单例

    单例的意思就是说在 Spring IoC 容器中只会存在一个 bean 的实例,无论一次调用还是多次调用,始终指向的都是同一个 bean 对象,本文小编将和大家一起分析Spring IOC bean为什么默认是单例,需要的朋友可以参考下
    2023-12-12
  • 迪米特法则_动力节点Java学院整理

    迪米特法则_动力节点Java学院整理

    这篇文章主要介绍了迪米特法则,迪米特法则就是一个在类创建方法和属性时需要遵守的法则,有兴趣的可以了解一下
    2017-08-08
  • SpringSecurity授权机制的实现(AccessDecisionManager与投票决策)

    SpringSecurity授权机制的实现(AccessDecisionManager与投票决策)

    本文主要介绍了SpringSecurity授权机制的实现,其核心是AccessDecisionManager和投票系统,下面就来介绍一下,感兴趣的可以了解一下
    2025-04-04
  • JAVA的LIST接口的REMOVE重载方法调用原理解析

    JAVA的LIST接口的REMOVE重载方法调用原理解析

    这篇文章主要介绍了JAVA的LIST接口的REMOVE重载方法调用原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • MyBatis的五种批量插入详解

    MyBatis的五种批量插入详解

    这篇文章主要介绍了MyBatis的五种批量插入详解,MyBatis是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低,需要的朋友可以参考下
    2023-08-08
  • 深入探究TimSort对归并排序算法的优化及Java实现

    深入探究TimSort对归并排序算法的优化及Java实现

    这篇文章主要介绍了TimSort归并排序的优化及Java实现,TimSort 是一个归并排序做了大量优化的版本,需要的朋友可以参考下
    2016-05-05
  • Ubuntu安装JDK与IntelliJ IDEA的详细过程

    Ubuntu安装JDK与IntelliJ IDEA的详细过程

    APT是Linux系统上的包管理工具,能自动解决软件包依赖关系并从远程存储库中获取安装软件包,这篇文章主要介绍了Ubuntu安装JDK与IntelliJ IDEA的过程,需要的朋友可以参考下
    2023-08-08
  • java和javascript中过滤掉img形式的字符串不显示图片的方法

    java和javascript中过滤掉img形式的字符串不显示图片的方法

    这篇文章主要介绍了java和javascript中过滤掉img形式的字符串不显示图片的方法,以实例形式分别讲述了采用java和javascript实现过滤掉img形式字符串的技巧,需要的朋友可以参考下
    2015-02-02
  • MySQL安装与idea的连接实现

    MySQL安装与idea的连接实现

    本文主要介绍了MySQL安装与idea的连接实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 详解SpringBoot结合策略模式实战套路

    详解SpringBoot结合策略模式实战套路

    这篇文章主要介绍了详解SpringBoot结合策略模式实战套路,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10

最新评论