工具类包名:cn.hutool.core.util.ZipUtil
场景:需要将本地的文件以流的形式压缩并传给前端(本意是想着如果压缩成文件,还得开一个InputStream来读,性能可能会下降,实验结论在后文)
问题发现
第一次直接调用ZipUtil的public static void zip(OutputStream out, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles)
方法
脱敏代码如下:
HttpServletResponse response = getResponse();
response.setHeader("Content-disposition","attachment;filename=" + URLEncoder.encode("testOutPut", "UTF-8") + ".zip");
File file = new File("C:\\Users\\win\\Desktop\\新建文本文档.txt");ServletOutputStream outputStream = response.getOutputStream();
ZipUtil.zip(outputStream, Charset.defaultCharset(),Boolean.FALSE,null,file);
遇到的问题:前端下载正常,使用win10资源管理器打开压缩包显示 Windows 无法打开文件夹。“压缩(zipped)文件夹”C:\Users\win\Downloads\testOutPut.zip无效。
面向百度编程后,得到线索:压缩文件结尾有额外的异常文件,所以打不开,解决方案是关闭输出流。
但是我是从前端拿到的输出流,根据谁创建谁关闭原则,不应该关闭这个流。(当然我尝试过了,outputStream.close()还是同样的问题)
神奇的是,只有资源管理器不能打开该压缩包,使用360压缩可以正常解压,使用7-zip虽然打开和解压会报错,但是依然正常解压出文件。
第二次调用ZipUtil的public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles)
方法
脱敏代码如下:
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
ZipUtil.zip(zipOutputStream,false,null,file);
zipOutputStream.close();
此时zip恢复正常,资源管理器可以正常打开。
问题溯源
- 第一个参数为OutputStream的方法本质上在内部将其包装成ZipOutputStream后再调用方法二
- 查看getZipOutputStream方法,其实就是判断是否为ZipOutputStream,否则new一个,将传入的流进行包装
到了这一步,我的想法是他创建了一个流,但是内部没有任何地方关闭它,且外部也没办法关闭,这样理论上不就违反了谁创建谁关闭原则吗?
后来经过老师指点,其实这个地方的思想是装饰者模式,本质上只是将传入的流进行包装,不是自己创建的新流,自然也不需要他关闭。
不过新的问题又出现了,按上述结论,zipOutputStream是不应该被关闭的,我为什么不加close就生成错误的zip,加了反而能正确运行呢?
// ZipOutputStream.java的close方法实际上调用的是他的父类DeflaterOutputStream.java的close
/*** Closes the ZIP output stream as well as the stream being filtered.* @exception ZipException if a ZIP file error has occurred* @exception IOException if an I/O error has occurred*/
public void close() throws IOException {if (!closed) {super.close();closed = true;}
}
// DeflaterOutputStream.java的close方法源码如下
/*** Writes remaining compressed data to the output stream and closes the* underlying stream.* @exception IOException if an I/O error has occurred*/
public void close() throws IOException {if (!closed) {finish();if (usesDefaultDeflater)def.end();out.close();closed = true;}
}
可以清楚的看到,DeflaterOutputStream.java的close方法先执行了finish()方法,再对流进行关闭。(同时修改关闭标志)
结论
实际上是流的finish()生效,每次创建一个压缩文件流时,需要finish()来标识该文件流已达到末尾,资源管理器在读到带结束标识的压缩文件才会正确打开(第三方压缩软件估计做了兼容处理)
而ZipUtil工具类中,作者直接new ZipOutputStream(ouputStream),却没有在任何地方调用finish()方法来标识结束,导致当传入一个不需要关闭的Stream的时候,无法正确标识结束
解决方案
当你在传入一个不希望被关闭的Stream时,请在外部自行new一个ZipOutputStream对象并传入该Stream,调用完ZipUtil的方法后,手动调用ZipOutputStream的finish()方法来标识