首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

使用 POI SAX 處理 Excel 文件,防止記憶體溢出

     編輯:Java 綜合教學

前言:POI 兩種處理模式的差異

Apache POI 提供了兩種處理 Excel 的模式:用戶模式和事件模式。用戶模式封裝良好,兼容 Excel 2003 與 2007+ 格式,使用方便,但會將整個文件載入記憶體,資料量超過 6 萬行時極易發生 OutOfMemoryError。為了解決大數據量讀取問題,POI 3.8 引入了基於 SAX 的流式讀取方式,對應 XLSX 格式的 XSSFSheetXMLHandler,可逐行解析 XML,大幅降低記憶體佔用。

核心介面:SheetContentsHandler

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):頁首頁尾資訊(較少使用)

自定義處理器:SheetSaxHandler 實作

參考 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 到資料庫批次寫入

完整讀取流程包含:開啟 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();
}

總結與避坑指南

  • 記憶體優勢:SAX 模式不載入整份文件,適合百萬行級匯入。
  • 欄位對齊:Excel 中合併儲存格或尾部空值會導致列數不足,需在 endRow 補齊。
  • 型別轉換:DataFormatter 可統一輸出格式化字串,若需保留數值型別,可搭配 XSSFCell 的 getNumericCellValue 進階處理。
  • 錯誤處理:解析異常時務必關閉 OPCPackage、資料庫連線與 PreparedStatement,避免資源洩漏。

透過 SAX 事件驅動模式,結合批次寫入與流式處理,即可穩定高效地完成大型 Excel 匯入任務。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved