產品經理須要導出一個頁面的全部的信息到 EXCEL 文件。html
對於 excel 導出,是一個很常見的需求。前端
最多見的解決方案就是使用 poi 直接同步導出一個 excel 文件。java
若是導出的文件比較大,好比幾十萬條數據,同步導出頁面就會卡主,用戶沒法進行其餘操做。git
導出的時候,任務比較耗時就會阻塞主線程。github
若是導出的服務是暴露給外部(先後端分離),這種大量的數據傳輸十分消耗性能。數據庫
使用異常處理導出請求,後臺 MQ 通知本身進行處理。apache
MQ 消費以後,多線程處理 excel 文件導出,生成文件後上傳到 FTP 等文件服務器。編程
前端直接查詢而且展示對應的任務執行列表,去 FTP 等文件服務器下載文件便可。後端
正常的 poi 在處理比較大的 excel 的時候,會出現內存溢出。服務器
網上的解決方案也比較多。
好比官方的 SXSSF (Since POI 3.8 beta3) 解決方式。
或者使用封裝好的包
原理都是強制使用 xssf 版本的Excel。
你也可使用 easyexcel,固然這個註釋文檔有些欠缺,並且設計的比較複雜,不是很推薦。
我這裏使用的是 hutool BigExcelWriter,
懶得本身再寫一遍。
若是一次查詢 100W 條數據庫,而後把這些信息所有加載到內存中,是不可取的。
建議有2個:
限制每一次分頁的數量。好比一次最多查詢 1w 條。分紅 100 次查詢。(必須)
雖然使用者提出要導出相似於 3 個月的全部信息,可是數量太多,毫無心義。(提出者本身可能體會不到)
儘可能避免 FULL-GC 的狀況發生,由於目前的全部方式對於 excel 的輸出流都會佔用內存,100W 條很容易致使 FULL-GC。
去數據庫讀取的時候必定要記得分頁,省得給數據庫太大的壓力。
一次讀取太多,也會致使內存直線上升。
好比 100W 條數據,則分紅 100 次去數據庫讀取。
傳統的 excel 導出,都是前端一個請求,直接 HTTP 同步返回。導出 100W 條,就在那裏傻等。
這客戶體驗不友好,並且網絡傳輸,系統佔用多種問題。
建議使用異步處理的方式,將文件上傳到文件服務器。前端直接去文件服務器讀取。
對於上面提到的工具,好比 Hutool,在表頭的處理方面無法很方便的統一。
你能夠本身定義相似於 easypoi/easyexcel 中的註解,本身反射解析。
而後統一處理表頭便可。
OO 的方式操做 excel,編程更加方便優雅。
sax 模式讀取,SXSS 模式寫入。避免 excel 大文件 OOM。
基於註解,編程更加靈活。
寫入能夠基於對象列表,也能夠基於 Map,實際使用更加方便。
讀取跳過空白行
實際工做和學習中,apache poi 操做 excel 過於複雜。
近期也看了一些其餘的工具框架:
easypoi
easyexcel
都或多或少難以知足本身的實際須要,因而就本身寫了一個操做 excel 導出的工具。
使用 maven 管理。
<dependency> <groupId>com.github.houbb</groupId> <artifactId>iexcel</artifactId> <version>0.0.2</version> </dependency>
你能夠直接參考 ExcelUtilTest.java
定義一個須要寫入/讀取的 excel 對象。
只有聲明瞭 @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 有幾個實現類,你能夠直接 new 或者藉助 ExcelUtil
類去建立。
IExcelWriter 實現類 | ExcelUtil 如何建立 | 說明 |
---|---|---|
HSSFExcelWriter | ExcelUtil.get03ExcelWriter() | 2003 版本的 excel |
XSSFExcelWriter | ExcelUtil.get07ExcelWriter() | 2007 版本的 excel |
SXSSFExcelWriter | ExcelUtil.getBigExcelWriter() | 大文件 excel,避免 OOM |
一個將對象列表寫入 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); } }
/** * 構建測試的對象列表 * @return 對象列表 */ private List<ExcelFieldModel> buildModelList() { List<ExcelFieldModel> models = new ArrayList<>(); ExcelFieldModel model = new ExcelFieldModel(); model.setName("測試1號"); model.setAge("25"); model.setEmail("123@gmail.com"); model.setAddress("貝克街23號"); ExcelFieldModel modelTwo = new ExcelFieldModel(); modelTwo.setName("測試2號"); modelTwo.setAge("30"); modelTwo.setEmail("125@gmail.com"); modelTwo.setAddress("貝克街26號"); models.add(model); models.add(modelTwo); return models; }
有時候列表只寫入一次很常見,全部就簡單的封裝了下:
/** * 只寫入一次列表 * 實際上是對原來方法的簡單封裝 */ @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 有幾個實現類,你能夠直接 new 或者藉助 ExcelUtil
類去建立。
IExcelReader 實現類 | ExcelUtil 如何建立 | 說明 |
---|---|---|
ExcelReader | ExcelUtil.getExcelReader() | 小文件的 excel 讀取實現 |
Sax03ExcelReader | ExcelUtil.getBigExcelReader() | 大文件的 2003 excel 讀取實現 |
Sax07ExcelReader | ExcelUtil.getBigExcelReader() | 大文件的 2007 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
的屬性說明以下:
屬性 | 類型 | 默認值 | 說明 |
---|---|---|---|
mapKey | String | "" |
僅用於生成的入參爲 map 時,會將 map.key 對應的值映射到 bean 上。若是不傳:默認使用當前字段名稱 |
headName | String | "" |
excel 表頭字段名稱,若是不傳:默認使用當前字段名稱 |
writeRequire | boolean | true | excel 文件是否須要寫入此字段 |
readRequire | boolean | true | excel 文件是否讀取此字段 |
/** * 寫出數據,本方法只是將數據寫入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);
建立 IExcelWriter 的時候,能夠指定 sheet 的下標或者名稱。來指定寫入的 sheet。
建立 IExcelWriter 的後,能夠調用 excelWriter.containsHead(bool)
指定是否生成 excel 表頭。
/** * 讀取當前 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);
建立 IExcelReader 的時候,能夠指定 sheet 的下標或者名稱。來指定讀取的 sheet。
注意:大文件 sax 讀取模式,只支持指定 sheet 的下標。
建立 IExcelReader 的後,能夠調用 excelReader.containsHead(bool)
指定是否讀取 excel 表頭。