使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)

前言

最近有讀者在問easypoi的問題,抽空整理了一份文章。

正文

EasyPOI功能如同名字Easy,主打的功能就是容易,讓一個沒接觸過POI的人員可以方便的寫出excel導(dǎo)出,Excel模板導(dǎo)出,Excel導(dǎo)入,word模板導(dǎo)出。通過簡單的注解和模板語言(熟悉的表達式語法),完成以前復(fù)雜的寫法。

本文主要通過簡單的分析讓讀者知道Excel模板該如何編寫,EasyPOI要如何使用才能導(dǎo)出滿足自己需要的Excel數(shù)據(jù),從而簡化編碼。同時本文還會對一些不常見的功能如圖片導(dǎo)出功能進行說明,讓讀者少踩坑。

版本及依賴說明

EasyPOI4.0.0及以后的版本依賴于apache POI的4.0.0及以后版本。所以在maven的配置中,兩者的版本號一定要匹配。

需要注意的是,Apache POI的4.0.0相對之前的版本有很大的變更,如果之前代碼中Excel操作部分依賴于舊的版本,那么不建議使用4.0.0及之后的版本。當(dāng)然,如果之前代碼不涉及或很少涉及WorkBook的創(chuàng)建細(xì)節(jié),使用新版也沒有問題。

筆者需要改寫的項目基于JEECG 3.7版本,依賴的是3.9版本的Apache POI,而JEECG維護的jeasypoi版本最高只有2.2.0,而該版本并不支持模板導(dǎo)出圖片功能。說到這里又要吐槽以下JEECG團隊,既然自己不打算維護jeasypoi,那項目中直接使用官方的EasyPOI不就好了,2.2.0版本的jeasypoi給開發(fā)者挖了多少坑啊!

為了和舊版本兼容,又想使用EasyPOI帶來的圖片導(dǎo)出功能,所以筆者最終采用的EasyPOI版本是3.3.0,對應(yīng)的Apache POI依賴是3.15。

Maven配置如下所示:

<properties> ????<poi.version>3.15</poi.version> ????<easypoi.version>3.3.0</easypoi.version></properties><dependencies> ????<dependency> ????????<groupid>org.apache.poi</groupid> ????????<artifactid>poi</artifactid> ????????<version>${poi.version}</version> ????</dependency> ????<dependency> ????????<groupid>org.apache.poi</groupid> ????????<artifactid>poi-ooxml</artifactid> ????????<version>${poi.version}</version> ????</dependency> ????<dependency> ????????<groupid>org.apache.poi</groupid> ????????<artifactid>poi-ooxml-schemas</artifactid> ????????<version>${poi.version}</version> ????</dependency> ????<dependency> ????????<groupid>org.apache.poi</groupid> ????????<artifactid>poi-scratchpad</artifactid> ????????<version>${poi.version}</version> ????</dependency> ????<dependency> ????????<groupid>cn.afterturn</groupid> ????????<artifactid>easypoi-web</artifactid> ????????<version>${easypoi.version}</version> ????</dependency></dependencies>

Excel模板的設(shè)計

我們使用EasyPOI的模板導(dǎo)出功能就是不想通過編碼的方式來設(shè)計Excel報表的樣式,所以工作的第一步就是設(shè)計Excel模板,分清楚哪些部分是固定的,哪些是需要循環(huán)填充的。EasyPOI有自己的表達式語言,每種表達式的詳細(xì)介紹請參考后文的參考鏈接。

一個簡單的Excel報表模板

一些簡單的模板就不在這里詳細(xì)解釋了,只放一下效果圖和模板配置內(nèi)容。等讀者明白了復(fù)雜的模板如何制作并如何填值的時候,簡單的很快就能明白了。

先看看報表效果圖:

使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)

再看看實際的模板:

使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)

看了上述兩張圖,是不是已經(jīng)感受到模板導(dǎo)出功能的強大了呢?

一個復(fù)雜的Excel報表模板

下面要介紹的這個模板比較復(fù)雜,不像是常見的那種一行是一條記錄的情況,所以將詳細(xì)介紹該模板的配置,順帶對EasyPOI的部分表達式進行簡單介紹。

還是先看效果圖:

使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)

再看看模板:

使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)

這兩張圖一對比,是不是有種知識改變命運的感覺?

復(fù)雜模板設(shè)計剖析

從貨品信息的模板圖及效果圖中我們發(fā)現(xiàn),整個模板實際上分為上下兩部分。其中上部分為不變的抬頭信息,下部分為循環(huán)插入的貨品明細(xì)信息。所以我們關(guān)注的重點是下半部分的語法。

下半部分的第一列圖中沒有顯示完整,實際上是{{!fe: list t.id。

注意,這里 沒有 }}符號!根據(jù)EasyPOI的官方文檔,{{}}代表的是表達式,根據(jù)表達式取里邊的值。仔細(xì)看圖可以發(fā)現(xiàn),表達式的閉合符號{{}}出現(xiàn)在圖中的右下角。也就是說,從第一列{{開始至右下角}}結(jié)束,這中間的所有內(nèi)容都是表達式的一部分。

因為整個模板信息都是表達式的一部分,所以即使是普通字符串也需要專門標(biāo)明。下面對表達式中的子表達式進行逐個說明。

!fe: 遍歷數(shù)據(jù)不創(chuàng)建row。

官方文檔中的這句話大家理解起來可能有點費解,什么叫不創(chuàng)建row?實際上,不創(chuàng)建row是相對于創(chuàng)建row而言的,創(chuàng)建row的表達式是fe:。

就像是數(shù)據(jù)庫中每條記錄對應(yīng)著一個實體對象,創(chuàng)建row表示每行就是一個實體對象Entity,這個實體對象的屬性用{{}}表達式包裹起來。

不創(chuàng)建row表示整個表達式中只有一個實體對象Object,只不過這個Object比較特別,它是由list中N個Entity拼接起來的。每一個Entity不僅僅是指模型本身,也包含了Excel的樣式,比如占用了幾個單元格,單元格的坐標(biāo)、排布順序等。

list 自定義的名稱,表示表達式中的數(shù)據(jù)集合,由代碼以list為鍵,從map中獲取值的集合。

list這個名字容易理解,就是一個占位符,可以隨便取。EasyPOI解析到list就知道Map中存在著該鍵的值的集合,后邊解析到數(shù)據(jù)就從該集合中取即可。

搜索Java知音公眾號,回復(fù)“后端面試”,送你一份Java面試題寶典.pdf

t 預(yù)定義值,表示集合中的任意對象。

從模板中我們大致能感覺到,list中每個對象叫做t,t.name就代表t的name屬性,所以t這個名字就可以隨便叫,反正它和list一樣,作用是占位符。

但實際上這是一個大坑!如果你把t換成了其他值比如g,模板中其他地方寫g.name g.code等等,最終是解析不到的!官方文檔對這一點并沒有強調(diào),而是作者實際踩了坑之后才發(fā)現(xiàn)的!

]] 換行符 多行遍歷導(dǎo)出。

對于這個符號的官方解釋也是莫名其妙,什么叫換行符,多行遍歷導(dǎo)出?實際上它的意思就是,當(dāng)解析到表達式中含有這個符號,該行后邊的內(nèi)容就不解析了,管你后邊有沒有其他內(nèi)容或者樣式。

該符號一定要寫在每行的最后一列,不然會出現(xiàn)每行列數(shù)不一樣的情況,EasyPOI內(nèi)部做賦值的時候就會報空指針異常了。

‘’ 單引號表示常量值 ‘’ 比如’1’ 那么輸出的就是 1

官方文檔中這里的介紹也有坑。”是表示常量值,但實際上Excel中只是這么些是不對的,因為Excel的單元格中遇到’后會認(rèn)為后面都是字符串,所以得在單元格中寫”庫別:’,這樣顯示出來的才是’庫別:’,而不是字符串庫別:’。

經(jīng)過上述分析,圖中的模板實際上就類似以下內(nèi)容:

{{!fe: list t.id ‘庫別:’ t.bin 換行 ‘商品名稱:’ t.name 換行 ‘商品編號:’ t.code t.barcode 換行 ‘生產(chǎn)日期:’ t.proDate 換行 ‘進貨日期:’ t.recvDate}}

如果list中有多條記錄,上述字符串就再循環(huán)拼接一些內(nèi)容,最終都在一個{{}}表達式中。

至此,模板的設(shè)計已剖析完畢,讀者可根據(jù)自己的需求結(jié)合官方文檔自行設(shè)計模板。下面將對模板賦值進行介紹。

準(zhǔn)備模板數(shù)據(jù)

從上節(jié)的描述中可知,只需要準(zhǔn)備一個Map的對象即可,其中鍵為list,值為一個List數(shù)組,數(shù)組中元素類型為Map。代碼如下:

Map<String>?total?=?new?HashMap(); List<map>&gt;?mapList?=?new?ArrayList(); for?(int?i?=?1;?i??map?=?new?HashMap(); ????map.put("id",?i?+?""); ????map.put("bin",?"001?1000千克"); ????map.put("name",?"商品"?+?i); ????map.put("code",?"goods"?+?i); ????map.put("proDate",?"2019-05-30"); ????map.put("recvDate",?"2019-07-07");  ????//?插入圖片 ????ByteArrayOutputStream?byteArrayOut?=?new?ByteArrayOutputStream(); ????BufferedImage?bufferImg?=?ImageIO.read(BarcodeUtil.generateToStream("001")); ????ImageIO.write(bufferImg,?"jpg",?byteArrayOut); ????ImageEntity?imageEntity?=?new?ImageEntity(byteArrayOut.toByteArray(),?200,?1000); ????map.put("barcode",?imageEntity);  ????mapList.add(map); } total.put("list",?mapList);</map></string>

圖片數(shù)據(jù)導(dǎo)出

上述代碼中需要特殊關(guān)注的是圖片導(dǎo)出部分。EasyPOI導(dǎo)出圖片有兩種方式,一種是通過圖片的Url,還有一種是獲取圖片的byte[],畢竟圖片的本質(zhì)就是byte[]。因為筆者的項目中圖片不是存放在數(shù)據(jù)庫之中,而是需要根據(jù)查詢結(jié)果動態(tài)生成條碼,所以通過byte[]導(dǎo)出圖片。

ImageEntity是EasyPOI內(nèi)置的一個JavaBean,用于設(shè)定圖片的寬度和高度、導(dǎo)出方式、RowSpan和ColumnSpan等。調(diào)試EasyPOI的源碼可知,當(dāng)設(shè)置了RowSpan或者ColumnSpan之后,圖片的高度設(shè)置就失效了,圖片大小會自動填充圖片所在的單元格。

圖片導(dǎo)出的坑點在于導(dǎo)出圖片的大小。假設(shè)我們將四個單元格合成為一個,希望導(dǎo)出的圖片能填充合并之后的單元格,但是對不起,EasyPOI暫時做不到,它只會填充合并之前左上角的單元格,具體原因如下源碼所示:

//BaseExportService.java ClientAnchor?anchor; if?(type.equals(ExcelType.HSSF))?{ ????anchor?=?new?HSSFClientAnchor(0,?0,?0,?0,?(short)?cell.getColumnIndex(),?cell.getRow().getRowNum(),?(short)?(cell.getColumnIndex()?+?1), ????????????cell.getRow().getRowNum()?+?1); }?else?{ ????anchor?=?new?XSSFClientAnchor(0,?0,?0,?0,?(short)?cell.getColumnIndex(),?cell.getRow().getRowNum(),?(short)?(cell.getColumnIndex()?+?1), ????????????cell.getRow().getRowNum()?+?1); }

可以看到,在創(chuàng)建圖片插入位置的時候已經(jīng)指定了圖片的跨度為1行1列,即左上角的單元格。如果之前又設(shè)置了RowSpan或者ColumnSpan,那么圖片高度的設(shè)置也會失效,最終導(dǎo)致導(dǎo)出的圖片非常小。

搜索Java知音公眾號,回復(fù)“后端面試”,送你一份Java面試題寶典.pdf

個人認(rèn)為ImageEntity提供的RowSpan或者ColumnSpan的set方法并沒有什么用,因為我們動態(tài)創(chuàng)建的合并單元格并不能被賦值。所以,導(dǎo)出圖片的最好方式就是直接指定它的高度,因為寬度會自動填充單元格,模板中單元格的寬度要合適。

//ExcelExportOfTemplateUtil.java if?(img.getRowspan()&gt;1?||?img.getColspan()?&gt;?1){ ????img.setHeight(0); ????PoiMergeCellUtil.addMergedRegion(cell.getSheet(),cell.getRowIndex(), ????????????cell.getRowIndex()?+?img.getRowspan()?-?1,?cell.getColumnIndex(),?cell.getColumnIndex()?+?img.getColspan()?-1); }

將數(shù)據(jù)導(dǎo)出至模板

以上準(zhǔn)備工作全部完成后就可以將模板和數(shù)據(jù)進行組裝了,或者說是渲染,代碼如下所示:

public?static?void?exportByTemplate(String?templateName,?Map<string>?data,?OutputStream?fileOut)?{ ????TemplateExportParams?params?=?new?TemplateExportParams("export/template/"?+?templateName,?true); ????try?{ ????????Workbook?workbook?=?ExcelExportUtil.exportExcel(params,?data); ????????workbook.write(fileOut); ????}?catch?(Exception?e)?{ ????????LogUtil.error("",?e); ????} }</string>

總結(jié)

網(wǎng)上針對EasyPOI的介紹多限于最基本的行插入功能,但實際上Excel模板的需求可能各式各樣。本文只是拋磚引玉,對EasyPOI中的部分概念做了詳細(xì)介紹,希望幫助大家少踩坑。

如果想詳細(xì)了解EasyPOI的各種功能,參考鏈接中的文檔說明及測試項目源碼就是最好的學(xué)習(xí)資料。希望大家都能得心應(yīng)手地使用EasyPOI,大大提升開發(fā)效率!

參考鏈接

EasyPOI官方文檔

  • https://opensource.afterturn.cn/doc/easypoi.html

EasyPOI測試項目

  • https://gitee.com/lemur/easypoi-test

一些坑

近日有網(wǎng)友求助我解決EasyPOI的復(fù)雜模板配置問題,通過解決該網(wǎng)友的問題發(fā)現(xiàn)了EasyPOI中的幾個坑點,補充說明幾個問題。

在復(fù)雜模板設(shè)計剖析一節(jié)中已經(jīng)描述了EasyPOI支持的復(fù)雜的模板該如何配置。該模板的配置是絕對正確的,但是有3個點沒有說清楚,大家在照葫蘆畫瓢時容易出錯:

  1. {{!fe: list需要在一個單獨的列中。EasyPOI源碼中是根據(jù)該單元格的行、列跨度來決定list中的每個元素需要多少行的。比如上述圖片中,該單元格的跨度是5行1列,也就是說,以后list中的每個元素都會占用5行。如果覺得該列不符合自定義模板的風(fēng)格,可以把該列的列寬設(shè)置為0,但一定需要有{{!fe: list。
  2. 在對象的起始和結(jié)束符號{{}}之間不能有任何空的單元格!代碼中在解析到該單元格為空時會直接拋異常,如果就希望該單元格為空,得顯示寫入空字符串:’’’。
  3. 換行符]]必須占用每行的最后一個單元格!比如說第一行有10個單元格,第二行只用了前5個,那么不能直接在第5個結(jié)束直接寫換行符]],而是需要把6-10個單元格合并,然后寫入]]。參考上述圖片中生產(chǎn)日期所在行的最后一列。這么設(shè)置的原因是EasyPOI要求每行的單元格數(shù)目完全一致,因為源碼中判斷了每個單元格的列跨度,如果提前使用了]]換行符,那么該列的數(shù)目就和其他行不同,那么賦值的時候就亂掉了,會出現(xiàn)索引異常。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊8 分享