Apache POI 提供了兩種處理 Excel 的模式:用戶模式和事件模式。用戶模式封裝良好,兼容 Excel 2003 與 2007+ 格式,使用方便,但會將整個文件載入記憶體,資料量超過 6 萬行時極易發生 OutOfMemoryError。為了解決大數據量讀取問題,POI 3.8 引入了基於 SAX 的流式讀取方式,對應 XLSX 格式的 XSSFSheetXMLHandler,可逐行解析 XML,大幅降低記憶體佔用。
POI 已將 SAX 解析 Sheet XML 的細節封裝,開發者只需實作 SheetContentsHandler 介面,覆寫三個關鍵方法:
startRow(int rowNum):開始讀取某行endRow(int rowNum):結束讀取某行cell(String cellReference, String formattedValue, XSSFComment comment):讀取單元格資料headerFooter(String text, boolean isHeader, String tagName):頁首頁尾資訊(較少使用)參考 POI 範例 XLSX2CSV.java,我們實作一個支援「列數補齊」與「行事件回呼」的處理器:
protected class SheetSaxHandler implements SheetContentsHandler {
private int currentRow = -1;
private int currentCol = -1;
private int minColumns;
private final List<SheetRowListener> listeners = new ArrayList<>();
private final List<String> rowData = new ArrayList<>();
public SheetSaxHandler(int minColumns) { this.minColumns = minColumns; }
public void setMinColumns(int minColumns) { this.minColumns = minColumns; }
public void addRowListener(SheetRowListener listener) { listeners.add(listener); }
private void fireRowAdded(List<String> row, int rowNum) throws SQLException {
for (SheetRowListener l : listeners) l.onRow(row, rowNum);
}
@Override public void startRow(int rowNum) {
currentRow = rowNum; currentCol = -1; rowData.clear();
}
@Override public void endRow(int rowNum) {
// 補齊空白欄位
for (int i = currentCol; i < minColumns; i++) rowData.add("");
try { fireRowAdded(rowData, rowNum); } catch (SQLException e) { e.printStackTrace(); }
}
@Override public void cell(String cellReference, String formattedValue, XSSFComment comment) {
if (cellReference == null) cellReference = new CellAddress(currentRow, currentCol).formatAsString();
int thisCol = new CellReference(cellReference).getCol();
int missed = thisCol - currentCol - 1;
for (int i = 0; i < missed; i++) rowData.add("");
currentCol = thisCol;
// 簡易型別處理:嘗試解析為數字
try { Double.parseDouble(formattedValue); rowData.add(formattedValue); }
catch (NumberFormatException e) { rowData.add(formattedValue); }
}
@Override public void headerFooter(String text, boolean isHeader, String tagName) { /* ignore */ }
}
interface SheetRowListener { void onRow(List<String> row, int rowNum); }
完整讀取流程包含:開啟 OPCPackage、建立 ReadOnlySharedStringsTable 與 StylesTable、迭代各 Sheet 的 InputStream、配置 XMLReader 與 XSSFSheetXMLHandler、最後啟動 SAX 解析。解析過程中透過 SheetRowListener 將每行資料交給業務邏輯(如批次寫入 Oracle),並控制批次大小避免記憶體堆積。
public int importExcel(String filePath, String pcId) throws ... {
File file = new File(filePath);
try (OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ);
Connection conn = dataSource.getConnection()) {
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
XSSFReader reader = new XSSFReader(pkg);
StylesTable styles = reader.getStylesTable();
XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
// 依設定對應 Sheet 索引
Map<Integer, JSONObject> sheetCfgMap = buildSheetConfigMap();
int sheetIndex = 1;
while (sheets.hasNext()) {
InputStream sheetStream = sheets.next();
JSONObject cfg = sheetCfgMap.get(sheetIndex++);
if (cfg != null) {
processSheet(styles, strings, sheetStream, cfg, conn, pcId);
}
}
}
return 0;
}
private void processSheet(StylesTable styles, ReadOnlySharedStringsTable strings,
InputStream sheetStream, JSONObject cfg, Connection conn, String pcId)
throws IOException, ParserConfigurationException, SAXException, SQLException {
SheetSaxHandler handler = new SheetSaxHandler(cfg.getJSONArray("fields").size());
PreparedStatement ps = conn.prepareStatement(buildInsertSql(cfg, pcId));
int dataStart = cfg.getIntValue("dataStartRow");
handler.addRowListener((row, rowNum) -> {
if (rowNum < dataStart - 1) return;
setParameters(ps, cfg, row, rowNum - dataStart);
if (rowNum % batchSize == 0) { ps.executeBatch(); ps.clearBatch(); }
});
XMLReader parser = SAXHelper.newXMLReader();
ContentHandler contentHandler = new XSSFSheetXMLHandler(
styles, null, strings, handler, new DataFormatter(), false);
parser.setContentHandler(contentHandler);
parser.parse(new InputSource(sheetStream));
ps.executeBatch(); ps.clearBatch(); ps.close();
}
endRow 補齊。DataFormatter 可統一輸出格式化字串,若需保留數值型別,可搭配 XSSFCell 的 getNumericCellValue 進階處理。OPCPackage、資料庫連線與 PreparedStatement,避免資源洩漏。透過 SAX 事件驅動模式,結合批次寫入與流式處理,即可穩定高效地完成大型 Excel 匯入任務。