文章目录
如题
写这篇文章源于前不久在用到poi导出大量数据时,使用的SXSSFWorkbook,一开始直接用的poi-4.1.2版本的相关jar包,但是后来本地/测试环境均没问题,一部署到正式服务器报如下错误:
excelExport(DeviceAction.java:643)] : java.io.IOException: This archive contains unclosed entries.
当时是直接try-catch()语句logger.error(e),这样输出错误的,而且又在生产环境,所以没有开详细的日志记录,只有如上一句报错信息。网上相关帖子都有在说,是官方poi 在4.0以后的版本,jar包中apache-commons-compress 存在功能相关的错误,建议更换版本。 情况紧急确实我更换了3.17版本后,未曾出现此错误,可以正常导出。
今天有时间来对比一下二者在源码层面的改变。
涉及的部分源码
POI-4.1.2
我们直接从 workbook.write(out) 点击write方法进入它的内部:
public void write(OutputStream stream) throws IOException {
this.flushSheets();
File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx");
boolean deleted;
try {
FileOutputStream os = new FileOutputStream(tmplFile);
Throwable var5 = null;
try {
this._wb.write(os);
} catch (Throwable var67) {
var5 = var67;
throw var67;
} finally {
if (os != null) {
if (var5 != null) {
try {
os.close();
} catch (Throwable var65) {
var5.addSuppressed(var65);
}
} else {
os.close();
}
}
}
ZipSecureFile zf = new ZipSecureFile(tmplFile); // 一.
var5 = null;
try {
ZipFileZipEntrySource source = new ZipFileZipEntrySource(zf);
Throwable var7 = null;
try {
this.injectData(source, stream); // 二.
} catch (Throwable var66) {
var7 = var66;
throw var66;
} finally {
if (source != null) {
if (var7 != null) {
try {
source.close();
} catch (Throwable var64) {
var7.addSuppressed(var64);
}
} else {
source.close();
}
}
}
} catch (Throwable var69) {
var5 = var69;
throw var69;
} finally {
if (zf != null) {
if (var5 != null) {
try {
zf.close();
} catch (Throwable var63) {
var5.addSuppressed(var63);
}
} else {
zf.close();
}
}
}
} finally {
deleted = tmplFile.delete();
}
if (!deleted) {
throw new IOException("Could not delete temporary file after processing: " + tmplFile);
}
}
然后点击 this.injectData(source, stream); 进入数据写入方法injectData()内部:
protected void injectData(ZipEntrySource zipEntrySource, OutputStream out) throws IOException {
ZipArchiveOutputStream zos = this.createArchiveOutputStream(out); // 这里输出流用到了一个新的,zip压缩文件输出流
try {
Enumeration en = zipEntrySource.getEntries(); // 获取entries
// 遍历 .xlsx文件流
while(en.hasMoreElements()) {
ZipArchiveEntry ze = (ZipArchiveEntry)en.nextElement();
ZipArchiveEntry zeOut = new ZipArchiveEntry(ze.getName());
zeOut.setSize(ze.getSize());
zeOut.setTime(ze.getTime());
zos.putArchiveEntry(zeOut);
try {
InputStream is = zipEntrySource.getInputStream(ze);
Throwable var8 = null;
try {
if (is instanceof ZipArchiveThresholdInputStream) {
((ZipArchiveThresholdInputStream)is).setGuardState(false);
}
//根据名称获取xsheet对象
XSSFSheet xSheet = this.getSheetFromZipEntryName(ze.getName());
if (xSheet != null && !(xSheet instanceof XSSFChartSheet)) {
SXSSFSheet sxSheet = this.getSXSSFSheet(xSheet);
InputStream xis = sxSheet.getWorksheetXMLInputStream();
Throwable var12 = null;
// 获取xsheet文件流
try {
copyStreamAndInjectWorksheet(is, zos, xis);
} catch (Throwable var61) {
var12 = var61;
throw var61;
} finally {
if (xis != null) {
if (var12 != null) {
try {
xis.close();
} catch (Throwable var60) {
var12.addSuppressed(var60);
}
} else {
xis.close();
}
}
}
} else { // 流拷贝
IOUtils.copy(is, zos);
}
} catch (Throwable var63) {
var8 = var63;
throw var63;
} finally {
if (is != null) {
if (var8 != null) {
try {
is.close();
} catch (Throwable var59) {
var8.addSuppressed(var59);
}
} else {
is.close();
}
}
}
} finally {
zos.closeArchiveEntry(); // 关闭压缩的entry
}
}
} finally {
zos.finish(); // 最终结束zip压缩文件输出流
zipEntrySource.close();
}
}
我们再点击 ZipArchiveOutputStream 类名进入它的源码里,查看 finish() 方法:
public void finish() throws IOException {
if (this.finished) {
throw new IOException("This archive has already been finished");
} else if (this.entry != null) {
throw new IOException("This archive contains unclosed entries.");
} else {
this.cdOffset = this.streamCompressor.getTotalBytesWritten();
this.writeCentralDirectoryInChunks();
this.cdLength = this.streamCompressor.getTotalBytesWritten() - this.cdOffset;
this.writeZip64CentralDirectory();
this.writeCentralDirectoryEnd();
this.metaData.clear();
this.entries.clear();
this.streamCompressor.close();
this.finished = true;
}
}
可以看出最终引发我上面报错信息的就是此处抛出的异常。 即此时的entry并不是null,导致触发异常。我们再点开 closeArchiveEntry():
public void closeArchiveEntry() throws IOException {
this.preClose();
this.flushDeflater();
long bytesWritten = this.streamCompressor.getTotalBytesWritten() - this.entry.dataStart;
long realCrc = this.streamCompressor.getCrc32();
this.entry.bytesRead = this.streamCompressor.getBytesRead();
Zip64Mode effectiveMode = this.getEffectiveZip64Mode(this.entry.entry);
boolean actuallyNeedsZip64 = this.handleSizesAndCrc(bytesWritten, realCrc, effectiveMode);
this.closeEntry(actuallyNeedsZip64, false);
this.streamCompressor.reset();
}
这里有调用过 **this.closeEntry()**这个方法,而在这个方法里:
private void closeEntry(boolean actuallyNeedsZip64, boolean phased) throws IOException {
if (!phased && this.channel != null) {
this.rewriteSizesAndCrc(actuallyNeedsZip64);
}
if (!phased) {
this.writeDataDescriptor(this.entry.entry);
}
this.entry = null; // 这里
}
就是说如果执行到该方法里,那么会把 entry 置为 null。现在我们反推,即 未执行到 closeEntry() ——> 未执行到 closeArchiveEntry() ——> 即 injectData() 方法里未执行到try-finally语句中的
finally {
zos.closeArchiveEntry(); // 关闭压缩的entry
}
继续往上推,try-finally语句如果没有执行,在 injectData() 方法里 就表明 while循环 也没有执行,也就是最外层的try-finally语句,try块捕捉到了异常,但是没有处理(或交给上层处理?) 执行了finally语句中的finish()方法结束这个压缩输出流。又因为这个压缩文件里包含未关闭的entries导致抛出异常。
分析到这里我就止步了,就是说.xlsx的文件流有问题未进行遍历直接finish,造成entries未及时关闭的异常。
再来看POI-3.17
同样路径,workbook.write() 方法进入:
public void write(OutputStream stream) throws IOException {
this.flushSheets();
File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx");
boolean deleted;
try {
FileOutputStream os = new FileOutputStream(tmplFile);
try {
this._wb.write(os);
} finally {
os.close();
}
ZipFileZipEntrySource source = new ZipFileZipEntrySource(new ZipFile(tmplFile));
this.injectData(source, stream);
} finally {
deleted = tmplFile.delete();
}
if (!deleted) {
throw new IOException("Could not delete temporary file after processing: " + tmplFile);
}
}
刷写sheets到磁盘以及临时文件都一样,然后并没有繁琐的异常抛出捕获语句。之后的injectData()处理的代码存在不同:
injectData() :
protected void injectData(ZipEntrySource zipEntrySource, OutputStream out) throws IOException {
try {
ZipOutputStream zos = new ZipOutputStream(out);
InputStream is;
try {
for(Enumeration en = zipEntrySource.getEntries(); en.hasMoreElements(); is.close()) {
ZipEntry ze = (ZipEntry)en.nextElement();
zos.putNextEntry(new ZipEntry(ze.getName()));
is = zipEntrySource.getInputStream(ze);
XSSFSheet xSheet = this.getSheetFromZipEntryName(ze.getName());
if (xSheet != null && !(xSheet instanceof XSSFChartSheet)) {
SXSSFSheet sxSheet = this.getSXSSFSheet(xSheet);
InputStream xis = sxSheet.getWorksheetXMLInputStream();
try {
copyStreamAndInjectWorksheet(is, zos, xis);
} finally {
xis.close();
}
} else {
IOUtils.copy(is, zos);
}
}
} finally {
zos.close();
}
} finally {
zipEntrySource.close();
}
}
与4.1.2的区别几乎仅仅在于使用了不同的类方法, ZipArchiveOutputStream 和 ZipOutputStream。
3.17 流的使用较为简单,但是4.1.2还要关闭entries再关闭流。(理论上来说版本越高,针对捕获的异常进行处理更为精细,但是本篇文章开头问题,显然导出用低版本的较为适合)
POI 源码步骤分析
我们知道poi导出的 excel文件其实是由一些 .xml文件组成的,不信可以将你的某个 .xlsx文件命名为 .zip后缀,可以解压查看内部包含文件,格式文件以及sheet数据文件。
- write方法中, this.flushSheets(); 将sheet数据刷新到磁盘。继续往内层走,等全部写完后, 更新标志位 this.allFlushed = true;
- 然后指定路径下创建一个poi-sxssf-template随机数.xlsx的临时文件;
- 根据用户的自定义信息将其封装写入该临时文件;
- 将创建的sheet、样式等写入 临时文件;
- 将生成的sheet数据文件替换模板条目,处理数据(即解压模板文件获取其文件流,将excel数据写入);
- 关闭文件流,删除生成的临时文件。
而在处理数据的方法中的逻辑步骤:
- 遍历xml文件
- 如果xml文件的名称与sheet名称一致,则需要获取实际的数据存放位置文件,进行数据的封装和写入到xml中
- 如果xml文件不属于sheet,则直接将文件流进行拷贝输出
从这部分逻辑可以看出,它为什么要创建这个poi-sxssf-template临时文件,因为它有xlsx共有的xml文件,所以需要这个临时文件去输出到用户指定的流里,每个xlsx文件的实际区别主要是sheetN.xml的数据文件。
总体流程就是,用户每次创建的sheet,都会产生一个文件输出流,workbook开始调用 write方法时,会根据 flusheets——> flushRows() 将数据刷写到磁盘。最后在处理数据时,将数据写入到输出流遍历封装成xml文件输出。最后删除临时文件。
最后
其实 高版本出问题指向的就是我的压缩包不完整了,尤其是大数据量时。 有看到一位老哥提过一嘴,说是有可能有以下几个原因或方法:
① 网络不稳定
② 设置缓存 和超时设置增大一些。
毕竟正式服务器压力不是测试及本地环境可比的。 留作记录。
有想法的欢迎踢我,我虚心学习~
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/157254.html