目錄
麻煩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?完成:失敗;
反射
首先想到,也是大多數封裝者都在使用的方式是就是
反射+註解
這種方式可以更好的支持複雜情況,但是反射依然會降低性能,同時註解對數據對象會造成代碼侵入,而且對該工具類封裝者的其他使用者無疑會增加學習成本。
匿名內部類—— 作爲監聽函數
這種方式也可以很好的支持複雜情況,但是使用匿名內部類的語法顯然患有“垂直問題”(這意味着代碼需要太多的線條來表達基本概念),太過冗雜。至於性能,應該也不如直接傳遞函數來的快吧。
函數接口(Lambda)—— 作爲監聽函數
這種方式是基於第5條方法調用的字節碼指令
麻煩2
Excel一次性讀寫數據量比較大,造成內存溢出
或頻繁的Full GC
,該如何解決?
解決方法
- 讀Excel —— eventmodel
- 寫Excel —— streaming.SXSSFWorkbook
原理
POI的使用對我們來說很常見,對下面兩個概念應該並不陌生:
- HSSFWorkbook(處理97(-2007) 的.xls)
- XSSFWorkbook(處理2007 OOXML (.xlsx) )
但是對於 內存溢出
或頻繁的Full GC
。【https://poi.apache.org/components/spreadsheet/how-to.html】
eventmodel ,用來讀Excel,並沒有將Excel整個加載到內存中,而是允許用戶從InputStream 每讀取一些信息,就交給 回調函數 或 監聽器 ,至於丟棄,存儲還是怎麼處理這些內容,都交由用戶。streaming .SXSSFWorkbook ,用來寫Excel(是對XSSFWorkbook的封裝,僅支持.xlsx),通過 滑動窗口 來實現,只在內存中保留滑動窗口允許存在的行數,超出的行Rows被寫出到臨時文件,當調用write(OutputStream stream)
方法寫出內容時,再直接從臨時內存寫出到目標OutputStream 。SXSSFWorkbook 的使用會產生一些侷限性。- Only a limited number of rows are accessible at a point in time.
- Sheet.clone() is not supported.
- Formula evaluation is not supported
開源解決方案
Universal solution for reading and writing simply Excel based onfunctional programming and POI EventModel
GridExcel是基於Java8函數式編程和POI EventModel實現的用於Excel簡單讀寫的通用解決方案。致力於解決上述兩個問題。(注意,GridExcel並沒有改變POI,僅僅是對它的合理封裝。)
- 基於POI EventModel,在讀寫數據量非常大的Excel時,降低內存佔用避免OOM與頻繁FullGC
- 基於函數編程,支持關聯對象等多種複雜情況的處理,學習成本低
- 支持流式API,使代碼編寫和理解更簡單,更直觀
- 支持使用
阻塞窗口
+監聽函數
的方式去處理從Excel中讀取的數據
概念基礎
Apache POI
在業務開發中我們經常會遇到Excel的導入導出,而
【https://poi.apache.org/components/spreadsheet/index.html】
EventModel
什麼是
The SSeventmodel package is anAPI for reading Excel files without loading the whole spreadsheet into memory . Itdoes require more knowledge onthe part ofthe user ,but reduces memory consumption bymore than tenfold . It isbased onthe AWT event model incombination with SAX . Ifyou need read -only access ,this isthe 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語言的發展,不斷出現一些解決方案:
- Java 1.0, 使用Abstract Window Toolkit (AWT) EventModel來實現,但笨拙且不可行
- Java 1.1,提出一系列“Listeners”
- 後來使用
內部類
和匿名內部類
來實現,但是大多數情況下,它們只是被用作事件處理。 - 再後來發現更多地方將代
碼塊作爲對象
(實際上是數據)不僅有用而且是必要的,但是Java中函數編程還是很笨拙,它需要成長。 - 直到Java 1.7,Java引入了java.lang.invoke包,提供一 種新的動態確定目標方法的機制(可以不用再單純依靠固化在虛擬機中的字節碼調用指令),稱爲MethodHandle,模擬字節碼的方法指針調用,類似於C/C++的
Function Pointer (函數指針)並引入第5條方法調用的字節碼指令invokedynamic 。 - 直到Java 1.8,基於Java 1.7提出的字節碼指令
invokedynamic ,實現了Lamda技術,將函數作爲參數在方法間傳遞,Java開始更好的支持函數式編程。 - 用反射不是早就可以實現了嗎?Reflection API 重量級,性能低。
注意: 5、6、7 參考《深入理解Java虛擬機》第2版,8.3.3 動態類型語言支持。
快速使用
<dependency >
<groupId >com.github.liuhuagui</groupId >
<artifactId >gridexcel</artifactId >
<version >2.3</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")));
}
無實體類讀寫Excel
由於沒有自定義業務實體類,這裏我們可以使用Map.class來代替。下面是讀入Excel的例子,寫Excel可以參照實現。
/**
* No Entity無實體類讀Excel文件
* 業務邏輯處理方式三選一:
* 1.啓用windowListener,並將業務邏輯放在該函數中。
* 2.不啓用windowListener,使用get()方法取回全部數據集合,做後續處理。
* 3.readFunction函數,直接放在函數中處理 或 使用final or effective final的局部變量存放這寫數據,做後續處理。
* 注意:使用EventModel時readFunction函數的輸入爲每行的cell值集合List<String>。
* @throws Exception
*/
@Test
public void readXlsxByEventModelWithoutEntity () throws Exception {
InputStream resourceAsStream = Thread.currentThread ().getContextClassLoader ().getResourceAsStream ("2007.xlsx");
GridExcel.readByEventModel (resourceAsStream,Map.class ,ExcelType.XLSX)
.window (2,ts -> System.out.println (JSON.toJSONString (ts)))//推薦在這裏執行自己的業務邏輯
.process (cs ->{
Map<String , Object > map = new HashMap <String , Object >();
map.put("tradeOrderId",cs.get(0));
map.put("consultantName",cs.get(3));
map.put("paymentRatio",cs.get(16));
return map;
},1);
}