使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)
點(diǎn)擊上方藍(lán)色“程序猿DD”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!

作者 |?星懸月
EasyPOI功能如同名字Easy,主打的功能就是容易,讓一個(gè)沒接觸過POI的人員可以方便的寫出Excel導(dǎo)出,Excel模板導(dǎo)出,Excel導(dǎo)入,Word模板導(dǎo)出。通過簡單的注解和模板語言(熟悉的表達(dá)式語法),完成以前復(fù)雜的寫法。
本文主要通過簡單的分析讓讀者知道Excel模板該如何編寫,EasyPOI要如何使用才能導(dǎo)出滿足自己需要的Excel數(shù)據(jù),從而簡化編碼。同時(shí)本文還會對一些不常見的功能如圖片導(dǎo)出功能進(jìn)行說明,讓讀者少踩坑。
版本及依賴說明
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é),使用新版也沒有問題。
筆者需要改寫的項(xiàng)目基于JEECG 3.7版本,依賴的是3.9版本的Apache POI,而JEECG維護(hù)的jeasypoi版本最高只有2.2.0,而該版本并不支持模板導(dǎo)出圖片功能。說到這里又要吐槽以下JEECG團(tuán)隊(duì),既然自己不打算維護(hù)jeasypoi,那項(xiàng)目中直接使用官方的EasyPOI不就好了,2.2.0版本的jeasypoi給開發(fā)者挖了多少坑啊!
為了和舊版本兼容,又想使用EasyPOI帶來的圖片導(dǎo)出功能,所以筆者最終采用的EasyPOI版本是3.3.0,對應(yīng)的Apache POI依賴是3.15。
Maven配置如下所示:
<properties>
????<poi.version>3.15poi.version>
????<easypoi.version>3.3.0easypoi.version>
properties>
<dependencies>
????<dependency>
????????<groupId>org.apache.poigroupId>
????????<artifactId>poiartifactId>
????????<version>${poi.version}version>
????dependency>
????<dependency>
????????<groupId>org.apache.poigroupId>
????????<artifactId>poi-ooxmlartifactId>
????????<version>${poi.version}version>
????dependency>
????<dependency>
????????<groupId>org.apache.poigroupId>
????????<artifactId>poi-ooxml-schemasartifactId>
????????<version>${poi.version}version>
????dependency>
????<dependency>
????????<groupId>org.apache.poigroupId>
????????<artifactId>poi-scratchpadartifactId>
????????<version>${poi.version}version>
????dependency>
????<dependency>
????????<groupId>cn.afterturngroupId>
????????<artifactId>easypoi-webartifactId>
????????<version>${easypoi.version}version>
????dependency>
dependencies>
Excel模板的設(shè)計(jì)
我們使用EasyPOI的模板導(dǎo)出功能就是不想通過編碼的方式來設(shè)計(jì)Excel報(bào)表的樣式,所以工作的第一步就是設(shè)計(jì)Excel模板,分清楚哪些部分是固定的,哪些是需要循環(huán)填充的。EasyPOI有自己的表達(dá)式語言,每種表達(dá)式的詳細(xì)介紹請參考后文的參考鏈接。
一個(gè)簡單的Excel報(bào)表模板
一些簡單的模板就不在這里詳細(xì)解釋了,只放一下效果圖和模板配置內(nèi)容。等讀者明白了復(fù)雜的模板如何制作并如何填值的時(shí)候,簡單的很快就能明白了。
先看看報(bào)表效果圖:

再看看實(shí)際的模板:

看了上述兩張圖,是不是已經(jīng)感受到模板導(dǎo)出功能的強(qiáng)大了呢?
一個(gè)復(fù)雜的Excel報(bào)表模板
下面要介紹的這個(gè)模板比較復(fù)雜,不像是常見的那種一行是一條記錄的情況,所以將詳細(xì)介紹該模板的配置,順帶對EasyPOI的部分表達(dá)式進(jìn)行簡單介紹。
還是先看效果圖:
再看看模板:

這兩張圖一對比,是不是有種知識改變命運(yùn)的感覺?
復(fù)雜模板設(shè)計(jì)剖析
從貨品信息的模板圖及效果圖中我們發(fā)現(xiàn),整個(gè)模板實(shí)際上分為上下兩部分。其中上部分為不變的抬頭信息,下部分為循環(huán)插入的貨品明細(xì)信息。所以我們關(guān)注的重點(diǎn)是下半部分的語法。
下半部分的第一列圖中沒有顯示完整,實(shí)際上是{{!fe: list t.id。
注意,這里 沒有 }}符號!根據(jù)EasyPOI的官方文檔,{{}}代表的是表達(dá)式,根據(jù)表達(dá)式取里邊的值。仔細(xì)看圖可以發(fā)現(xiàn),表達(dá)式的閉合符號{{}}出現(xiàn)在圖中的右下角。也就是說,從第一列{{開始至右下角}}結(jié)束,這中間的所有內(nèi)容都是表達(dá)式的一部分。
因?yàn)檎麄€(gè)模板信息都是表達(dá)式的一部分,所以即使是普通字符串也需要專門標(biāo)明。下面對表達(dá)式中的子表達(dá)式進(jìn)行逐個(gè)說明。
!fe: 遍歷數(shù)據(jù)不創(chuàng)建row。
官方文檔中的這句話大家理解起來可能有點(diǎn)費(fèi)解,什么叫不創(chuàng)建row?實(shí)際上,不創(chuàng)建row是相對于創(chuàng)建row而言的,創(chuàng)建row的表達(dá)式是fe:。
就像是數(shù)據(jù)庫中每條記錄對應(yīng)著一個(gè)實(shí)體對象,創(chuàng)建row表示每行就是一個(gè)實(shí)體對象Entity,這個(gè)實(shí)體對象的屬性用{{}}表達(dá)式包裹起來。
不創(chuàng)建row表示整個(gè)表達(dá)式中只有一個(gè)實(shí)體對象Object,只不過這個(gè)Object比較特別,它是由list中N個(gè)Entity拼接起來的。每一個(gè)Entity不僅僅是指模型本身,也包含了Excel的樣式,比如占用了幾個(gè)單元格,單元格的坐標(biāo)、排布順序等。
list 自定義的名稱,表示表達(dá)式中的數(shù)據(jù)集合,由代碼以list為鍵,從Map
list這個(gè)名字容易理解,就是一個(gè)占位符,可以隨便取。EasyPOI解析到list就知道Map
t 預(yù)定義值,表示集合中的任意對象。
從模板中我們大致能感覺到,list中每個(gè)對象叫做t,t.name就代表t的name屬性,所以t這個(gè)名字就可以隨便叫,反正它和list一樣,作用是占位符。
但實(shí)際上這是一個(gè)大坑!如果你把t換成了其他值比如g,模板中其他地方寫g.name g.code等等,最終是解析不到的!官方文檔對這一點(diǎn)并沒有強(qiáng)調(diào),而是作者實(shí)際踩了坑之后才發(fā)現(xiàn)的!
]] 換行符 多行遍歷導(dǎo)出。
對于這個(gè)符號的官方解釋也是莫名其妙,什么叫換行符,多行遍歷導(dǎo)出?實(shí)際上它的意思就是,當(dāng)解析到表達(dá)式中含有這個(gè)符號,該行后邊的內(nèi)容就不解析了,管你后邊有沒有其他內(nèi)容或者樣式。
該符號一定要寫在每行的最后一列,不然會出現(xiàn)每行列數(shù)不一樣的情況,EasyPOI內(nèi)部做賦值的時(shí)候就會報(bào)空指針異常了。
‘’ 單引號表示常量值 ‘’ 比如’1’ 那么輸出的就是 1
官方文檔中這里的介紹也有坑。''是表示常量值,但實(shí)際上Excel中只是這么些是不對的,因?yàn)镋xcel的單元格中遇到'后會認(rèn)為后面都是字符串,所以得在單元格中寫''庫別:',這樣顯示出來的才是'庫別:',而不是字符串庫別:'。
經(jīng)過上述分析,圖中的模板實(shí)際上就類似以下內(nèi)容:
{{!fe: list t.id ‘庫別:’ t.bin 換行 ‘商品名稱:’ t.name 換行 ‘商品編號:’ t.code t.barcode 換行 ‘生產(chǎn)日期:’ t.proDate 換行 ‘進(jìn)貨日期:’ t.recvDate}}
如果list中有多條記錄,上述字符串就再循環(huán)拼接一些內(nèi)容,最終都在一個(gè){{}}表達(dá)式中。
至此,模板的設(shè)計(jì)已剖析完畢,讀者可根據(jù)自己的需求結(jié)合官方文檔自行設(shè)計(jì)模板。下面將對模板賦值進(jìn)行介紹。
準(zhǔn)備模板數(shù)據(jù)
從上節(jié)的描述中可知,只需要準(zhǔn)備一個(gè)Map
Map?total?=?new?HashMap<>();
List 圖片數(shù)據(jù)導(dǎo)出
上述代碼中需要特殊關(guān)注的是圖片導(dǎo)出部分。EasyPOI導(dǎo)出圖片有兩種方式,一種是通過圖片的Url,還有一種是獲取圖片的byte[],畢竟圖片的本質(zhì)就是byte[]。因?yàn)楣P者的項(xiàng)目中圖片不是存放在數(shù)據(jù)庫之中,而是需要根據(jù)查詢結(jié)果動態(tài)生成條碼,所以通過byte[]導(dǎo)出圖片。
ImageEntity是EasyPOI內(nèi)置的一個(gè)JavaBean,用于設(shè)定圖片的寬度和高度、導(dǎo)出方式、RowSpan和ColumnSpan等。調(diào)試EasyPOI的源碼可知,當(dāng)設(shè)置了RowSpan或者ColumnSpan之后,圖片的高度設(shè)置就失效了,圖片大小會自動填充圖片所在的單元格。
圖片導(dǎo)出的坑點(diǎn)在于導(dǎo)出圖片的大小。假設(shè)我們將四個(gè)單元格合成為一個(gè),希望導(dǎo)出的圖片能填充合并之后的單元格,但是對不起,EasyPOI暫時(shí)做不到,它只會填充合并之前左上角的單元格,具體原因如下源碼所示:
//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)建圖片插入位置的時(shí)候已經(jīng)指定了圖片的跨度為1行1列,即左上角的單元格。如果之前又設(shè)置了RowSpan或者ColumnSpan,那么圖片高度的設(shè)置也會失效,最終導(dǎo)致導(dǎo)出的圖片非常小。
個(gè)人認(rèn)為ImageEntity提供的RowSpan或者ColumnSpan的set方法并沒有什么用,因?yàn)槲覀儎討B(tài)創(chuàng)建的合并單元格并不能被賦值。所以,導(dǎo)出圖片的最好方式就是直接指定它的高度,因?yàn)閷挾葧詣犹畛鋯卧瘢0逯袉卧竦膶挾纫线m。
//ExcelExportOfTemplateUtil.java
if?(img.getRowspan()>1?||?img.getColspan()?>?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ù)進(jìn)行組裝了,或者說是渲染,代碼如下所示:
public?static?void?exportByTemplate(String?templateName,?Map?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);
????}
}
總結(jié)
網(wǎng)上針對EasyPOI的介紹多限于最基本的行插入功能,但實(shí)際上Excel模板的需求可能各式各樣。本文只是拋磚引玉,對EasyPOI中的部分概念做了詳細(xì)介紹,希望幫助大家少踩坑。
如果想詳細(xì)了解EasyPOI的各種功能,參考鏈接中的文檔說明及測試項(xiàng)目源碼就是最好的學(xué)習(xí)資料。希望大家都能得心應(yīng)手地使用EasyPOI,大大提升開發(fā)效率!
參考鏈接
EasyPOI官方文檔
https://opensource.afterturn.cn/doc/easypoi.html
EasyPOI測試項(xiàng)目
https://gitee.com/lemur/easypoi-test
一些坑
近日有網(wǎng)友求助我解決EasyPOI的復(fù)雜模板配置問題,通過解決該網(wǎng)友的問題發(fā)現(xiàn)了EasyPOI中的幾個(gè)坑點(diǎn),補(bǔ)充說明幾個(gè)問題。
在復(fù)雜模板設(shè)計(jì)剖析一節(jié)中已經(jīng)描述了EasyPOI支持的復(fù)雜的模板該如何配置。該模板的配置是絕對正確的,但是有3個(gè)點(diǎn)沒有說清楚,大家在照葫蘆畫瓢時(shí)容易出錯(cuò):
{{!fe: list需要在一個(gè)單獨(dú)的列中。EasyPOI源碼中是根據(jù)該單元格的行、列跨度來決定list中的每個(gè)元素需要多少行的。比如上述圖片中,該單元格的跨度是5行1列,也就是說,以后list中的每個(gè)元素都會占用5行。如果覺得該列不符合自定義模板的風(fēng)格,可以把該列的列寬設(shè)置為0,但一定需要有{{!fe: list。 在對象的起始和結(jié)束符號{{}}之間不能有任何空的單元格!代碼中在解析到該單元格為空時(shí)會直接拋異常,如果就希望該單元格為空,得顯示寫入空字符串:’’’。 換行符]]必須占用每行的最后一個(gè)單元格!比如說第一行有10個(gè)單元格,第二行只用了前5個(gè),那么不能直接在第5個(gè)結(jié)束直接寫換行符]],而是需要把6-10個(gè)單元格合并,然后寫入]]。參考上述圖片中生產(chǎn)日期所在行的最后一列。這么設(shè)置的原因是EasyPOI要求每行的單元格數(shù)目完全一致,因?yàn)樵创a中判斷了每個(gè)單元格的列跨度,如果提前使用了]]換行符,那么該列的數(shù)目就和其他行不同,那么賦值的時(shí)候就亂掉了,會出現(xiàn)索引異常。
往期推薦
我們在星球聊了很多深度話題,你不來看看?


我的星球是否適合你?
點(diǎn)擊閱讀原文看看我們都聊過啥?
