问题描述
POI提供了HSSF、XSSF、SXSSF三种方式操作Excel。其中SXSSF的构造器有一个rowAccessWindowSize,SXSSF的每一个sheet对应一个临时文件,当行数大于rowAccessWindowSize时,就会向临时文件中flush,内存中只保留最后的rowAccessWindowSize条记录,以保证内存的低占用率,假设 rowAccessWindowSize 为100,那么当写入第101条记录时,第一条记录即 sheet.getRow(0) 的结果为 null。
解决方案
由于业务场景需要,在处理excel数据导出时需要回溯之前已写入的数据,那么可以有如下三种方案:
-
设置合适的 rowAccessWindowSize
这个值需要根据具体的场景设置合理值
-
取消rowAccessWindowSize限制
通过源码发现rowAccessWindowSize 值设置为 -1,这样就是不限制大小了。
-
自定义缓存记录已写入的历史数据
这里因为业务的场景限制,无法推测出一个合理的 rowAccessWindowSize 值,而第二种方案则直接失去了SXSSFWorkbook本身的使用场景的意义即降低内存占用率。因为在实际的需求中,只需要获取到已写入的数据单元格值,对于其他的 style等excel属性并不关心,且在缓存数据时可只缓存涉及的单元格数据(假设有20列,但只关注其中3列),这同样可以降低内存占用率,故采用第三种方案。当然为了保证在使用时对已有代码的改动最小,采用了自定义 Workbook类的设计。
详细设计及实现
自定义CSXSSFCell类
package org.apache.poi.xssf.streaming;
import org.apache.poi.ss.usermodel.CellType;
public class CSXSSFCell extends SXSSFCell {
public CSXSSFCell(SXSSFRow row, CellType cellType) {
super(row, cellType);
}
@Override
public void setCellValue(String value) {
super.setCellValue(value);
//针对String的value做增强
SXSSFSheet sxSheet = this.getSheet();
if (sxSheet instanceof CSXSSFSheet) {
((CSXSSFSheet) sxSheet).setCellValue(this.getRowIndex(), this.getColumnIndex(), value);
}
}
}
该类集成SXSSFCell并重写了 setCellValue(String value)方法,只针对 String的value做增强缓存处理,如需增强其他,则可重写 其他 数据类型的 setCellValue方法。
自定义CSXSSFRow类
package org.apache.poi.xssf.streaming;
import cn.hutool.core.util.ReflectUtil;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.CellType;
import java.util.SortedMap;
public class CSXSSFRow extends SXSSFRow {
public CSXSSFRow(SXSSFSheet sheet) {
super(sheet);
}
@Override
public SXSSFCell createCell(int column, CellType type) {
checkBounds(column);
CSXSSFCell cell = new CSXSSFCell(this, type);
//this._cells.put(column, cell);
SortedMap<Integer, SXSSFCell> _cells = (SortedMap<Integer, SXSSFCell>) ReflectUtil.getFieldValue(this, "_cells");
_cells.put(column, cell);
return cell;
}
private static void checkBounds(int cellIndex) {
SpreadsheetVersion v = SpreadsheetVersion.EXCEL2007;
int maxcol = SpreadsheetVersion.EXCEL2007.getLastColumnIndex();
if (cellIndex < 0 || cellIndex > maxcol) {
throw new IllegalArgumentException("Invalid column index (" + cellIndex + "). Allowable column range for " + v.name() + " is (0.." + maxcol + ") or ('A'..'" + v.getLastColumnName() + "')");
}
}
}
该类继承SXSSFRow并重写createCell方法返回自定义CSXSSFCell类,因为SXSSFRow类本身有些属性是private私有的,这里使用了反射绕开权限控制。
自定义CSXSSFSheet类
package org.apache.poi.xssf.streaming;
import cn.hutool.core.util.ReflectUtil;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* 由于 SXSSFSheet 类有方法(如上)的访问修饰符是default,只限于相同包底下的类访问,
* 所以这里新建的子类包名与父类相同 为 org.apache.poi.xssf.streaming
*/
public class CSXSSFSheet extends SXSSFSheet {
private Map<Integer, Map<Integer, Object>> sheetData;
public void clearSheetData() {
if (sheetData != null) {
sheetData.clear();
sheetData = null;
}
}
/**
* 清除 rowStart与rowEnd之间,cellStart与cellEnd之间的单元格数据
* @param rowStart
* @param rowEnd
* @param cellStart
* @param cellEnd
*/
public void clearSheetData(int rowStart, int rowEnd, int cellStart, int cellEnd) {
if (sheetData != null) {
for (int rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
Map<Integer, Object> rowData = sheetData.get(rowIndex);
if (rowData != null) {
for (int cellIndex = cellStart; cellIndex <= cellEnd; cellIndex++) {
rowData.remove(cellIndex);
}
if (rowData.size() == 0) {
//数据为空了,则清除row
rowData = null;
sheetData.remove(rowIndex);
}
}
}
}
}
public CSXSSFSheet setCellValue(int rowIndex, int cellIndex, Object value) {
if (value != null) {
if (sheetData == null) {
sheetData = new HashMap<>();
}
Map<Integer, Object> rowData = sheetData.get(rowIndex);
if (rowData == null) {
rowData = new HashMap<>();
sheetData.put(rowIndex, rowData);
}
rowData.put(cellIndex, value);
}
return this;
}
public Object getCellValue(int rowIndex, int cellIndex) {
if (sheetData == null) {
return null;
}
Map<Integer, Object> rowData = sheetData.get(rowIndex);
if (rowData == null) {
return null;
}
return rowData.get(cellIndex);
}
public CSXSSFSheet(SXSSFWorkbook workbook, XSSFSheet xSheet) throws IOException {
super(workbook, xSheet);
}
@Override
public SXSSFRow createRow(int rownum) {
int maxrow = SpreadsheetVersion.EXCEL2007.getLastRowIndex();
if (rownum >= 0 && rownum <= maxrow) {
if (rownum <= this.getSheetDataWriter().getLastFlushedRow()) {
throw new IllegalArgumentException("Attempting to write a row[" + rownum + "] " + "in the range [0," + this.getSheetDataWriter().getLastFlushedRow() + "] that is already written to disk.");
} else if (this._sh.getPhysicalNumberOfRows() > 0 && rownum <= this._sh.getLastRowNum()) {
throw new IllegalArgumentException("Attempting to write a row[" + rownum + "] " + "in the range [0," + this._sh.getLastRowNum() + "] that is already written to disk.");
} else {
CSXSSFRow newRow = new CSXSSFRow(this);
//this._rows.put(rownum, newRow);
TreeMap<Integer, SXSSFRow> _rows = (TreeMap<Integer, SXSSFRow>) ReflectUtil.getFieldValue(this, "_rows");
_rows.put(rownum, newRow);
//this.allFlushed = false;
ReflectUtil.setFieldValue(this, "allFlushed", false);
//int randomAccessWindowSize = this._randomAccessWindowSize;
int randomAccessWindowSize = (int) ReflectUtil.getFieldValue(this, "_randomAccessWindowSize");
int rowsSize = _rows.size();
if (randomAccessWindowSize >= 0 && rowsSize > randomAccessWindowSize) {
try {
this.flushRows(randomAccessWindowSize);
} catch (IOException var5) {
throw new RuntimeException(var5);
}
}
return newRow;
}
} else {
throw new IllegalArgumentException("Invalid row number (" + rownum + ") outside allowable range (0.." + maxrow + ")");
}
}
}
该类继承SXSSFSheet,内部定义了 sheetData 缓存单元格数据,及封装了操作单元格数据的clearSheetData、setCellValue、getCellValue方法,并重写了 createRow 方法以便返回自定义的 CSXSSFRow,通过反射绕开权限控制操作SXSSFSheet的私有属性。
自定义CSXSSFWorkbook类
package org.apache.poi.xssf.streaming;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import java.io.IOException;
/**
* 由于 SXSSFWorkbook 类有方法的访问修饰符是default,只限于相同包底下的类访问,
* 所以这里新建的子类包名与父类相同 为 org.apache.poi.xssf.streaming
*/
public class CSXSSFWorkbook extends SXSSFWorkbook {
public CSXSSFWorkbook(int rowAccessWindowSize) {
super(rowAccessWindowSize);
}
@Override
CSXSSFSheet createAndRegisterSXSSFSheet(XSSFSheet xSheet) {
CSXSSFSheet CSXSSFSheet;
try {
CSXSSFSheet = new CSXSSFSheet(this, xSheet);
} catch (IOException var4) {
throw new RuntimeException(var4);
}
this.registerSheetMapping(CSXSSFSheet, xSheet);
return CSXSSFSheet;
}
@Override
public CSXSSFSheet createSheet(String sheetName) {
return this.createAndRegisterSXSSFSheet(this.getXSSFWorkbook().createSheet(sheetName));
}
}
该类继承SXSSFWorkbook并重写createSheet方法,由于SXSSFWorkbook底层操作的其实是 XSSFSheet,其映射了 XSSFSheet 与 SXSSFSheet的关系,这里同样需要自定义返回 CSXSSFSheet 并 处理好与 XSSFSheet的映射关系,及重写 createAndRegisterSXSSFSheet方法。
如何使用?
-
创建workbook时由SXSSFWorkbook替换为CSXSSFWorkbook,即:
//workbook = new SXSSFWorkbook(rowAccessWindowSize); workbook = new CSXSSFWorkbook(rowAccessWindowSize); -
getCellValue的替换,假设封装了如下方法:
private static Object getCellValue(Sheet sheet, int rowIndex, int cellIndex) { if (sheet == null) { return null; } if (sheet instanceof CSXSSFSheet) { //CSXSSFSheet getValue的方式 return ((CSXSSFSheet) sheet).getCellValue(rowIndex, cellIndex); } else { Row row = sheet.getRow(rowIndex); if (row == null) { return null; } Cell cell = row.getCell(cellIndex); if(cell == null){ return null; } CellType cellType = cell.getCellTypeEnum(); Object cellValue = null; if (CellType.STRING.equals(cellType)) { cellValue = cell.getStringCellValue(); } else if (CellType.NUMERIC.equals(cellType)) { cellValue = String.valueOf(cell.getNumericCellValue()); } else if (CellType.FORMULA.equals(cellType)) { cellValue = cell.getCellFormula(); } else if (CellType.BOOLEAN.equals(cellType)) { cellValue = String.valueOf(cell.getBooleanCellValue()); } else if (CellType._NONE.equals(cellType)) { cellValue = null; } else if (CellType.BLANK.equals(cellType)) { cellValue = ""; } else if (CellType.ERROR.equals(cellType)) { cellValue = String.valueOf(cell.getErrorCellValue()); } else { cellValue = null; } return cellValue; } } -
同时为了降低内存占用率,可在适当时机 使用 CSXSSFSheet的 clearSheetData 方法清除缓存中的单元格数据