在使用 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线程抛出,在执行 ExcelWriterfinish()方法时发生了异常导致的。
源代码

// 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 ,然后通过其分别构造了 ExcelWriterExcelWriterSheetBuilder,之后又通过 ExcelWriterSheetBuilder构造出 WriteSheet,最后通过上面构造的 ExcelWriterWriteSheet进行写入。

EasyExcel
ExcelWriterBuilder
ExcelWriter
ExcelWriterSheetBuilder
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 对象。

EasyExcel
ExcelWriterBuilder
ExcelWriter
ExcelWriterSheetBuilder
WriteSheet

深入分析

异常抛出的对象是 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

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐