前言
最近做个小工具需要提供一个将 resources 资源文件夹下某个目录(放了一些模板集合)打包下载功能
尝试
祖传的 zip 文件夹功能代码先送上:
public void zip(ZipOutputStream out, File sourceFile, String base) throws Exception {
//如果路径为目录(文件夹)
if (sourceFile.isDirectory()) {
//取出文件夹中的文件(或子文件夹)
File[] fileList = sourceFile.listFiles();
if (fileList.length == 0) {
//如果文件夹为空,则只需在目的地zip文件中写入一个目录进入点
System.out.println(base + "/");
out.putNextEntry(new ZipEntry(base + "/"));
} else {
//如果文件夹不为空,则递归调用zip,文件夹中的每一个文件(或文件夹)进行压缩
for (File file : fileList) {
zip(out, file, base + "/" + file.getName());
}
}
} else {
//如果不是目录(文件夹),即为文件,则先写入目录进入点,之后将文件写入zip文件中
out.putNextEntry(new ZipEntry(base));
IOUtils.write(FileUtils.readFileToByteArray(sourceFile), out);
out.flush();
}
}
最开始是针对单个文件下载,很简单,通过 this.getClass().getResourceAsStream("/templates/demo.xml")
获取到指定文件的输入流,然后写入到 response.getOutputStream()
中去即可;
然后依样画葫芦针对文件夹下载,this.getClass().getResourceAsStream("/templates")
获取到文件夹的输入流,然鹅输出发现这个输入流拿到的信息是
file1.xml
file2.xml
dictionary1
这样的内容,而祖传 zip 第二个参数要求的是一个文件夹目录 File 对象,不太好整;
换个方式:
OutputStream ops = response.getOutputStream();
ZipOutputStream out = new ZipOutputStream(ops);
File parent = new File(this.getClass().getResource("/templates").getFile());
zip(out, parent, "");
out.close();
ops.flush();
ops.close();
通过拿到资源文件目录 /templates
所在的 File 信息,然后基于 response
的输出流生成 ZipOutputStream
,调用 zip 方法压缩.搞定!
麻烦
自测通过后打包成 jar 执行,问题出现了,会报错
java.io.FileNotFoundException: File 'file:...jar!/BOOT-INF/classes!/templates' does not exist
这是因为将应用打包成 jar 后,File parent = new File(this.getClass().getResource("/templates").getFile());
这行代码不再能正确获取到 /templates
所在的文件目录信息,导致下载失败!
解决
去 TMD 的百度搜索,全给推荐 csdn 和 cnblogs 的文章,也不知道谁抄谁的,千篇一律
File parent = new File(this.getClass().getResource("/templates").getFile());
换成
InputStream ips = this.getClass().getResourceAsStream("/templates/demo.xml")
大法,可我他喵的要下载文件夹啊!!!已拉黑!!!
探索
想着既然能通过 getResourceAsStream
获取到输入流,那我干脆自行遍历 /templates
资源文件夹,然后逐个转移到临时文件夹目录,然后针对临时文件夹打包下载.
说做就做!!!
将 this.getClass().getResourceAsStream("/templates")
获取到的输入流
file1.xml
file2.xml
dictionary1
进行遍历,然后又傻逼了...我倒是知道 file2.xml
是文件 dictionary1
是文件夹,针对文件夹还要往下层遍历,但是代码不知道啊?
千里之行死于足下...这可咋整?
发现
一番上上下下左左右右 BABA 操作之后发现, getResourceAsStream
方法如果参数是文件夹那返回的输入流的具体类型是 ByteArrayInputStream
,而针对文件,输入流的具体类型是 BufferedInputStream
,
这就好办了 ips instanceof ByteArrayInputStream
约等于 file.isDirectory()
的效果嘛.
实施
现在整体思路就很明朗了,先将 /templates
资源目录复制到临时文件夹中保存,然后针对临时文件夹进行 zip 压缩,然后输出给 response
完成打包下载功能;
下面是将 /templates
资源目录复制到临时文件夹的代码:
public void copyResourcesToTempDictionary(String sourceParentPath, String name, File tempParent) throws Exception {
String path = sourceParentPath + "/" + name;
InputStream ips = this.getClass().getResourceAsStream(path);
File file = new File(tempParent, name);
if (file.exists()) {
file.delete();
}
if (ips instanceof ByteArrayInputStream) {
//文件夹
file.mkdirs();
List<String> children = IOUtils.readLines(ips, StandardCharsets.UTF_8);
if (CollectionUtils.isEmpty(children)) {
return;
}
for (String child : children) {
copyResourcesToTempDictionary(path, child, file);
}
} else if (ips instanceof BufferedInputStream) {
file.createNewFile();
FileUtils.writeByteArrayToFile(file, IOUtils.toByteArray(ips));
}
}
整体流程调用代码(设置响应头/编码/文件名等操作略):
OutputStream ops = response.getOutputStream();
ZipOutputStream out = new ZipOutputStream(ops);
File parent = new File(System.getProperty("java.io.tmpdir"), "~tmp");
if (parent.exists()) {
parent.delete();
}
parent.mkdirs();
copyResourcesToTempDictionary("", "templates", parent);
zip(out, parent, "");
out.close();
ops.flush();
ops.close();
Updates
既然 zip 是从 source 写到输出流,这个 sources 既可以是 File,当然也可以来自输入流嘛,于是忍不住对祖传的 zip 方法下手了,针对这种 resources
文件夹的压缩新增一个 zipResources
的方法:
public void zipResources(ZipOutputStream out, String sourceParentPath, String name) throws Exception {
String path = sourceParentPath + "/" + name;
InputStream ips = this.getClass().getResourceAsStream(path);
if (ips instanceof ByteArrayInputStream) {
//取出文件夹中的文件(或子文件夹)
List<String> children = IOUtils.readLines(ips, StandardCharsets.UTF_8);
if (CollectionUtils.isEmpty(children)) {
//如果文件夹为空,则只需在目的地zip文件中写入一个目录进入点
out.putNextEntry(new ZipEntry(sourceParentPath));
} else {
for (String child : children) {
zipResources(out, path, child);
}
}
} else {
//如果不是目录(文件夹),即为文件,则先写入目录进入点,之后将文件写入zip文件中
out.putNextEntry(new ZipEntry(path));
IOUtils.write(IOUtils.toByteArray(ips), out);
out.flush();
}
}
这样一来,就不需要借助临时文件夹中转了,整体流程调用可简化为:
OutputStream ops = response.getOutputStream();
ZipOutputStream out = new ZipOutputStream(ops);
zipResources(out, "", "templates");
out.close();
ops.flush();
ops.close();
真是机智的骚年!
Q&A
文中有用到一些 IO 操作 utils 来自 commons 系列,附 maven 地址:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
各位如果有完成过类似的 case,有更优雅或更合适的方案的话,欢迎在评论指出.
One More Thing
我岳母身患骨髓增生异常综合征伴骨髓纤维化,急需筹钱做骨髓移植手术,方便的话转请大家帮忙转发一下朋友圈,感谢大家!
轻松筹地址:https://m2.qschou.com/project/love/love_v7.html?projuuid=23a9dbd5-78e3-429f-8b46-c7efce4a9443
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于