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

  |   2 评论   |   687 浏览

前言

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

评论

发表评论