SXSSF导出:对比一下poi-3.17版本和poi-4.1.2(4.0以上)版本的改动

文章详细分析了Apache POI 4.1.2和3.17版本在处理大量数据导出Excel时的源码差异,指出4.1.2版本由于压缩文件未关闭entries引发的IOException问题,而3.17版本则没有此类问题。作者建议在大数据导出场景下使用低版本的POI。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如题

写这篇文章源于前不久在用到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数据文件。

  1. write方法中, this.flushSheets(); 将sheet数据刷新到磁盘。继续往内层走,等全部写完后, 更新标志位 this.allFlushed = true;
  2. 然后指定路径下创建一个poi-sxssf-template随机数.xlsx的临时文件;
  3. 根据用户的自定义信息将其封装写入该临时文件;
  4. 将创建的sheet、样式等写入 临时文件;
  5. 将生成的sheet数据文件替换模板条目,处理数据(即解压模板文件获取其文件流,将excel数据写入);
  6. 关闭文件流,删除生成的临时文件。

而在处理数据的方法中的逻辑步骤:

  1. 遍历xml文件
  2. 如果xml文件的名称与sheet名称一致,则需要获取实际的数据存放位置文件,进行数据的封装和写入到xml中
  3. 如果xml文件不属于sheet,则直接将文件流进行拷贝输出

从这部分逻辑可以看出,它为什么要创建这个poi-sxssf-template临时文件,因为它有xlsx共有的xml文件,所以需要这个临时文件去输出到用户指定的流里,每个xlsx文件的实际区别主要是sheetN.xml的数据文件。

总体流程就是,用户每次创建的sheet,都会产生一个文件输出流,workbook开始调用 write方法时,会根据 flusheets——> flushRows() 将数据刷写到磁盘。最后在处理数据时,将数据写入到输出流遍历封装成xml文件输出。最后删除临时文件。

最后

其实 高版本出问题指向的就是我的压缩包不完整了,尤其是大数据量时。 有看到一位老哥提过一嘴,说是有可能有以下几个原因或方法:
① 网络不稳定
② 设置缓存 和超时设置增大一些。

毕竟正式服务器压力不是测试及本地环境可比的。 留作记录。

有想法的欢迎踢我,我虚心学习~

java_POI教程.pdf javaPOI操作Excel文件.doc POI_API帮助文档.chm poi-bin-3.9-20121203.tar.gz poi帮助.docx POI中文帮助文档.pdf poi中文教程.doc第一章 POI简介 实际的开发中,表现层的解决方案虽然有多样,但是IE浏览器已成为最多人使用的浏览器,因为大家都用Windows。在企业办公系统中,常常有客户这样子要求:你要把我们的报表直接用Excel打开(电信系统、银行系统)。或者是:我们已经习惯用Excel打印。 workbook(HSSFWorkbook),一个workbook可以有多个sheet(HSSFSheet)组成,一个sheet是由多个row(HSSFRow)组成,一个row是由多个cell(HSSFCell)组成。 POI可以到www.apache.org下载到。实际运行时,需要有poi包就可以了。HSSF提供给用户使用的对象在rg.apache.poi.hssf.usermodel包中,主要部分包括Excel对象,样式格式,还有辅助操作。有以下几种对象: HSSFWorkbook excel的文档对象 HSSFSheet excel的表单 HSSFRow excel的行 HSSFCell excel的格子单元 HSSFFont excel字体 HSSFDataFormat 日期格式 在poi1.7中才有以下2项: HSSFHeader sheet头 HSSFFooter sheet尾(只有打印的时候才能看到效果) 这个样式 HSSFCellStyle cell样式 辅助操作包括 HSSFDateUtil 日期 HSSFPrintSetup 打印 HSSFErrorConstants 错误信息表 以下可能需要使用到如下的类 import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFCellStyle; import org.apache.poi.hssf.usermodel.HSSFDataFormat; import org.apache.poi.hssf.usermodel.HSSFFont; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.util.HSSFColor; 先看poi的examples包中提供的最简单的例子,建立一个空xls文件。 import java.io.FileOutputStream; import java.io.IOException; import org.apache.poi.hssf.usermodel.HSSFWorkbook; public class ExcelSample1 { public static void main(String[] args) throws IOException { //创建一个excel文件 HSSFWorkbook wb= new HSSFWorkbook(); FileOutputStream fileOut= new FileOutputStream("c:\\workbook.xls"); // FileOutputStream fileOut= new FileOutputStream("c:/workbook.xls"); wb.write(fileOut); fileOut.close(); } } 通过这个例子,我们在c盘下建立的是一个空白的xls文件(不是空文件)。在此基础上,我们可以进一步看其它的例子。 import org.apache.poi.hssf.usermodel.*; import java.io.FileOutputStream; import java.io.IOException; public class CreateCells { public static void main(String[] args) throws IOException { HSSFWorkbook wb = new HSSFWorkbook(); //建立新HSSFWorkbook对象 HSSFSheet sheet = wb.createSheet("new sheet"); //建立新的sheet对象 HSSFRow row = sheet.createRow((short)0); //在sheet里创建一行,参数为行号(第一行,此处可想象成数组) HSSFCell cell = row.createCell((short)0); //在row里建立新cell(单元格),参数为列号(第一列) cell.setCellvalue(1); //设置cell的整数类型的值 row.createCell((short)1).setCellvalue(1.2); //设置cell浮点类型的值 row.createCell((short)2).setCellvalue("test"); //设置cell字符类型的值 row.createCell((short)3).setCellvalue(true); //设置cell布尔类型的值 HSSFCellStyle cellStyle = wb.createCellStyle(); //建立新的cell样式 cellStyle.setDataFormat(HSSFDataFormat. getBuiltinFormat("m/d/yy h:mm")); //设置cell样式为定制的日期格式 HSSFCell dCell =row.createCell((short)4); dCell.setCellvalue(new Date()); //设置cell为日期类型的值 dCell.setCellStyle(cellStyle); //设置该cell日期的显示格式 HSSFCell csCell =row.createCell((short)5); csCell.setEncoding(HSSFCell.ENCODING_UTF_16); //设置cell编码解决中文高位字节截断 csCell.setCellvalue("中文测试_Chinese Words Test"); //设置中西文结合字符串 row.createCell((short)6).setCellType(HSSFCell.CELL_TYPE_ERROR); //建立错误cell FileOutputStream fileOut = new FileOutputStream("workbook.xls"); wb.write(fileOut); fileOut.close(); } } 通过这个例子,我们可以清楚的看到xls文件从大到小包括了HSSFWorkbook HSSFSheet HSSFRow HSSFCell这样几个对象。我们可以在cell中设置各种类型的值。 尤其要注意的是如果你想正确的显示非欧美的字符时,尤其象中日韩这样的语言,必须设置编码为16位的即是HSSFCell.ENCODING_UTF_16,才能保证字符的高8位不被截断而引起编码失真形成乱码。 其他测试可以通过参考examples包中的测试例子掌握poi的详细用法,包括字体的设置,cell大小低纹的设置等。需要注意的是POI是一个仍然在完善中的公开代码的项目,所以有些功能正在不断的扩充。 感觉上面的操作比较的繁琐,然后就自己写了一个方法。这个方法不需要事先创建rowcell,直接进行cteateCell就可以了,在程序中会自动进行判断,如果不存在的话会创建。 private static void cteateCell(HSSFWorkbook wb,HSSFRow row,short col,short align,String val){ HSSFCell cell = row.createCell(col); cell.setEncoding(HSSFCell.ENCODING_UTF_16); cell.setCellValue(val); HSSFCellStyle cellstyle = wb.createCellStyle(); cellstyle.setAlignment(align); cell.setCellStyle(cellstyle); } 对里面的几个参数的说明: short col 应该是你的cell单元格的位置也就是列号; short align 应该是你的对齐方式; String val 应该是你单元格里面要添加的值; 具体的调用如下: HSSFRow row = sheet.createRow((short)1); cteateCell(wb,row,(short)0,HSSFCellStyle.ALIGN_CENTER_SELECTION,"SampleID"); 在上边的例子里我们看到了要设置一个单元格里面信息的格式(例如,要将信息居中)设置的操作如下: HSSFCellStyle cellstyle = wb.createCellStyle(); cellstyle.setAlignment(HSSFCellStyle.ALIGN_CENTER_SELECTION); cell.setCellStyle(cellstyle); 还有我们我们经常会用到的合并单元格,在这里我们也有这样的操作,代码如下: sheet.addMergedRegion(new Region(1,(short)1,2,(short)4)); 这里面我们还要介绍一个经常会遇到的问题,就是怎么来冻结一个窗口。poi也为我们集成了这样的事情了。代码如下: sheet.createFreezePane(1,2);  在这里我们需要注意的是 一、 该方法是在一个具体的sheet里面来进行操作。 二、 方法createFreezepane;有2个参数。前一个参数代表列;后一个参数代表行。 上边的代码对应的excel文件如下: 我么在画面上看到了明显的两条黑线,这就是冻结的窗口。 然后我们来看一个完整的STRUTS的小例子,在这个例子里面我们要做的事情是要模拟移动公司的网上营业厅里面的一个功能,我们要把一个客户当月的通话记录各种信息查询出来,并且生成一张excel报表。首先,我们来看一下网上效果的截图。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值