java 導出 excel 最佳實踐,java 大文件 excel 避免OOM(內存溢出) exce

產品需求

產品經理需要導出一個頁面的所有的信息到 EXCEL 文件。

需求分析

對於 excel 導出,是一個很常見的需求。

最常見的解決方案就是使用 poi 直接同步導出一個 excel 文件。

客戶體驗 & 服務性能

  • 客戶體驗

如果導出的文件比較大,比如幾十萬條數據,同步導出頁面就會卡主,用戶無法進行其他操作。

  • 服務性能

導出的時候,任務比較耗時就會阻塞主線程。

如果導出的服務是暴露給外部(前後端分離),這種大量的數據傳輸十分消耗性能。

解決方案

使用異常處理導出請求,後臺 MQ 通知自己進行處理。

MQ 消費之後,多線程處理 excel 文件導出,生成文件後上傳到 FTP 等文件服務器。

前端直接查詢並且展現對應的任務執行列表,去 FTP 等文件服務器下載文件即可。

EXCEL 導出需要考慮的問題

OOM

正常的 poi 在處理比較大的 excel 的時候,會出現內存溢出。

網上的解決方案也比較多。

比如官方的 SXSSF (Since POI 3.8 beta3) 解決方式。

或者使用封裝好的包

  1. easypoi ExcelBatchExportServer

  2. hutool BigExcelWriter

原理都是強制使用 xssf 版本的Excel

你也可以使用 easyexcel,當然這個註釋文檔有些欠缺,而且設計的比較複雜,不是很推薦。

我這裏使用的是 hutool BigExcelWriter
懶得自己再寫一遍。

FULL GC

如果一次查詢 100W 條數據庫,然後把這些信息全部加載到內存中,是不可取的。

建議有2個:

  1. 限制每一次分頁的數量。比如一次最多查詢 1w 條。分成 100 次查詢。(必須)

  2. 限制查詢得總條數。比如限制爲最多 10W 條。(根據實際情況選擇)

雖然使用者提出要導出類似於 3 個月的所有信息,但是數量太多,毫無意義。(提出者自己可能體會不到)

儘量避免 FULL-GC 的情況發生,因爲目前的所有方式對於 excel 的輸出流都會佔用內存,100W 條很容易導致 FULL-GC。

數據庫的壓力

去數據庫讀取的時候一定要記得分頁,免得給數據庫太大的壓力。

一次讀取太多,也會導致內存直線上升。

比如 100W 條數據,則分成 100 次去數據庫讀取。

網絡傳輸

傳統的 excel 導出,都是前端一個請求,直接 HTTP 同步返回。導出 100W 條,就在那裏傻等。

這客戶體驗不友好,而且網絡傳輸,系統佔用多種問題。

建議使用異步處理的方式,將文件上傳到文件服務器。前端直接去文件服務器讀取。

編程的便利性

對於上面提到的工具,比如 Hutool,在表頭的處理方面沒法很方便的統一。

你可以自己定義類似於 easypoi/easyexcel 中的註解,自己反射解析。

然後統一處理表頭即可。

IExcel 方便優雅的 excel 框架

特性

  • OO 的方式操作 excel,編程更加方便優雅。

  • sax 模式讀取,SXSS 模式寫入。避免 excel 大文件 OOM。

  • 基於註解,編程更加靈活。

  • 寫入可以基於對象列表,也可以基於 Map,實際使用更加方便。

  • 設計簡單,註釋完整。方便大家學習改造。

後期特性

  • 讀取跳過空白行

  • excel 樣式相關的註解開發

創作緣由

實際工作和學習中,apache poi 操作 excel 過於複雜。

近期也看了一些其他的工具框架:

  • easypoi

  • easyexcel

  • hutool-poi

都或多或少難以滿足自己的實際需要,於是就自己寫了一個操作 excel 導出的工具。

快速開始

引入 Jar

使用 maven 管理。

<dependency>
     <groupId>com.github.houbb</groupId>
     <artifactId>iexcel</artifactId>
     <version>0.0.2</version>
</dependency>

定義對象

你可以直接參考 ExcelUtilTest.java

定義一個需要寫入/讀取的 excel 對象。

  • ExcelFieldModel.java

只有聲明瞭 @ExcelField 的屬性纔會被處理,使用說明:@ExcelField

public class ExcelFieldModel {

    @ExcelField
    private String name;

    @ExcelField(headName = "年齡")
    private String age;

    @ExcelField(mapKey = "EMAIL", writeRequire = false, readRequire = false)
    private String email;

    @ExcelField(mapKey = "ADDRESS", headName = "地址", writeRequire = true)
    private String address;

    //getter and setter
}

寫入例子

IExcelWriter 的實現

IExcelWriter 有幾個實現類,你可以直接 new 或者藉助 ExcelUtil 類去創建。

IExcelWriter 實現類 ExcelUtil 如何創建 說明
HSSFExcelWriter ExcelUtil.get03ExcelWriter() 2003 版本的 excel
XSSFExcelWriter ExcelUtil.get07ExcelWriter() 2007 版本的 excel
SXSSFExcelWriter ExcelUtil.getBigExcelWriter() 大文件 excel,避免 OOM

IExcelWriter 接口說明

寫入到 2003

  • excelWriter03Test()

一個將對象列表寫入 2003 excel 文件的例子。

/**
 * 寫入到 03 excel 文件
 */
@Test
public void excelWriter03Test() {
    // 待生成的 excel 文件路徑
    final String filePath = "excelWriter03.xls";

    // 對象列表
    List<ExcelFieldModel> models = buildModelList();

    try(IExcelWriter excelWriter = ExcelUtil.get03ExcelWriter();
        OutputStream outputStream = new FileOutputStream(filePath)) {
        // 可根據實際需要,多次寫入列表
        excelWriter.write(models);

        // 將列表內容真正的輸出到 excel 文件
        excelWriter.flush(outputStream);
    } catch (IOException e) {
        throw new ExcelRuntimeException(e);
    }
}
  • buildModelList()
/**
 * 構建測試的對象列表
 * @return 對象列表
 */
private List<ExcelFieldModel> buildModelList() {
    List<ExcelFieldModel> models = new ArrayList<>();
    ExcelFieldModel model = new ExcelFieldModel();
    model.setName("測試1號");
    model.setAge("25");
    model.setEmail("[email protected]");
    model.setAddress("貝克街23號");

    ExcelFieldModel modelTwo = new ExcelFieldModel();
    modelTwo.setName("測試2號");
    modelTwo.setAge("30");
    modelTwo.setEmail("[email protected]");
    modelTwo.setAddress("貝克街26號");

    models.add(model);
    models.add(modelTwo);
    return models;
}

一次性寫入到 2007 excel

有時候列表只寫入一次很常見,所有就簡單的封裝了下:

/**
 * 只寫入一次列表
 * 其實是對原來方法的簡單封裝
 */
@Test
public void onceWriterAndFlush07Test() {
    // 待生成的 excel 文件路徑
    final String filePath = "onceWriterAndFlush07.xlsx";

    // 對象列表
    List<ExcelFieldModel> models = buildModelList();

    // 對應的 excel 寫入對象
    IExcelWriter excelWriter = ExcelUtil.get07ExcelWriter();

    // 只寫入一次列表
    ExcelUtil.onceWriteAndFlush(excelWriter, models, filePath);
}

讀取例子

excel 讀取時會根據文件名稱判斷是哪個版本的 excel。

IExcelReader 的實現

IExcelReader 有幾個實現類,你可以直接 new 或者藉助 ExcelUtil 類去創建。

IExcelReader 實現類 ExcelUtil 如何創建 說明
ExcelReader ExcelUtil.getExcelReader() 小文件的 excel 讀取實現
Sax03ExcelReader ExcelUtil.getBigExcelReader() 大文件的 2003 excel 讀取實現
Sax07ExcelReader ExcelUtil.getBigExcelReader() 大文件的 2007 excel 讀取實現

IExcelReader 接口說明

excel 讀取的例子

/**
 * 讀取測試
 */
@Test
public void readWriterTest() {
    File file = new File("excelWriter03.xls");
    IExcelReader<ExcelFieldModel> excelReader = ExcelUtil.getExcelReader(file);
    List<ExcelFieldModel> models = excelReader.readAll(ExcelFieldModel.class);
    System.out.println(models);
}

ExcelField 註解說明

@ExcelField 的屬性說明如下:

屬性 類型 默認值 說明
mapKey String "" 僅用於生成的入參爲 map 時,會將 map.key 對應的值映射到 bean 上。如果不傳:默認使用當前字段名稱
headName String "" excel 表頭字段名稱,如果不傳:默認使用當前字段名稱
writeRequire boolean true excel 文件是否需要寫入此字段
readRequire boolean true excel 文件是否讀取此字段

IExcelWriter 接口說明

/**
 * 寫出數據,本方法只是將數據寫入Workbook中的Sheet,並不寫出到文件<br>
 * <p>
 * data中元素支持的類型有:
 *  <pre>
 * 1. Bean,既元素爲一個Bean,第一個Bean的字段名列表會作爲首行,剩下的行爲Bean的字段值列表,data表示多行 <br>
 * </pre>
 * @param data 數據
 * @return this
 */
IExcelWriter write(Collection<?> data);

/**
 * 寫出數據,本方法只是將數據寫入Workbook中的Sheet,並不寫出到文件<br>
 *  將 map 按照 targetClass 轉換爲對象列表
 *  應用場景: 直接 mybatis mapper 查詢出的 map 結果,或者其他的構造結果。
 * @param mapList map 集合
 * @param targetClass 目標類型
 * @return this
 */
IExcelWriter write(Collection<Map<String, Object>> mapList, final Class<?> targetClass);

/**
 * 將Excel Workbook刷出到輸出流
 *
 * @param outputStream 輸出流
 * @return this
 */
IExcelWriter flush(OutputStream outputStream);

指定 sheet

創建 IExcelWriter 的時候,可以指定 sheet 的下標或者名稱。來指定寫入的 sheet。

是否包含表頭

創建 IExcelWriter 的後,可以調用 excelWriter.containsHead(bool) 指定是否生成 excel 表頭。

IExcelReader 接口說明

/**
 * 讀取當前 sheet 的所有信息
 * @param tClass 對應的 javabean 類型
 * @return 對象列表
 */
List<T> readAll(Class<T> tClass);

/**
 * 讀取指定範圍內的
 * @param tClass 泛型
 * @param startIndex 開始的行信息(從0開始)
 * @param endIndex 結束的行信息
 * @return 讀取的對象列表
 */
List<T> read(Class<T> tClass, final int startIndex, final int endIndex);

指定 sheet

創建 IExcelReader 的時候,可以指定 sheet 的下標或者名稱。來指定讀取的 sheet。

注意:大文件 sax 讀取模式,只支持指定 sheet 的下標。

是否包含表頭

創建 IExcelReader 的後,可以調用 excelReader.containsHead(bool) 指定是否讀取 excel 表頭。

拓展閱讀

excel 導出最佳實踐

iexcel 框架

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章