[alibaba/easyexcel]使用easyexcle导出时异常ExcelGenerateException: Can not close IO,并且下载到异常zip包

2024-05-11 181 views
7
   ExportDisbursementTotalVO exportDisbursementTotalVO = projectFundsService.exportDisbursementTotal(projectId);
        if (exportDisbursementTotalVO != null) {
            ServletOutputStream out = null;
            try {
                out = response.getOutputStream();
                ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder();
                excelWriterBuilder.excelType(ExcelTypeEnum.XLSX);
                excelWriterBuilder.file(out);
                excelWriterBuilder.autoCloseStream(true);
                ExcelWriter writer = excelWriterBuilder.build();
                WriteSheet writeSheet = excelWriterBuilder.sheet().build();
                writeSheet.setNeedHead(true);
                writeSheet.setHead(exportDisbursementTotalVO.getHeadList());
                String fileName = "预算支出合计信息";
                writer.write(exportDisbursementTotalVO.getDisbursementTotalMapList(), writeSheet);
                response.setHeader("Content-Disposition", "attachment;filename=" + DownloadUtil.getCodedFileName(request, fileName + ".xlsx"));
                writer.finish();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (out != null) {
                        out.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

异常提示 [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) at java.lang.ref.Finalizer.access$100(Finalizer.java:34) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:217) Caused by: java.lang.NullPointerException: null at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:530) at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:73) at org.apache.coyote.http11.Http11OutputBuffer.doWrite(Http11OutputBuffer.java:189) at org.apache.coyote.Response.doWrite(Response.java:599) at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:329) at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:766) at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:288) at org.apache.catalina.connector.OutputBuffer.close(OutputBuffer.java:241) at org.apache.catalina.connector.CoyoteOutputStream.close(CoyoteOutputStream.java:157) at com.alibaba.excel.context.WriteContextImpl.finish(WriteContextImpl.java:356) ... 7 common frames omitted [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) at java.lang.ref.Finalizer.access$100(Finalizer.java:34) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:217) 并且,下载到了一个zip包:

建议描述

回答

9

excelWriter.finish(); 用这个方法关闭io流试试的

8

excelWriter.finish(); 用这个方法关闭io流试试的

上边代码已经调用了这个方法了

9

能提供复现bug的最小的完整代码吗,包括实体类,和完整的Junit test? @PhageT2

3

我也遇到了这种情况的报错,原因是导出接口设置的超时时间是15S 然后超过15S 就会报这个错误同时生成一个错误的excel image 代码 image @lethal233

3

我也遇到这个问题了,问题同上,是在执行GC时尝试回收 ExcelWriter 对象时关闭流失败引起的异常, 这个异常在多次执行生成excel后,在gc尝试销毁 ExcelWriter 执行了 finish 操作。

public class GcBugTest {

    List<List<?>> data() {
        final String[][] datas = {{"1234", "5678"}
                , {"1234", "5678"}
                , {"1234", "5678"}
        };
        return Arrays.stream(datas).map(Arrays::asList).collect(Collectors.toList());
    }

    public void write() {
        final File file = new File(UUID.randomUUID() + ".xlsx");
        final ExcelWriterBuilder builder = EasyExcel.write(file).autoCloseStream(true);
        final ExcelWriter writer = builder.build();
        final ExcelWriterSheetBuilder sheetBuilder = builder.sheet(0);
        final WriteSheet sheet = sheetBuilder.build();
        writer.write(data(), sheet);
        writer.finish();
        file.deleteOnExit();
    }

    @Test
    public void singleThread() {
        final Logger logger = LoggerFactory.getLogger(this.getClass());
        for (int i = 0; i < 100; i++) {
            logger.info("try to create excel by {} times", i);
            write();
            System.gc();
        }
    }
}

excel-demo-bug.zip

7
2021-06-07 15:00:11.715  WARN 17536 --- [      Finalizer] com.alibaba.excel.ExcelWriter            : Destroy object failed

com.alibaba.excel.exception.ExcelGenerateException: Can not close IO.
    at com.alibaba.excel.context.WriteContextImpl.finish(WriteContextImpl.java:358)
    at com.alibaba.excel.write.ExcelBuilderImpl.finish(ExcelBuilderImpl.java:101)
    at com.alibaba.excel.ExcelWriter.finish(ExcelWriter.java:328)
    at com.alibaba.excel.ExcelWriter.finalize(ExcelWriter.java:338)
    at java.lang.System$2.invokeFinalize(System.java:1270)
    at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:102)
    at java.lang.ref.Finalizer.access$100(Finalizer.java:34)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:217)
Caused by: java.io.IOException: Stream Closed
    at java.io.FileOutputStream.writeBytes(Native Method)
    at java.io.FileOutputStream.write(FileOutputStream.java:326)
    at java.util.zip.DeflaterOutputStream.deflate(DeflaterOutputStream.java:253)
    at java.util.zip.ZipOutputStream.closeEntry(ZipOutputStream.java:255)
    at java.util.zip.ZipOutputStream.finish(ZipOutputStream.java:360)
    at java.util.zip.DeflaterOutputStream.close(DeflaterOutputStream.java:238)
    at java.util.zip.ZipOutputStream.close(ZipOutputStream.java:377)
    at org.apache.poi.xssf.streaming.SXSSFWorkbook.injectData(SXSSFWorkbook.java:401)
    at org.apache.poi.xssf.streaming.SXSSFWorkbook.write(SXSSFWorkbook.java:936)
    at com.alibaba.excel.context.WriteContextImpl.finish(WriteContextImpl.java:314)
    ... 7 common frames omitted
6
        String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        ExcelWriter excelWriter = null;
        try {
            // 这里 需要指定写用哪个class去写
            excelWriter = EasyExcel.write(fileName, DemoData.class).build();
            // 这里注意 如果同一个sheet只要创建一次
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
            for (int i = 0; i < 5; i++) {
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<DemoData> data = data();
                excelWriter.write(data, writeSheet);
            }
        } finally {
            // 千万别忘记finish 会帮忙关闭流
            if (excelWriter != null) {
                excelWriter.finish();
            }

代码写法有问题 参照。

0

这个问题在于 WriteSheet 的具体构造方式问题,在构造 WriteSheet 时如果使用上面构造出来的 ExcelWriterBuilder 去构造 WriteSheet 的话,之后就会抛出异常如:

ExcelWriterBuilder builder = EasyExcel.write(file);
// 复用 ExcelWriterBuilder,同时构造了 ExcelWriter 和 WriteSheet
// 构造 ExcelWriter
ExcelWriter excelWriter = builder.build();
// 构造 WriteSheet
WriteSheet writeSheet = builder.sheet("模板").build();
excelWriter.write(data(), writeSheet);

如果使用上述的代码去写入的话,就会抛 Can not close IO 的问题。 如果要正确处理该问题,可以不适用相同的 ExcelWriterBuilder 去构造 ExcelWriterWriteSheet,像 zhuangjiaju 写的那样,就不会报错了:

// 单独使用 EasyExcel 分别构造 ExcelWriter 和 WriteSheet
// 构造 ExcelWriter
ExcelWriter excelWriter = EasyExcel.write(file).build();
// 构造 WriteSheet
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);

到这里,应该是可以理解为对象之间的引用存在问题,导致 GC 过程中销毁不可达对象时,发生了异常销毁,如果遇到这个问题可以临时这样解决问题,之后再详细看下这里吧。

6

这个缺陷的问题在于当复用 ExcelWriterBuilder,同时构造 ExcelWriter 和 WriteSheet 时,会出现问题。 在 构造 ExcelWriter 的时候,会通过 ExcelWriterBuilder 持有 WriteWorkbook 对象,从而间接的持有了输出流; 但是在使用相同的 ExcelWriterBuilder 对象构造 WriteSheet 的时候,会为 WriterSheet 创建一个新的 ExcelWriter 对象,同样会通过 ExcelWriterBuilder 持有 WriteWorkbook 对象,也会间接持有相同的输出流。 file

public ExcelWriterSheetBuilder sheet(Integer sheetNo, String sheetName) {
    // 创建一个新的 ExcelWriter,并持有 WriteWorkbook 对象
    ExcelWriter excelWriter = build();
    ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
    //...
    return excelWriterSheetBuilder;
}

这样就会导致当我们在后续的代码中即使finish()了我们构造的 ExcelWriter 对象,进程中还会存在一个由 WriteSheet 持有的 ExcelWriter 对象没有没正常通过 finish() 关闭,而在之后的 gc 过程中通过 Object.finalize() 执行了 finish()调用。而由于之前已经通过 ExcelWriterfinish() 方法关闭了输出流,从而导致后面的 ExcelWriter 尝试关闭输出流时报错。

我想EasyExcel开发者最初的想法是可以直接创建 WriterSheet 后不同创建 ExcelWriter 也能够直接执行写入。但可能是存在问题吧就没有推荐这种方式。

对于使用者来说通过 ExcelWriterBuilder 对象能够同时构造后面要使用的 ExcelWriter 对象和 WriteSheet对象从理解上来说会感觉更合理,代码也会更简洁,但是却不小心引入了另外的问题。

个人感觉这里还是设计的有些不合理,提供了多种 WriterSheet 的构造方式但是没有说明会给使用者带来困扰。

// 当直接使用 ExcelWriter 写入的时候应该使用 EasyExcel.writerSheet("模板").build(); 方式构造 WriteSheet 对象
ExcelWriter excelWriter = EasyExcel.write(file).build();
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
// 或者下面的方式,不需要构造 ExcelWriter,因为在构造 WriteSheet 的时候会帮你构造一个 ExcelWriter
EasyExcel.write(file).sheet("模板").doWrite(data());
// doWrite 中会帮你自动 finish() 你不可见的 ExcelWriter
6

这个缺陷的问题在于当复用 ExcelWriterBuilder,同时构造 ExcelWriter 和 WriteSheet 时,会出现问题。 在 构造 ExcelWriter 的时候,会通过 ExcelWriterBuilder 持有 WriteWorkbook 对象,从而间接的持有了输出流; 但是在使用相同的 ExcelWriterBuilder 对象构造 WriteSheet 的时候,会为 WriterSheet 创建一个新的 ExcelWriter 对象,同样会通过 ExcelWriterBuilder 持有 WriteWorkbook 对象,也会间接持有相同的输出流。 file

public ExcelWriterSheetBuilder sheet(Integer sheetNo, String sheetName) {
    // 创建一个新的 ExcelWriter,并持有 WriteWorkbook 对象
    ExcelWriter excelWriter = build();
    ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
    //...
    return excelWriterSheetBuilder;
}

这样就会导致当我们在后续的代码中即使finish()了我们构造的 ExcelWriter 对象,进程中还会存在一个由 WriteSheet 持有的 ExcelWriter 对象没有没正常通过 finish() 关闭,而在之后的 gc 过程中通过 Object.finalize() 执行了 finish()调用。而由于之前已经通过 ExcelWriterfinish() 方法关闭了输出流,从而导致后面的 ExcelWriter 尝试关闭输出流时报错。

我想EasyExcel开发者最初的想法是可以直接创建 WriterSheet 后不同创建 ExcelWriter 也能够直接执行写入。但可能是存在问题吧就没有推荐这种方式。

对于使用者来说通过 ExcelWriterBuilder 对象能够同时构造后面要使用的 ExcelWriter 对象和 WriteSheet对象从理解上来说会感觉更合理,代码也会更简洁,但是却不小心引入了另外的问题。

个人感觉这里还是设计的有些不合理,提供了多种 WriterSheet 的构造方式但是没有说明会给使用者带来困扰。

// 当直接使用 ExcelWriter 写入的时候应该使用 EasyExcel.writerSheet("模板").build(); 方式构造 WriteSheet 对象
ExcelWriter excelWriter = EasyExcel.write(file).build();
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
// 或者下面的方式,不需要构造 ExcelWriter,因为在构造 WriteSheet 的时候会帮你构造一个 ExcelWriter
EasyExcel.write(file).sheet("模板").doWrite(data());
// doWrite 中会帮你自动 finish() 你不可见的 ExcelWriter

我在项目中也遇到了这个问题

      // 初始化excelWriter
      ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder();
      excelWriterBuilder.file(response.getOutputStream());
      excelWriterBuilder.head(head);
      excelWriterBuilder.sheet(sheetName);//异常来自这里
      // 初始化sheet
      WriteSheet writeSheet = new WriteSheet();
      writeSheet.setSheetName(sheetName);
      writeSheet.setSheetNo(RandomUtil.randomInt());
      return new ExcelStream<>(excelWriterBuilder.build(), writeSheet, writeTable);

按照上面的代码生成一个ExcelWriter对象,在接下来的操作里面可以循环write数据,在全部写入完成之后调用finish方法写出文件到Response。问题在过程中总是会异常的关闭流,根据日志发现finish方法在ExcelWriter.finalize()方法中被调用,问题finalize()方法是被GC调用,打断点也不能发现问题,看了你的回答才发现ExcelWriterBuilder.sheet(String sheetName)方法会生成一个一个新的ExcelWriter,这个ExcelWriter持有了response.getOutputStream,但是我在代码中没有使用链式调用,所以这个方法中生成的ExcelWriter实例没有使用会被回收,回收时执行finish方法然后就把outputStream关闭了 这个ExcelWriterBuilder.sheet(String sheetName)设计太奇怪了