在使用 EasyExcel 中的遇到的一个异常场景。由于不影响线上,而且抛出的异常比较古怪,所以拖了很久,今天终于找到问题原因了,这里做下总结。
Issue1872
背景
[Finalizer] WARN [com.alibaba.excel.ExcelWriter] ExcelWriter.java:342 - [] - Destroy object failed
com.alibaba.excel.exception.ExcelGenerateException: Can not close IO.
at com.alibaba.excel.context.WriteContextImpl.finish(WriteContextImpl.java:378)
at com.alibaba.excel.write.ExcelBuilderImpl.finish(ExcelBuilderImpl.java:95)
at com.alibaba.excel.ExcelWriter.finish(ExcelWriter.java:329)
at com.alibaba.excel.ExcelWriter.finalize(ExcelWriter.java:340)
at java.lang.System$2.invokeFinalize(System.java:1270)
at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:102)
根据异常内容,可以得知异常是由Finalizer
线程抛出,在执行 ExcelWriter
的 finish()
方法时发生了异常导致的。
源代码
// WriteContextImpl.finish 方法的部分源码
try {
if (writeExcel) {
writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream());
}
writeWorkbookHolder.getWorkbook().close();
} catch (Throwable t) {
throwable = t;
}
由源码看到,EasyExcel 在执行 finish()
操作时会首先向输出流执行写入 Workbook
后再关闭。
// 构造 ExcelWriterBuilder
final ExcelWriterBuilder builder = EasyExcel.write(file);
// 使用 builder 构造 ExcelWriter
final ExcelWriter writer = builder.build();
// 使用 builder 构造 ExcelWriterSheetBuilder
final ExcelWriterSheetBuilder sheetBuilder = builder.sheet(0);
final WriteSheet sheet = sheetBuilder.build();
writer.write(data(), sheet);
writer.finish();
我想大部分抛出该异常问题的人都是和我一样,先通过 EasyExcel
构造出了一个 ExcelWriterBuilder
,然后通过其分别构造了 ExcelWriter
和 ExcelWriterSheetBuilder
,之后又通过 ExcelWriterSheetBuilder
构造出 WriteSheet
,最后通过上面构造的 ExcelWriter
和 WriteSheet
进行写入。
这种构造方式会导致一个问题,是在于由ExcelWriterBuilder
构造的 ExcelWriterSheetBuilder
会额外持有一个 ExcelWriter
对象,这里姑且称之为 B
对象。B
对象与我们通过 ExcelWriterBuilder
构造出来的 ExcelWriter
A
对象持有相同的输出流。这就导致由于 A
对象会先执行 finsh()
操作关闭输出流,而B
对象在之后执行finsh()
方法时尝试写入输出流时写入失败,从而抛出异常。
正确的写法
final File file = new File(UUID.randomUUID() + ".xlsx");
// 使用 EasyExcel 构造 ExcelWriter
final ExcelWriter writer = EasyExcel.write(file).build();
// 使用 EasyExcel 构造 WriteSheet
final WriteSheet sheet = EasyExcel.writeSheet(0).build();
writer.write(data(), sheet);
writer.finish();
file.deleteOnExit();
使用 EasyExcel
构造 ExcelWriterSheetBuilder
对象而不是使用ExcelWriterBuilder
构造 ExcelWriterSheetBuilder
对象可以避免这个异常。这样可以绕开 ExcelWriterBuilder
构造 ExcelWriterSheetBuilder
时创建的额外的 ExcelWriter
对象。
深入分析
异常抛出的对象是 ExcelWriter
,抛出异常的原因是 Can not close IO
,造成异常的关键逻辑是 WriteSheet
的构造存在问题,现在我们深入代码进行分析。
有什么区别呢?
关键在于 ExcelWriterSheetBuilder
的构造方式。
使用 EasyEscel
构造 ExcelWriterSheetBuilder
ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder();
//...
return excelWriterSheetBuilder;
通过这种方式构造的 ExcelWriterSheetBuilder
不会持有 ExcelWriter
对象,在对象回收时不会抛出异常。
使用 ExcelWriterBuilder
构造 ExcelWriterSheetBuilder
ExcelWriter excelWriter = build();
// 先从 ExcelWriterBuilder 中构造了一个 ExcelWriter,并且传入到了 ExcelWriterSheetBuilder 中
ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
// ...
return excelWriterSheetBuilder;
通过 ExcelWriterBuilder
构造的 ExcelWriterSheetBuilder
的对象会通过 ExcelWriterBuilder
构造并持有一个 ExcelWriter
对象,它与我们直接通过 ExcelWriterBuilder
构造的 ExcelWriter
对象持有相同的输出流。
问题就出在了额外构造的 ExcelWriter
对象,如果是使用 ExcelWriter
进行写入的话,就很容易忽略掉这个额外构造的对象,像下面这样:
final ExcelWriterBuilder builder = EasyExcel.write(file);
// 使用 ExcelWriterBuilder 对象同时构造了 ExcelWriter 和 ExcelWriterSheetBuilder 对象
final ExcelWriter writer = builder.build();
final ExcelWriterSheetBuilder sheetBuilder = builder.sheet(0);
final WriteSheet sheet = sheetBuilder.build();
// 通过 ExcelWriter 进行写入到 WriteSheet 中
writer.write(data(), sheet);
writer.finish();
如上面的代码,当通过 sheetBuilder
构造 WriteSheet
时,会发现我们根本就没有注意到 ExcelWriterSheetBuilder
对象中还有一个 ExcelWriter
对象,也就是说上面代码中会出现两个 ExcelWriter
对象。
我们显式创建的 ExcelWriter
对象writer
会通过调用finish()
将其正常的结束掉,但是ExcelWriterSheetBuilder
中的 ExcelWriter
对象就会被我们忽略掉,这个对象会在垃圾回收时通过调用 Object.finalize()
方法中隐式调用 finish()
方法进行结束。由于两个 ExcelWriter
都持有相同的输出流,在第一个ExcelWriter
对象已经关闭了输出流的情况下第二个ExcelWriter
在这之后尝试向输出流中写入数据则会抛出异常。
从设计的角度来看,这里 ExcelWriterSheetBuilder
的设计是开发者取巧了。这里提供了更多的工具方法,但是使用不当的话就会抛出一个莫名其妙的异常。个人认为不是一个合理设计,代码逻辑上来看没什么问题,但是对于使用者来说就比较痛苦了,成为一个坑。
以上是我的总结,希望能帮到你。
参考资料
使用easyexcle导出时异常ExcelGenerateException: Can not close IO,并且下载到异常zip包
使用EasyExcel导出Excel时报错 Can not close IO 及EasyExcel.write()方法找不到,已解决
EasyExcel Yuque
EasyExcel Github