開源工具 — Java8讓Excel的讀寫變得更加簡單高效


Apache POI

在業務開發中我們經常會遇到Excel的導入導出,而 Apache POI 是Java開發者常用的API。
https://poi.apache.org/components/spreadsheet/index.html

GridExcel

Universal solution for reading and writing simply Excel based on functional programming and POI EventModel

GridExcel是基於Java8函數式編程和POI EventModel實現的用於Excel簡單讀寫的通用解決方案。

  • 基於POI EventModel,在讀寫數據量非常大的Excel時,降低內存佔用避免OOM與頻繁FullGC
  • 基於函數編程,支持關聯對象等多種複雜情況的處理,學習成本低
  • 支持流式API,使代碼編寫和理解更簡單,更直觀

EventModel

什麼是EventModel?在POI FAQ(常見問題解答)【https://poi.apache.org/help/faq.html#faq-N100C2】官方給出解釋:

The SS eventmodel package is an API for reading Excel files without loading the whole spreadsheet into memory. It does require more knowledge on the part of the user, but reduces memory consumption by more than tenfold. It is based on the AWT event model in combination with SAX. If you need read-only access, this is the best way to do it.

SS eventmodel包是一個用於讀取Excel文件而不將整個電子表格加載到內存中的API。 它確實需要用戶掌握更多知識,但是將內存消耗減少了十倍以上。 它基於AWT(Abstract Window Toolkit)event model與SAX的結合。 如果您需要只讀訪問權限,這是最好的方式。

函數編程

說到函數編程,就不得不提Lambda表達式,如果對Java8中的Lambda不瞭解或理解不深刻,可以看下甲骨文官網給出的這篇文章,【https://www.oracle.com/technetwork/articles/java/architect-lambdas-part1-2080972.html】,個人認爲這是Java8 Lambda從入門到進階最好的文章之一。

其中函數編程的目的就是實現代碼塊傳遞,即,將方法作爲參數在方法間傳遞。爲此,隨着Java語言的發展,不斷出現一些解決方案:

  1. Java 1.0, 使用Abstract Window Toolkit (AWT) EventModel來實現,但笨拙且不可行
  2. Java 1.1,提出一系列“Listeners”
  3. 後來使用內部類匿名內部類來實現,但是大多數情況下,它們只是被用作事件處理。
  4. 再後來發現更多地方將代碼塊作爲對象(實際上是數據)不僅有用而且是必要的,但是Java中函數編程還是很笨拙,它需要成長。
  5. 直到Java 1.7,Java引入了java.lang.invoke包,提供一 種新的動態確定目標方法的機制(可以不用再單純依靠固化在虛擬機中的字節碼調用指令),稱爲MethodHandle,模擬字節碼的方法指針調用,類似於C/C++的Function Pointer(函數指針)並引入第5條方法調用的字節碼指令invokedynamic
  6. 直到Java 1.8,基於Java 1.7提出的字節碼指令invokedynamic,實現了Lamda技術,將函數作爲參數在方法間傳遞,Java開始更好的支持函數式編程。
  7. 用反射不是早就可以實現了嗎?Reflection API 重量級,性能低。

注意: 5、6、7參考《深入理解Java虛擬機》第2版,8.3.3 動態類型語言支持。


在POI的使用過程中,對大多數API User來說經常面臨兩個問題,這也是GridExcel致力解決的問題。

問題1. 僅使用簡單的導入導出功能,但每次業務的數據對象結構不同,需要重新編寫處理方法,很麻煩!

解決方法

將Excel處理邏輯抽取出來,封裝成工具類。

封裝條件

與大多數Java API一樣,POI把更多的精力放在高級功能的處理上,比如Formula(公式)、Conditional Formatting(條件格式)、Zoom(縮放)等。對於僅僅做數據導入導出功能的API User,很少使用這些高級特性,這允許API用戶對POI的使用進行簡單的封裝。

封裝方式

無論是讀是寫,我們都需要解決Excel中的Columns(列)與Java數據對象Fields(字段)的映射關係,將這種映射關係作爲參數(Map對象HashMap或LinkedHashMap),傳遞給工具類。

對於Columns不難理解,它可以是有序的數字或字母,也可以是其它字符串用來作爲首行,表示該列數據的含義。

對於Fields,它的處理需要兼容複雜情況,如下:

  • 查詢字段時出現異常
  • 字段或單元格的值爲null
  • 該列的值可能對應關聯對象、甚至是關聯集合中的某個字段值
  • 字段或單元格的值需要做特殊處理,例如value == true?完成:失敗;
反射

首先想到,也是大多數封裝者都在使用的方式是就是Reflection API,從上文 函數編程 章節我們瞭解到,反射重量級,會降低代碼的性能,同時對複雜情況的處理支持性不夠好。

反射+註解

這種方式可以更好的支持複雜情況,但是反射依然會降低性能,同時註解對數據對象會造成代碼侵入,而且對該工具類封裝者的其他使用者無疑會增加學習成本。

匿名內部類

這種方式也可以很好的支持複雜情況,但是使用匿名內部類的語法顯然患有“垂直問題”(這意味着代碼需要太多的線條來表達基本概念),太過冗雜。至於性能,應該也不如直接傳遞函數來的快吧。

函數接口(Lambda)

這種方式是基於第5條方法調用的字節碼指令invokeDynamic實現的,直接傳遞函數代碼塊,很好的支持複雜情況,性能較高,代碼編寫更簡單結構更加簡潔,而且對數據對象代碼零侵入。

問題2. Excel導入或導出數據量比較大,造成內存溢出頻繁的Full GC,該如何解決?

解決方法

  • 讀Excel —— eventmodel
  • 寫Excel —— streaming.SXSSFWorkbook

原理

POI的使用對我們來說很常見,對下面兩個概念應該並不陌生:

  • HSSFWorkbook(處理97(-2007) 的.xls)
  • XSSFWorkbook(處理2007 OOXML (.xlsx) )

但是對於eventmodelstreaming.SXSSFWorkbook就很少接觸了,它們是POI提供的專門用來解決內存佔用問題的low level API(低級API),使用它們可以讀寫數據量非常大的Excel,同時可以避免內存溢出頻繁的Full GC。【https://poi.apache.org/components/spreadsheet/how-to.html】

  • eventmodel,用來讀Excel,並沒有將Excel整個加載到內存中,而是允許用戶從InputStream每讀取一些信息,就交給回調函數監聽器,至於丟棄,存儲還是怎麼處理這些內容,都交由用戶。
  • streaming.SXSSFWorkbook,用來寫Excel(是對XSSFWorkbook的封裝,僅支持.xlsx),通過滑動窗口來實現,只在內存中保留滑動窗口允許存在的行數,超出的行Rows被寫出到臨時文件,當調用write(OutputStream stream)方法寫出內容時,再直接從臨時內存寫出到目標OutputStreamSXSSFWorkbook的使用會產生一些侷限性。
    • Only a limited number of rows are accessible at a point in time.
    • Sheet.clone() is not supported.
    • Formula evaluation is not supported

解決途徑

  • https://github.com/liuhuagui/gridexcel
    基於Java函數編程(Lambda),支持流式API,使用環境Java1.8或更高,學習成本:Lambda

實際上POI官網已經給了用戶使用示例,而上述工具只是做了自己的封裝實現,讓使用更方便。


快速使用

<dependency>
    <groupId>com.github.liuhuagui</groupId>
    <artifactId>gridexcel</artifactId>
    <version>2.2</version>
</dependency>

GridExcel.java

GridExcel.java提供了多種靜態方法,可以直接使用,具體式例可參考測試代碼(提供了測試數據和測試文件):

  • https://github.com/liuhuagui/gridexcel/blob/master/src/test/java/ReadTest.java
  • https://github.com/liuhuagui/gridexcel/blob/master/src/test/java/WriteTest.java

流式API

/**
  * 業務邏輯處理方式三選一:
  * 1.啓用windowListener,並將業務邏輯放在該函數中。
  * 2.不啓用windowListener,使用get()方法取回全部數據集合,做後續處理。
  * 3.readFunction函數,直接放在函數中處理 或 使用final or effective final的局部變量存放這寫數據,做後續處理。
  * 注意:使用EventModel時readFunction函數的輸入爲每行的cell值集合List<String>。
  * @throws Exception
  */
 @Test
 public void readXlsxByEventModel() throws Exception {
     InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("2007.xlsx");
     GridExcel.readByEventModel(resourceAsStream,TradeOrder.class,ExcelType.XLSX)
             .window(2,ts -> System.out.println(JSON.toJSONString(ts)))//推薦在這裏執行自己的業務邏輯
             .process(cs ->{
                 TradeOrder tradeOrder = new TradeOrder();
                 tradeOrder.setTradeOrderId(Long.valueOf(cs.get(0)));
                 Consultant consultant = new  Consultant();
                 consultant.setConsultantName(cs.get(3));
                 tradeOrder.setConsultant(consultant);
                 tradeOrder.setPaymentRatio(cs.get(16));
                 return tradeOrder;
             },1);
 }
 /**
  * 使用Streaming UserModel寫出數據到Excel
  * @throws Exception
  */
 @Test
 public void writeExcelByStreaming() throws Exception {
     GridExcel.writeByStreaming(TradeOrder.class)
             .head(writeFunctionMap())//對象字段到Excel列的映射
             .createSheet()
             .process(MockData.data())//模擬數據。在這裏設置業務數據集合。
             .write(FileUtils.openOutputStream(new File("/excel/test.xlsx")));
 }

ReadExcel

ReadExcelByUserModel

Use user model to read excel file. userModel ——

  • 缺點:內存消耗大,會將excel信息全部加載到內存再進行處理。
  • 優點:現成的API,使用和理解更簡單。
  • 使用場景:可以處理數據量較小的Excel。
ReadExcelByEventModel

Use event model to read excel file. eventModel ——

  • 缺點:沒有現成的API,使用和理解較爲複雜,適合中高級程序員(GridExcel的目標之一就是讓EventModel的使用變得簡單)
  • 優點:非常小的內存佔用,並沒有在一開始就將所有內容加載到內存中,而是把主體內容的處理(存儲,使用,丟棄)都交給了用戶,用戶可以自定義監聽函數來處理這些內容。
  • 使用場景:可以處理較大數據量的Excel,避免OOM和頻繁FullGC

WriteExcel

WriteExcelByUserModel

Use user model to write excel file. userModel ——

  • 缺點:會將產生的spreadsheets對象整個保存在內存中,所以write Excel的大小受到堆內存(Heap space)大小限制。
  • 優點:使用和理解更簡單。
  • 使用場景:可以寫出數據量較小的Excel。
WriteExcelByStreaming

Use API-compatible streaming extension of XSSF to write very large excel file. streaming userModel——

  • 缺點
    • 僅支持XSSF;
    • Sheet.clone() is not supported;
    • Formula evaluation is not supported;
    • Only a limited number of rows are accessible at a point in time.
  • 優點:通過滑動窗口來實現,內存中只保留指定size of rows的內容,超出部分被寫出到臨時文件,write Excel的大小不再受到堆內存(Heap space)大小限制。
  • 使用場景:可以寫出非常大的Excel。

Issues

在使用工具過程中出現問題,有功能添加或改動需求的可以向作者提Issue:https://github.com/liuhuagui/gridexcel/issues

  • 比如說,想要增加對首行以外的行列做樣式擴展

如有疑問可以聯繫作者:[email protected]

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