SpringBoot实现多种来源的Zip多层目录打包下载
需要将一批文件(可能分布在不同目录、不同来源)打包成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 listsubdirs; }
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 listfilerefs; }
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) { // ==== 示例数据 ==== listdata = 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://oss.xajjn.com/article/2025/05/07/2300031245.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 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 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打包内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
推荐阅读
-
IDEA中使用Gradle构建项目中文报GBK错误的解决方案
-
将Java应用做成exe可执行软件的流程步骤
-
Java中减少if-else的设计模式和优化技巧
前言“过于依赖if-else不仅会让代码变得臃肿不堪,还会使维护成本大大增加。其实,if-else虽然是最基础的条件分支,...
-
Spring Boot 中使用 Drools 规则引擎的完整步骤
-
Spring Boot整合Drools规则引擎实战指南及最佳实践
一、drools简介与核心概念1.1什么是drools?drools是redhat旗下的开源业务规则管理系统(brms),...
-
Springboot项目瘦身之如何将jar包与lib依赖分开打包
将jar包与lib依赖分开打包方法一:项目和依赖完全分离maven-jar-plugin负责生成jar文件(jar文件中...
-
Spring动态修改bean属性配置key的几种方法
静态配置的局限性先来看一个典型场景。假设我们有一个数据源配置类:@configuration@configurationpr...
-
Java如何判断一个IP是否在给定的网段内
-
从零开始学java之二叉树和哈希表实现代码
-
Java如何解决ArrayList的并发问题
arraylist是java.util包中的一个类,它不是线程安全的。如果多个线程同时对同一个arraylist进行操作,可能会...