java实现压缩包嵌套压缩包的下载

目录

需求场景:

前言:

有问题的思路:

        第一次问题分析:

        有问题的代码:

        思考过程:

新的的思路:

重新分析问题

代码案例:

封装工具类:

测试工具类:


需求场景:

接到一个需求,总结下来的核心要点是,要求下载一个zip文件,经过一些列业务分类之后,这个下载下来的zip包里面可能还有zip包,但是服务器上的原始文件都是word和excel这样的文件,也就是需要在代码段进行一个嵌套或者说递归打包的过程。

实际上就是需要一个类似这样的目录层级:

附件.zip

        -第一个内部附件.zip

                -工作记录.xls

前言:

评论区有小伙伴发现我第一次的思路有错误,这里保留了第一次的有问题的思路,给大家分享一下思考的过程,如果有小伙伴想直接看结果,可以直接跳过第一段直接查看新的思路

有问题的思路:

第一次问题分析:

1、首先常规的把全部附件打到一个压缩包,或者是带文件夹层级的都很常见,但是想嵌套成zip包,这个没有尝试过。

2、百度之后也无果,都是复制粘贴后最简单的压缩和解压缩,也就是单层的zip包压缩,并没有找到什么有效的方法,那没办法,得自己想

3、先分析常规的打包是怎么做的,我这边是http下载请求,就是通过ZipOutputStream把文件流输出到http的response中的输出流中去,而ZipOutputStream又是通过一个个ZipEntry对象和文件的inputStream流去写入数据

4、有了上面一层的理解就很简单了,其实只需要把内部打包的ZipOutputStream直接往外部的ZipOutputStream里面输出就可以了,我们只需要在外面把文件名定义好就可以了


有问题的代码:

void testExport(HttpServletResponse response){

    //下载的文件名
    String fileName="附件.zip";
    try {
        //根据自己前端以及浏览器设置文件名编码
        fileName = URLEncoder.encode(fileName,"UTF-8");
    } catch (Exception e) {
        log.error(e.getMessage(),e);
    }
    //设置响应头编码
    response.setContentType("application/octet-stream;charset=UTF-8");
    response.setCharacterEncoding("utf-8");
    //设置文件名
    response.setHeader("Content-Disposition","attachment;filename="+fileName);
    //把Content-Disposition这个字段开放给前端可见,这样前端就可以取到后端生成的文件名
    response.setHeader("Access-Control-Expose-Headers","Content-Disposition");
    //真正的下载,这里都是用的jdk7的写法,所有的流都不用我手动关闭了
    try (
        //这里面为了防止内存溢出等问题,使用了BufferedOutputStream缓冲流,可以看到这里是把流输出到了response里面
        ZipOutputStream zos1=new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()), StandardCharsets.UTF_8);
    ){
        zos1.setMethod(ZipOutputStream.DEFLATED);
        zos1.putNextEntry(new ZipEntry("第一个内部压缩包.zip"));
        try (
            //这里才是嵌套压缩的重点,可以看到这里zos2里面的输出流是zos1,这样,在内部处理的时候只需要处理zos2就行了,输出的操作会由zos2自己往zos里面输出
            ZipOutputStream zos2=new ZipOutputStream(new BufferedOutputStream(zos1), StandardCharsets.UTF_8);
        ){
            //这里面内层压缩包的附件
            zos2.setMethod(ZipOutputStream.DEFLATED);
            zos2.putNextEntry(new ZipEntry("工作记录.xls"));
            try (InputStream is = new FileInputStream("data/工作记录.xls")){
                int b;
                byte[] buffer = new byte[1024];
                while ((b=is.read(buffer))!=-1){
                    zos2.write(buffer,0,b);
                }
                zos2.flush();
            }catch (Exception e){
                log.error("文件输出异常",e);
            }
        }catch (Exception e){
            log.error("内部打包异常",e);
        }
        zos1.flush();
    }catch (Exception e){
        log.error("文件下载失败",e);
    }

}

思考过程:

1、其实一开始想错了,总在想一个output没办法往另一个output里面去写,甚至考虑过把output转为input,再写入到最外层的zipoutputstram中去,但是因为这个业务场景文件后期是会不断增多的,把全部的流放到内存中去大概率是会出问题的。

2、然后就想利用服务器做缓存,先把打包好的文件保存到服务器中去,然后再去直接读文件拿到输入流,但是这样一个是我觉得一个是增加io的压力,临时文件也得去清理;还有一个考虑到这样就失去了边压缩边下载的速度了。

3、后面才研究一下这个打包的过程,发现并不难实现,只是网上没有类似的内容,还是得去好好学习io的知识才可以


新的的思路:

重新分析问题

1、上面的思路,评论区有小伙伴提出是有问题的,zos2会把zos1给close,导致二级无法打包多个压缩包,确实是存在这样的问题,我后面又去研究了一下,最终找到了一个新的方案。

2、上述问题其实如果继续用之前的思路,其实是没办法解决的,因为只要内部的流被关闭了一次,外层的流都会被关掉,所以我们得换一个思路,既然使用递归流不行,那我们还是只能考虑将文件存在内存中,对内存造成的负荷是没有办法避免的。

3、所以这里我用到了一个新的类,ByteArrayOutputStream,虽然名字是输出流,但它实际上是一个内存流,简单的理解为字节数组就行,甚至不需要去关闭它。所以这样我们就可以使用这个类,把内部打包完的压缩包暂时的存到这个ByteArrayOutputStream里面去,就相当于一个ByteArrayOutputStream对象就代表内部的一个zip文件,然后再把每个文件通过response的output输出出去。

代码案例:

这段代码是一个demo,这里是用写死的方式打包了两个压缩包,方便更好的理解ByteArrayOutputStream是如何使用的

    public void test(HttpServletResponse response){
        //下载的文件名
        String fileName="附件.zip";
        try {
            //根据自己前端以及浏览器设置文件名编码
            fileName = URLEncoder.encode(fileName,"UTF-8");
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }
        //设置响应头编码
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        //设置文件名
        response.setHeader("Content-Disposition","attachment;filename="+fileName);
        //把Content-Disposition这个字段开放给前端可见,这样前端就可以取到后端生成的文件名
        response.setHeader("Access-Control-Expose-Headers","Content-Disposition");
        //真正的下载,这里都是用的jdk7的写法,所有的流都不用我手动关闭了
        try (
                //最外层的压缩包,还是往response的output里面输出
                ZipOutputStream zos1=new ZipOutputStream(response.getOutputStream(), StandardCharsets.UTF_8);
        ){
            zos1.setMethod(ZipOutputStream.DEFLATED);
            zos1.putNextEntry(new ZipEntry("第一个内部压缩包.zip"));
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            try (
                    //这里就是把第一个内部的压缩包放到byteArrayOutputStream中去了
                    ZipOutputStream zos2=new ZipOutputStream(byteArrayOutputStream, StandardCharsets.UTF_8);
            ){
                //这里面内层压缩包的附件
                zos2.setMethod(ZipOutputStream.DEFLATED);
                zos2.putNextEntry(new ZipEntry("测试文件.txt"));
                try (InputStream is = new FileInputStream("/data/测试文件.txt")){
                    int b;
                    byte[] buffer = new byte[1024];
                    while ((b=is.read(buffer))!=-1){
                        zos2.write(buffer,0,b);
                    }
                    zos2.flush();
                }catch (Exception e){
                    log.error("文件输出异常",e);
                }
            }catch (Exception e){
                log.error("内部打包异常",e);
            }
            //往最外部的压缩包里面写入
            zos1.write(byteArrayOutputStream.toByteArray());
            zos1.flush();
            //重置这个类,这样不用每次都new一个新的对象造成内存负担
            byteArrayOutputStream.reset();
            zos1.setMethod(ZipOutputStream.DEFLATED);
            zos1.putNextEntry(new ZipEntry("第二个内部压缩包.zip"));
            try (
                    //和第一个压缩包一样
                    ZipOutputStream zos3=new ZipOutputStream(byteArrayOutputStream, StandardCharsets.UTF_8);
            ){
                zos3.setMethod(ZipOutputStream.DEFLATED);
                zos3.putNextEntry(new ZipEntry("测试文件.xlsx"));
                try (InputStream is = new FileInputStream("/data/测试文件.xlsx")){
                    int b;
                    byte[] buffer = new byte[1024];
                    while ((b=is.read(buffer))!=-1){
                        zos3.write(buffer,0,b);
                    }
                    zos3.flush();
                }catch (Exception e){
                    log.error("文件输出异常",e);
                }
            }catch (Exception e){
                log.error("内部打包异常",e);
            }

            zos1.write(byteArrayOutputStream.toByteArray());
            zos1.flush();

        }catch (Exception e){
            log.error("文件下载失败",e);
        }
    }

封装工具类:

由上面的demo,封装了一个工具类,这样就这样就可以实现递归打包,并且不限制个数了

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Slf4j
public class FileUtils {

    public static void recursionExportZip(HttpServletResponse response,String fileName,List<ZipPo> fileList){
        //下载的文件名
        try {
            //根据自己前端以及浏览器设置文件名编码
            fileName = URLEncoder.encode(fileName,"UTF-8");
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }
        //设置响应头编码
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        //设置文件名
        response.setHeader("Content-Disposition","attachment;filename="+fileName);
        //把Content-Disposition这个字段开放给前端可见,这样前端就可以取到后端生成的文件名
        response.setHeader("Access-Control-Expose-Headers","Content-Disposition");
        try (
                //把流输出到response里面
                ZipOutputStream zos1=new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()), StandardCharsets.UTF_8);
        ){
            recursionPackage(zos1,fileList);
        }catch (Exception e){
            log.error("文件下载失败",e);
        }
    }

    private static void recursionPackage(ZipOutputStream zos, List<ZipPo> fileList) {
        //先初始化一个,每次使用的时候reset,防止内存溢出
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (ZipPo zipPo : fileList) {
            zos.setMethod(ZipOutputStream.DEFLATED);
            try {
                zos.putNextEntry(new ZipEntry(zipPo.getName()));
            } catch (Exception e) {
                log.error(e.getMessage(),e);
            }
            //判断对象是附件还是压缩包
            if(CollectionUtils.isEmpty(zipPo.getFileList())){ //附件
                //获取附件并写到压缩包中
                try (InputStream is = new FileInputStream(zipPo.getPath())){
                    int b;
                    byte[] buffer = new byte[1024];
                    while ((b=is.read(buffer))!=-1){
                        zos.write(buffer,0,b);
                    }
                    zos.flush();
                }catch (Exception e){
                    log.error("文件输出异常",e);
                }
            }else {//压缩包
                //重置一下内存流
                byteArrayOutputStream.reset();
                try (
                        //把压缩包,往byteArrayOutputStream里面输出
                        ZipOutputStream zos2=new ZipOutputStream(byteArrayOutputStream, StandardCharsets.UTF_8);
                ){
                    //递归压缩
                    recursionPackage(zos2,zipPo.getFileList());
                }catch (Exception e){
                    log.error("内部打包异常",e);
                }
                //把byteArrayOutputStream写到压缩包中,这个地方需要特别注意一下,不能把这两个写在上面的try里面,因为zos2关闭的时候会影响到zos,导致流的关闭顺序出错,压缩包文件被损坏,感谢评论区大佬的指正
                try {
                    zos.write(byteArrayOutputStream.toByteArray());
                    zos.flush();
                } catch (IOException e) {
                    log.error("内部打包异常",e);
                }
            }
        }
    }


    @Data
    public static class ZipPo{
        /**
         * 文件名/压缩包名
         */
        private String name;
        /**
         * 附件路径,为压缩包时该字段为null
         */
        private String path;
        /**
         * 递归的文件,当为文件时改字段为null
         */
        private List<ZipPo> fileList;

        public ZipPo() {
        }

        public ZipPo(String name, String path) {
            this.name = name;
            this.path = path;
        }

        public ZipPo(String name, List<ZipPo> fileList) {
            this.name = name;
            this.fileList = fileList;
        }
    }
}

测试工具类:

public void download(HttpServletResponse response){
        //单文件
        List<FileUtils.ZipPo> list = new ArrayList<>();
        list.add(new FileUtils.ZipPo("测试文件.txt","/data/测试文件.txt"));

        //第一个压缩包里面两个文件
        List<FileUtils.ZipPo> list1_1=new ArrayList<>();
        list1_1.add(new FileUtils.ZipPo("测试文件.xlsx","/data/测试文件.xlsx"));
        list1_1.add(new FileUtils.ZipPo("测试文件.docx","/data/测试文件.docx"));
        list.add(new FileUtils.ZipPo("第一个内部压缩包.zip",list1_1));

        //第二个压缩包
        List<FileUtils.ZipPo> list1_2=new ArrayList<>();
        list1_2.add(new FileUtils.ZipPo("测试文件.bmp","/data/测试文件.bmp"));
        //嵌套一个压缩包
        List<FileUtils.ZipPo> list2_1=new ArrayList<>();
        list2_1.add(new FileUtils.ZipPo("测试文件.pptx","/data/测试文件.pptx"));
        list1_2.add(new FileUtils.ZipPo("第二个内部压缩包里面的第一个压缩包.zip",list2_1));
        list.add(new FileUtils.ZipPo("第二个内部压缩包.zip",list1_2));
        
        FileUtils.recursionExportZip(response,"附件.zip",list);
}

原附件:

下载后的压缩包:

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
要在Java实现压缩下载,你可以使用Java压缩库来创建压缩文件,然后将其发送到客户端进行下载。以下是一个简单的示例代码: ``` import java.io.*; import java.util.zip.*; public class ZipDownload { public static void main(String[] args) { // 设置要压缩的文件路径 String sourceFile = "/path/to/source/file"; // 设置下载文件的名称 String downloadFileName = "compressed.zip"; try { // 创建压缩文件输出流 FileOutputStream fos = new FileOutputStream(downloadFileName); ZipOutputStream zipOut = new ZipOutputStream(fos); // 创建要压缩的文件对象 File fileToZip = new File(sourceFile); // 添加要压缩的文件到压缩文件输出流 zipFile(fileToZip, fileToZip.getName(), zipOut); // 关闭压缩文件输出流 zipOut.close(); fos.close(); System.out.println("压缩完成:" + downloadFileName); } catch (IOException e) { e.printStackTrace(); } } private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException { if (fileToZip.isHidden()) { return; } if (fileToZip.isDirectory()) { if (fileName.endsWith("/")) { zipOut.putNextEntry(new ZipEntry(fileName)); zipOut.closeEntry(); } else { zipOut.putNextEntry(new ZipEntry(fileName + "/")); zipOut.closeEntry(); } File[] children = fileToZip.listFiles(); for (File childFile : children) { zipFile(childFile, fileName + "/" + childFile.getName(), zipOut); } return; } FileInputStream fis = new FileInputStream(fileToZip); ZipEntry zipEntry = new ZipEntry(fileName); zipOut.putNextEntry(zipEntry); byte[] bytes = new byte[1024]; int length; while ((length = fis.read(bytes)) >= 0) { zipOut.write(bytes, 0, length); } fis.close(); } } ``` 这段代码将指定的文件压缩为一个名为 "compressed.zip" 的压缩,并保存在当前目录中。你可以根据需要修改源文件路径、下载文件名称和保存路径等参数。运行该代码后,你将获得一个压缩用于下载

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值