解決Java開發(fā)中使用POI讀寫Excel時(shí)面對(duì)的兩個(gè)麻煩

麻煩1

僅使用簡(jiǎn)單的導(dǎo)入導(dǎo)出功能,但每次業(yè)務(wù)的數(shù)據(jù)對(duì)象結(jié)構(gòu)不同,需要重新編寫處理方法,很麻煩!

解決方法

將Excel讀寫邏輯抽取出來,只關(guān)注業(yè)務(wù)邏輯,封裝成工具類。

封裝條件

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

封裝方式

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

對(duì)于Columns不難理解,它可以是有序的數(shù)字或字母,也可以是其它字符串用來作為首行,表示該列數(shù)據(jù)的含義。

對(duì)于Fields,它的處理需要兼容復(fù)雜情況,如下:

  • 查詢字段時(shí)出現(xiàn)異常
  • 字段或單元格的值為null
  • 該列的值可能對(duì)應(yīng)關(guān)聯(lián)對(duì)象、甚至是關(guān)聯(lián)集合中的某個(gè)字段值
  • 字段或單元格的值需要做特殊處理,例如value == true?完成:失敗;
反射

首先想到,也是大多數(shù)封裝者都在使用的方式是就是Reflection API,從上文 函數(shù)編程 章節(jié)我們了解到,反射重量級(jí),會(huì)降低代碼的性能,同時(shí)對(duì)復(fù)雜情況的處理支持性不夠好。

反射+注解

這種方式可以更好的支持復(fù)雜情況,但是反射依然會(huì)降低性能,同時(shí)注解對(duì)數(shù)據(jù)對(duì)象會(huì)造成代碼侵入,而且對(duì)該工具類封裝者的其他使用者無疑會(huì)增加學(xué)習(xí)成本。

匿名內(nèi)部類—— 作為監(jiān)聽函數(shù)

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

函數(shù)接口(Lambda)—— 作為監(jiān)聽函數(shù)

這種方式是基于第5條方法調(diào)用的字節(jié)碼指令invokeDynamic實(shí)現(xiàn)的,直接傳遞函數(shù)代碼塊,很好的支持復(fù)雜情況,性能較高,代碼編寫更簡(jiǎn)單結(jié)構(gòu)更加簡(jiǎn)潔,而且對(duì)數(shù)據(jù)對(duì)象代碼零侵入。


麻煩2

Excel一次性讀寫數(shù)據(jù)量比較大,造成內(nèi)存溢出頻繁的Full GC,該如何解決?

解決方法

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

原理

POI的使用對(duì)我們來說很常見,對(duì)下面兩個(gè)概念應(yīng)該并不陌生:

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

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

  • eventmodel ,用來讀Excel,并沒有將Excel整個(gè)加載到內(nèi)存中,而是允許用戶從 InputStream 每讀取一些信息,就交給 回調(diào)函數(shù)監(jiān)聽器 ,至于丟棄,存儲(chǔ)還是怎么處理這些內(nèi)容,都交由用戶。
  • streaming.SXSSFWorkbook ,用來寫Excel(是對(duì)XSSFWorkbook的封裝,僅支持.xlsx),通過 滑動(dòng)窗口 來實(shí)現(xiàn),只在內(nèi)存中保留滑動(dòng)窗口允許存在的行數(shù),超出的行Rows被寫出到臨時(shí)文件,當(dāng)調(diào)用write(OutputStream stream)方法寫出內(nèi)容時(shí),再直接從臨時(shí)內(nèi)存寫出到目標(biāo) OutputStream 。 SXSSFWorkbook 的使用會(huì)產(chǎn)生一些局限性。
  • Only a limited number of rows are accessible at a point in time.
  • Sheet.clone() is not supported.
  • Formula evaluation is not supported

開源解決方案

GridExcelhttps://github.com/liuhuagui/gridexcel

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

GridExcel是基于Java8函數(shù)式編程和POI EventModel實(shí)現(xiàn)的用于Excel簡(jiǎn)單讀寫的通用解決方案。致力于解決上述兩個(gè)問題。(注意,GridExcel并沒有改變POI,僅僅是對(duì)它的合理封裝。)

  • 基于POI EventModel,在讀寫數(shù)據(jù)量非常大的Excel時(shí),降低內(nèi)存占用避免OOM與頻繁FullGC
  • 基于函數(shù)編程,支持關(guān)聯(lián)對(duì)象等多種復(fù)雜情況的處理,學(xué)習(xí)成本低
  • 支持流式API,使代碼編寫和理解更簡(jiǎn)單,更直觀
  • 支持使用阻塞窗口+監(jiān)聽函數(shù)的方式去處理從Excel中讀取的數(shù)據(jù)

概念基礎(chǔ)

Apache POI

在業(yè)務(wù)開發(fā)中我們經(jīng)常會(huì)遇到Excel的導(dǎo)入導(dǎo)出,而 Apache POI 是Java開發(fā)者常用的API。
https://poi.apache.org/components/spreadsheet/index.html

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包是一個(gè)用于讀取Excel文件而不將整個(gè)電子表格加載到內(nèi)存中的API。 它確實(shí)需要用戶掌握更多知識(shí),但是將內(nèi)存消耗減少了十倍以上。 它基于AWT(Abstract Window Toolkit)event model與SAX的結(jié)合。 如果您需要只讀訪問權(quán)限,這是最好的方式。

函數(shù)編程

說到函數(shù)編程,就不得不提 Lambda表達(dá)式 ,如果對(duì)Java8中的Lambda不了解或理解不深刻,可以看下甲骨文官網(wǎng)給出的這篇文章,【https://www.oracle.com/technetwork/articles/java/architect-lambdas-part1-2080972.html】,個(gè)人認(rèn)為這是Java8 Lambda從入門到進(jìn)階最好的文章之一。

其中函數(shù)編程的目的就是實(shí)現(xiàn)代碼塊傳遞,即,將方法作為參數(shù)在方法間傳遞。為此,隨著Java語言的發(fā)展,不斷出現(xiàn)一些解決方案:

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

注意: 5、6、7 參考《深入理解Java虛擬機(jī)》第2版,8.3.3 動(dòng)態(tài)類型語言支持。


快速使用

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

GridExcel.java

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

流式API

/**
  * 業(yè)務(wù)邏輯處理方式三選一:
  * 1.啟用windowListener,并將業(yè)務(wù)邏輯放在該函數(shù)中。
  * 2.不啟用windowListener,使用get()方法取回全部數(shù)據(jù)集合,做后續(xù)處理。
  * 3.readFunction函數(shù),直接放在函數(shù)中處理 或 使用final or effective final的局部變量存放這寫數(shù)據(jù),做后續(xù)處理。
  * 注意:使用EventModel時(shí)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è)務(wù)邏輯
             .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())//對(duì)象字段到Excel列的映射
             .createSheet()
             .process(MockData.data())//模擬數(shù)據(jù)。在這里設(shè)置業(yè)務(wù)數(shù)據(jù)集合。
             .write(FileUtils.openOutputStream(new File("/excel/test.xlsx")));
 }

無實(shí)體類讀寫Excel

由于沒有自定義業(yè)務(wù)實(shí)體類,這里我們可以使用Map.class來代替。下面是讀入Excel的例子,寫Excel可以參照實(shí)現(xiàn)。

 /**
     * No Entity無實(shí)體類讀Excel文件
     * 業(yè)務(wù)邏輯處理方式三選一:
     * 1.啟用windowListener,并將業(yè)務(wù)邏輯放在該函數(shù)中。
     * 2.不啟用windowListener,使用get()方法取回全部數(shù)據(jù)集合,做后續(xù)處理。
     * 3.readFunction函數(shù),直接放在函數(shù)中處理 或 使用final or effective final的局部變量存放這寫數(shù)據(jù),做后續(xù)處理。
     * 注意:使用EventModel時(shí)readFunction函數(shù)的輸入為每行的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)))//推薦在這里執(zhí)行自己的業(yè)務(wù)邏輯
                .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);
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容