Apache POI
在業(yè)務開發(fā)中我們經(jīng)常會遇到Excel的導入導出,而 Apache POI 是Java開發(fā)者常用的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函數(shù)式編程和POI EventModel實現(xiàn)的用于Excel簡單讀寫的通用解決方案匈织。
- 基于POI EventModel,在讀寫數(shù)據(jù)量非常大的Excel時驮俗,降低內存占用避免OOM與頻繁FullGC
- 基于函數(shù)編程贾漏,支持關聯(lián)對象等多種復雜情況的處理,學習成本低
- 支持流式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的結合芽唇。 如果您需要只讀訪問權限顾画,這是最好的方式。
函數(shù)編程
說到函數(shù)編程匆笤,就不得不提Lambda表達式研侣,如果對Java8中的Lambda不了解或理解不深刻,可以看下甲骨文官網(wǎng)給出的這篇文章炮捧,【https://www.oracle.com/technetwork/articles/java/architect-lambdas-part1-2080972.html】庶诡,個人認為這是Java8 Lambda從入門到進階最好的文章之一。
其中函數(shù)編程的目的就是實現(xiàn)代碼塊傳遞
咆课,即末誓,將方法作為參數(shù)在方法間傳遞扯俱。為此,隨著Java語言的發(fā)展喇澡,不斷出現(xiàn)一些解決方案:
- Java 1.0迅栅, 使用Abstract Window Toolkit (AWT) EventModel來實現(xiàn),但笨拙且不可行
- Java 1.1晴玖,提出一系列“Listeners”
- 后來使用
內部類
和匿名內部類
來實現(xiàn)读存,但是大多數(shù)情況下,它們只是被用作事件處理呕屎。 - 再后來發(fā)現(xiàn)更多地方將代
碼塊作為對象
(實際上是數(shù)據(jù))不僅有用而且是必要的让簿,但是Java中函數(shù)編程還是很笨拙,它需要成長秀睛。 - 直到Java 1.7尔当,Java引入了java.lang.invoke包,提供一 種新的動態(tài)確定目標方法的機制(可以不用再單純依靠固化在虛擬機中的字節(jié)碼調用指令)蹂安,稱為MethodHandle居凶,模擬字節(jié)碼的方法指針調用,類似于C/C++的Function Pointer(函數(shù)指針)并引入第5條方法調用的字節(jié)碼指令invokedynamic藤抡。
- 直到Java 1.8,基于Java 1.7提出的字節(jié)碼指令invokedynamic抹估,實現(xiàn)了Lamda技術缠黍,將函數(shù)作為參數(shù)在方法間傳遞,Java開始更好的支持函數(shù)式編程药蜻。
- 用反射不是早就可以實現(xiàn)了嗎瓷式?Reflection API 重量級,性能低语泽。
注意: 5贸典、6、7參考《深入理解Java虛擬機》第2版踱卵,8.3.3 動態(tài)類型語言支持廊驼。
在POI的使用過程中,對大多數(shù)API User來說經(jīng)常面臨兩個問題惋砂,這也是GridExcel致力解決的問題妒挎。
問題1. 僅使用簡單的導入導出功能,但每次業(yè)務的數(shù)據(jù)對象結構不同西饵,需要重新編寫處理方法酝掩,很麻煩!
解決方法
將Excel處理邏輯抽取出來眷柔,封裝成工具類期虾。
封裝條件
與大多數(shù)Java API一樣原朝,POI把更多的精力放在高級功能的處理上,比如Formula(公式)镶苞、Conditional Formatting(條件格式)喳坠、Zoom(縮放)等。對于僅僅做數(shù)據(jù)導入導出功能的API User宾尚,很少使用這些高級特性丙笋,這允許API用戶對POI的使用進行簡單的封裝。
封裝方式
無論是讀是寫煌贴,我們都需要解決Excel中的Columns(列)與Java數(shù)據(jù)對象Fields(字段)的映射關系御板,將這種映射關系作為參數(shù)(Map對象HashMap或LinkedHashMap),傳遞給工具類牛郑。
對于Columns不難理解怠肋,它可以是有序的數(shù)字或字母,也可以是其它字符串用來作為首行淹朋,表示該列數(shù)據(jù)的含義笙各。
對于Fields,它的處理需要兼容復雜情況础芍,如下:
- 查詢字段時出現(xiàn)異常
- 字段或單元格的值為null
- 該列的值可能對應關聯(lián)對象杈抢、甚至是關聯(lián)集合中的某個字段值
- 字段或單元格的值需要做特殊處理,例如
value == true?完成:失斅匦浴惶楼;
反射
首先想到,也是大多數(shù)封裝者都在使用的方式是就是Reflection API诊杆,從上文 函數(shù)編程 章節(jié)我們了解到歼捐,反射重量級,會降低代碼的性能晨汹,同時對復雜情況的處理支持性不夠好豹储。
反射+注解
這種方式可以更好的支持復雜情況,但是反射依然會降低性能淘这,同時注解對數(shù)據(jù)對象會造成代碼侵入剥扣,而且對該工具類封裝者的其他使用者無疑會增加學習成本。
匿名內部類
這種方式也可以很好的支持復雜情況铝穷,但是使用匿名內部類的語法顯然患有“垂直問題”(這意味著代碼需要太多的線條來表達基本概念)朦乏,太過冗雜。至于性能氧骤,應該也不如直接傳遞函數(shù)來的快吧呻疹。
函數(shù)接口(Lambda)
這種方式是基于第5條方法調用的字節(jié)碼指令invokeDynamic實現(xiàn)的,直接傳遞函數(shù)代碼塊,很好的支持復雜情況刽锤,性能較高镊尺,代碼編寫更簡單結構更加簡潔,而且對數(shù)據(jù)對象代碼零侵入并思。
當然如果你還沒有使用Java1.8或更高版本庐氮,那么你可以參考匿名內部類或反射+注解,不過還是推薦反射+注解宋彼,Alibaba/easyexcel【https://github.com/alibaba/easyexcel】對你來說會是不錯的選擇弄砍。
問題2. Excel導入或導出數(shù)據(jù)量比較大,造成內存溢出
或頻繁的Full GC
输涕,該如何解決音婶?
解決方法
- 讀Excel —— eventmodel
- 寫Excel —— streaming.SXSSFWorkbook
原理
POI的使用對我們來說很常見,對下面兩個概念應該并不陌生:
- HSSFWorkbook(處理97(-2007) 的.xls)
- XSSFWorkbook(處理2007 OOXML (.xlsx) )
但是對于eventmodel和streaming.SXSSFWorkbook就很少接觸了莱坎,它們是POI提供的專門用來解決內存占用問題的low level API(低級API)衣式,使用它們可以讀寫數(shù)據(jù)量非常大的Excel,同時可以避免內存溢出
或頻繁的Full GC
檐什〔晡裕【https://poi.apache.org/components/spreadsheet/how-to.html】
- eventmodel,用來讀Excel乃正,并沒有將Excel整個加載到內存中住册,而是允許用戶從InputStream每讀取一些信息,就交給回調函數(shù)或監(jiān)聽器瓮具,至于丟棄荧飞,存儲還是怎么處理這些內容,都交由用戶搭综。
-
streaming.SXSSFWorkbook,用來寫Excel(是對XSSFWorkbook的封裝划栓,僅支持.xlsx)兑巾,通過滑動窗口來實現(xiàn),只在內存中保留滑動窗口允許存在的行數(shù)忠荞,超出的行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
解決途徑
-
https://github.com/liuhuagui/gridexcel
基于Java函數(shù)編程(Lambda),支持流式API碧绞,使用環(huán)境Java1.8或更高府框,學習成本:Lambda -
https://github.com/alibaba/easyexcel
基于反射+注解+監(jiān)聽器,使用環(huán)境Java1.6或以上讥邻,學習成本:模型注解
實際上POI官網(wǎng)已經(jīng)給了用戶使用示例迫靖,而上述兩個工具都只是做了自己的封裝實現(xiàn)院峡,使用者只需要拿來用就好。
快速使用
<dependency>
<groupId>com.github.liuhuagui</groupId>
<artifactId>gridexcel</artifactId>
<version>2.2</version>
</dependency>
GridExcel.java
GridExcel.java提供了多種靜態(tài)方法系宜,可以直接使用照激,具體式例可參考測試代碼(提供了測試數(shù)據(jù)和測試文件):
- 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
/**
* 業(yè)務邏輯處理方式三選一:
* 1.啟用windowListener,并將業(yè)務邏輯放在該函數(shù)中盹牧。
* 2.不啟用windowListener俩垃,使用get()方法取回全部數(shù)據(jù)集合,做后續(xù)處理汰寓。
* 3.readFunction函數(shù)口柳,直接放在函數(shù)中處理 或 使用final or effective final的局部變量存放這寫數(shù)據(jù),做后續(xù)處理踩寇。
* 注意:使用EventModel時readFunction函數(shù)的輸入為每行的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)))//推薦在這里執(zhí)行自己的業(yè)務邏輯
.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寫出數(shù)據(jù)到Excel
* @throws Exception
*/
@Test
public void writeExcelByStreaming() throws Exception {
GridExcel.writeByStreaming(TradeOrder.class)
.head(writeFunctionMap())//對象字段到Excel列的映射
.createSheet()
.process(MockData.data())//模擬數(shù)據(jù)。在這里設置業(yè)務數(shù)據(jù)集合俺孙。
.write(FileUtils.openOutputStream(new File("/excel/test.xlsx")));
}
ReadExcel
ReadExcelByUserModel
Use user model to read excel file. userModel ——
- 缺點:內存消耗大辣卒,會將excel信息全部加載到內存再進行處理。
- 優(yōu)點:現(xiàn)成的API睛榄,使用和理解更簡單荣茫。
- 使用場景:可以處理數(shù)據(jù)量較小的Excel。
ReadExcelByEventModel
Use event model to read excel file. eventModel ——
- 缺點:沒有現(xiàn)成的API场靴,使用和理解較為復雜啡莉,適合中高級程序員(GridExcel的目標之一就是讓EventModel的使用變得簡單)
- 優(yōu)點:非常小的內存占用,并沒有在一開始就將所有內容加載到內存中旨剥,而是把主體內容的處理(存儲咧欣,使用,丟棄)都交給了用戶轨帜,用戶可以自定義監(jiān)聽函數(shù)來處理這些內容魄咕。
- 使用場景:可以處理較大數(shù)據(jù)量的Excel,避免OOM和頻繁FullGC
WriteExcel
WriteExcelByUserModel
Use user model to write excel file. userModel ——
- 缺點:會將產生的spreadsheets對象整個保存在內存中蚌父,所以write Excel的大小受到堆內存(Heap space)大小限制哮兰。
- 優(yōu)點:使用和理解更簡單。
- 使用場景:可以寫出數(shù)據(jù)量較小的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.
- 優(yōu)點:通過滑動窗口來實現(xiàn)右遭,內存中只保留指定size of rows的內容,超出部分被寫出到臨時文件,write Excel的大小不再受到堆內存(Heap space)大小限制狸演。
- 使用場景:可以寫出非常大的Excel言蛇。
Issues
在使用工具過程中出現(xiàn)問題,有功能添加或改動需求的可以向作者提Issue:https://github.com/liuhuagui/gridexcel/issues
- 比如說宵距,想要增加對首行以外的行列做樣式擴展
如有疑問可以聯(lián)系作者:799600902@qq.com