在使用 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
