"前言 最近做个小工具需要提供一个将 resources 资源文件夹下某个目录 (放了一些模板集合) 打包下载功能 尝试 祖传的 zip 文件夹功能代码先送上: public void zip(ZipOutputStream out, File sourceFile, String base) throws Except .."

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

前言

最近做个小工具需要提供一个将 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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    2168 引用 • 7424 回帖 • 1019 关注
  • Q&A

    提问之前请先看一下这篇文章 《提问的智慧》精读注解版,好的问题比好的答案更有价值。

    941 引用 • 5907 回帖 • 607 关注
感谢    关注    收藏    赞同    反对    举报    分享
3 回帖    
请输入回帖内容...
  • washmore      

    any one?

       感谢    赞同    反对    举报    折叠    分享    评论    回复
  • 2501224066      

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

    1 回复
       感谢    赞同    反对    举报    折叠    分享    评论    回复
  • washmore            

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

       感谢    赞同    反对    举报    折叠    分享    评论    回复