[原创]springboot 中 resources 资源目录里面的文件夹压缩下载

本贴最后更新于 2022 天前,其中的信息可能已经事过景迁

前言

最近做个小工具需要提供一个将 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

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    8112 引用 • 37003 回帖 • 160 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • washmore
    作者

    囧,没有什么高深的内容呀

  • 其他回帖
  • someone

    看一看

  • 2501224066

    虽然看不懂,但似乎很屌的样子 👍

    1 回复
  • washmore
    作者

    any one?