起因
业务中需要导出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文件相关的有两个,XSSFSheet
和SXSSFSheet
,Hutool的工具类在使用ExcelUtil.getBigWriter()
后得到的Sheet
是SXSSFSheet
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行,就会被清除,后面再读取就会报空指针异常
解决
- 使用
XSSFSheet
将ExcelUtil.getBigWriter()
改为ExcelUtil.getWriter(true)
,这样创建的Sheet对象就是XSSFSheet,XSSFSheet中存储row的是一个List,不是缓存,不会被清空,这样就可以在后续操作中读取到相关属性
不过这种方式在数据量大的情况下会出现性能问题,甚至会OOM
- 新增另外的字段提前存储所需内容
根据业务要求,最后写入图片逻辑中需要读取的也就是单元格的长宽数据,可以在写入一般数据时先把所需的长款数据临时存起来,用的时候再直接调用就行