Apache POI写入行数据后行数据消失

Apache POI写入行数据后行数据消失

LonelyMan 7 2024-04-17

起因

业务中需要导出Excel,处理流程是:先写入表头,再写入除了图片外的所有行内容,最后再插入图片

        ExcelWriter writer = ExcelUtil.getBigWriter();
        Workbook workbook = writer.getWorkbook();
        Sheet poiSheet = workbook.getSheetAt(0);
        writeHeader(poiSheet, columns, createHeaderCellStyle(workbook));
        writeRows(poiSheet, rows);
        insertImages(poiSheet, getSheetImages(data, sheetId));

writeRows()方法内会去查询在写入行列时设定的行高列宽,但是在测试时发现只要数据行数大于99,调用poiSheet.getRow(rowIndex)时会获取不到该行,导致获取其行高时报空指针异常

分析

Debug发现,poiSheet.getRow(1)的结果在执行writeRows()方法后变为null ,所以可以判断是writeRows()中的sheet.createRow()方法导致的

POI的Sheet有多种实现,与新版XLSX格式的Excel文件相关的有两个,XSSFSheetSXSSFSheet,Hutool的工具类在使用ExcelUtil.getBigWriter()后得到的SheetSXSSFSheet

SXSSFSheet中存在一个字段_randomAccessWindowSize

public class SXSSFSheet implements Sheet, OoxmlSheetExtensions {
    /*package*/ final XSSFSheet _sh;
    protected final SXSSFWorkbook _workbook;
    private final TreeMap<Integer,SXSSFRow> _rows = new TreeMap<>();
    protected SheetDataWriter _writer;
    private int _randomAccessWindowSize = SXSSFWorkbook.DEFAULT_WINDOW_SIZE;
    protected final AutoSizeColumnTracker _autoSizeColumnTracker;
    private int outlineLevelRow;
    private int lastFlushedRowNumber = -1;
    private boolean allFlushed;
    
    ....................
}

该字段代表的是一个内部窗口大小,它是SXSSFSheet类的一个核心配置参数,决定了在内存中缓存的最大行数,默认值为100

    @Override
    public SXSSFRow createRow(int rownum) {
        int maxrow = SpreadsheetVersion.EXCEL2007.getLastRowIndex();
        ................

        SXSSFRow newRow = new SXSSFRow(this);
        newRow.setRowNumWithoutUpdatingSheet(rownum);
        _rows.put(rownum, newRow);
        allFlushed = false;
        if(_randomAccessWindowSize >= 0 && _rows.size() > _randomAccessWindowSize) {
            try {
                flushRows(_randomAccessWindowSize);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
        }
        return newRow;
    }

SXSSFSheet_rows并不是存储所有行数据的池子,而是一个缓存

SXSSFSheet中每次新增行后,会比对缓存中的行数是否大于该窗口大小,如果大于就会把第一条行数据写入到临时文件中,并清除该行

这个逻辑在直接导出时没有影响,但是在业务中,写入所有行数据后还需要读取之前写入的行数据的内容,例如文章开头的例子,表头占一行,写入99条数据后刚好100行,如果再写入一条数据,缓存中的第一条数据,也就是第0行,就会被清除,后面再读取就会报空指针异常

解决

  1. 使用XSSFSheet

ExcelUtil.getBigWriter()改为ExcelUtil.getWriter(true),这样创建的Sheet对象就是XSSFSheet,XSSFSheet中存储row的是一个List,不是缓存,不会被清空,这样就可以在后续操作中读取到相关属性

不过这种方式在数据量大的情况下会出现性能问题,甚至会OOM

  1. 新增另外的字段提前存储所需内容

根据业务要求,最后写入图片逻辑中需要读取的也就是单元格的长宽数据,可以在写入一般数据时先把所需的长款数据临时存起来,用的时候再直接调用就行