🔓为什么POI的SXSSFWorkbook占用内存更小?
🏆POI的SXSSFWorkbook
SXSSFWorkbook类是Apache POI库的一部分,它是一个流行的Java库,用于读写Microsoft Office文件。
SXSSFWorkbook类代表XSSFWorkbook类的流版本,用于创建和操作Excel(.xlsx)文件。
通过使用SXSSFWorkbook类,您可以处理大型Excel文件而不会遇到OutOfMemoryError,因为它将数据写入临时文件而不是全部保存在内存中。这使得处理大型数据集时非常高效。
以下是使用SXSSFWorkbook创建Excel文件的示例:
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFCell;
import java.io.FileOutputStream;
public class ExcelWriter {
public static void main(String[] args) {
try (SXSSFWorkbook workbook = new SXSSFWorkbook(); // 创建一个SXSSFWorkbook对象
FileOutputStream outputStream = new FileOutputStream("output.xlsx")) { // 创建一个文件输出流
XSSFSheet sheet = workbook.createSheet("Sheet1"); // 创建一个名为"Sheet1"的工作表
// 创建行和单元格
for (int rowNum = 0; rowNum < 10; rowNum++) {
XSSFRow row = sheet.createRow(rowNum); // 创建一行
for (int cellNum = 0; cellNum < 5; cellNum++) {
XSSFCell cell = row.createCell(cellNum); // 创建一个单元格
cell.setCellValue("Row " + rowNum + ", Cell " + cellNum); // 设置单元格的值
}
}
workbook.write(outputStream); // 将工作簿写入文件
} catch (Exception e) {
e.printStackTrace();
}
}
}
在这个示例中,我们创建了一个新的SXSSFWorkbook,然后在其中创建了一个sheet,并将sheet填充了行和单元格。最后,我们使用FileOutputStream将工作簿写入输出文件中。
🏆POI的SXSSFWorkbook占用内存
SXSSFWorkbook 类是为了处理大型 Excel 文件而设计的。它的实现原理是通过将部分数据写入磁盘上的临时文件来减少内存占用。
在SXSSFWorkbook类中,有一个类叫做sheetDataWriter,这个类的作用就是将部分数据写入磁盘上的临时文件的。
public class SXSSFWorkbook implements Workbook {
protected SheetDatalriter createSheetDatawriter() throws IOException {
if( compressTmpFiles) {
return new GZIPSheetDatawriter( sharedStringSource);
}
return new SheetDatawriter( sharedStringSource);
}
}
写入过程是在 SheetDataWriter 的 writeRow 方法中实现的。此方法会被 SXSSFSheet 调用,以将行数据转换成XML 并写入临时文件。
public void writeRow(int rownum,SXSSFRow row) throws IOException {
if ( numberOfFlushedRows == 0)
_lowestIndex0fFlushedRows = rownum;
_numberLastFlushedRow = Math.max(rownum, numberLastFlushedRow);
_numberOfCellsOfLastFlushedRow = row.getLastCellNum();
_numberOfFlushedRows++;
beginRow(rownum, row);
Iterator<Cel1> cells = row.allCellsIterator();
int columnIndex = 0;
while (cells.hasNext()) {
writeCell(columnIndex++, cells.next());
}
endRow();
}
writeRow()方法会循环调用writeCell()方法:
public void writeCell(int columnIndex,Cell cell) throws IOException {
if (cell == null) {
return;
}
String ref = new CellReference( rownum, columnIndex).formatAsString();
_out.write("<c");
writeAttribute("r", ref);
Cellstyle cellstyle = cell.getCellstyle();
if (cellstyle.getIndex() != ) {
// need to convert the short to unsigned short as the indexes can be up to 64k
// ideally we would use int for this index, but that would need changes to some more
//APIs
writeAttribute("s",Integer.toString(cellStyle.getIndex() & 0xffff));
}
CellType cellType = cel1.getCellType();
switch (cellType) {
case BLANK: {
_out.write('>');
break;
}
case FORMULA: {
switch(cell.getCachedFormulaResultType()) {
case NUMERIC:
writeAttribute("t","n");
break;
case STRING:
writeAttribute("t",STCellType.STR.toString());
break;
case BOOLEAN:
writeAttribute("t","b");
break;
case ERROR:
writeAttribute("t","e");
break;
}
_out.write("><f>");
outputQuotedString(cell.getCellFormula());
_out.write("</f>"):
switch (cell.getCachedFormulaResultType()) {
case NUMERIC:
double nval = cell.getNumericCellValue();
if (!Double.isNaN(nval)) {
_out.write("<v>");
_out.write(Double.tostring(nval));
_out.write("</v>");
}
break;
case STRING:
String value = cell.getstringCellValue();
if(value != null && !value.isEmpty()) {
_out.write("<v>");
_out.write(value);
_out.write("</v>");
}
break;
case BOOLEAN:
_out.write("><v>");
_out.write(cell.getBooleanCellValue() ?“1” :"0");
_out.write("</v>");
break;
case ERROR: {
FormulaError error = FormulaError.forInt(cell.getErrorCellValue());
_out.write("><v>");
_out.write(error.getString());
_out.write("</v>");
break;
}
}
break;
}
case STRING: {
if ( sharedStringSource != null) {
XSSFRichTextString rt = new XSSFRichTextString(cell.getStringCellValue());
int sRef = sharedStringSource.addSharedStringItem(rt);
writeAttribute("t",STCellType.s.toString());
_out.write("><v>");
_out.write(String.value0f(sRef));
_out.write("</v>");
} else {
writeAttribute("t","inlineStr");
_out.write("><is><t");
if (hasLeadingTrailingSpaces(cell.getStringCellValue())) {
writeAttribute("xml:space","preserve");
}
out .write(">");
outputQuotedstring(cell.getstringCellValue());
_out.write("</t></is>");
}
break;
}
case NUMERIC: {
writeAttribute("t","n");
_out.write("><v>");
_out.write(Double.toString(cell.getNumericCellValue()));
_out .write("</v>”) ;
break;
}
case BOOLEAN: {
writeAttribute("t","b");
_out .write("><v>”) ;
_out.write(cell.getBooleanCellValue() ?"1” :"0");
out.write("</v>");
break;
}
case ERROR: {
FormulaError error = FormulaError.forInt(cell.getErrorCellValue());
writeAttribute("t","e");
_out .write("><v>”);
_out.write(error.getstring());
_out.write("</v>");
break;
}
default: {
throw new IllegalStateException("Invalid cell type: " + cellType);
}
}
_out.write("</c>");
}
在这个方法中,数据会在 out.write(…) 调用时写入磁盘,这里的_out其实就是一个写入磁盘文件的Writer,他的write方法就会把内容写入到临时文件中。
我尝试着在 out初始化的地方,也就是:
public SheetDatawriter() throws IOException {
_fd = createTempFile();
_out = createWriter( fd);
}
中加了断点,就能在运行过程中找到这个临时文件,tail一下临时文件就会发现它不断地有文件写入。
感兴趣的也可以debug看一下这个临时文件的内容,其实它就是一个xml文件,然后写入的就是我们excel中的内容。
所以,在SXSSFWorkbook中,我们在写入文件时,并不是把所有内容都暂留在内存内,而是会把部分数据写入临时文件,来减少对内存的占用,内存中只保留当前的一部分数据,这样就可以避免内存溢出的问题了。
🏆扩展
配置行缓存限制
我们可以主动设置行缓存限制,超过这个限制的数据将被写入磁盘上的临时文件。在创建SXSSFWorkbook的时候,可以指定rowAccessWindowSize来实现。
/**
* Construct an empty workbook and specify the window for row access.
* <p>
* When a new node is created via (@link SXSSFSheet#createRow) and the total number
* of unflushed records would exceed the specified value, then the
* row with the lowest index value is flushed and cannot be accessed
* via f@link SXSSFSheet#getRow] anymore.
* </p>
* <p>
* A value of <code>-1</code> indicates unlimited access. In this case all
* records that have not been flushed by a call to <code>flush()</code> are available
* for random access.
* </p>
* <p>
* A value of <code>0</code> is not allowed because it would flush any newly created row
* without having a chance to specify any cells.
* </p>
* @param rowAccesslindowSize the number of rows that are kept in memory until flushed out , see above.
*/
public SXSSFWorkbook(int rowAccesswindowSize){
this(null /*workbook*/, rowAccessWindowSize):
}