前言
最近有讀者在問(wèn)easypoi的問(wèn)題,抽空整理了一份文章。
正文
EasyPOI功能如同名字Easy,主打的功能就是容易,讓一個(gè)沒(méi)接觸過(guò)POI的人員可以方便的寫(xiě)出Excel導(dǎo)出,Excel模板導(dǎo)出,Excel導(dǎo)入,Word模板導(dǎo)出。通過(guò)簡(jiǎn)單的注解和模板語(yǔ)言(熟悉的表達(dá)式語(yǔ)法),完成以前復(fù)雜的寫(xiě)法。
本文主要通過(guò)簡(jiǎn)單的分析讓讀者知道Excel模板該如何編寫(xiě),EasyPOI要如何使用才能導(dǎo)出滿(mǎn)足自己需要的Excel數(shù)據(jù),從而簡(jiǎn)化編碼。同時(shí)本文還會(huì)對(duì)一些不常見(jiàn)的功能如圖片導(dǎo)出功能進(jìn)行說(shuō)明,讓讀者少踩坑。
版本及依賴(lài)說(shuō)明
EasyPOI4.0.0及以后的版本依賴(lài)于Apache POI的4.0.0及以后版本。所以在maven的配置中,兩者的版本號(hào)一定要匹配。
需要注意的是,Apache POI的4.0.0相對(duì)之前的版本有很大的變更,如果之前代碼中Excel操作部分依賴(lài)于舊的版本,那么不建議使用4.0.0及之后的版本。當(dāng)然,如果之前代碼不涉及或很少涉及WorkBook的創(chuàng)建細(xì)節(jié),使用新版也沒(méi)有問(wèn)題。
筆者需要改寫(xiě)的項(xiàng)目基于JEECG 3.7版本,依賴(lài)的是3.9版本的Apache POI,而JEECG維護(hù)的jeasypoi版本最高只有2.2.0,而該版本并不支持模板導(dǎo)出圖片功能。說(shuō)到這里又要吐槽以下JEECG團(tuán)隊(duì),既然自己不打算維護(hù)jeasypoi,那項(xiàng)目中直接使用官方的EasyPOI不就好了,2.2.0版本的jeasypoi給開(kāi)發(fā)者挖了多少坑??!
為了和舊版本兼容,又想使用EasyPOI帶來(lái)的圖片導(dǎo)出功能,所以筆者最終采用的EasyPOI版本是3.3.0,對(duì)應(yīng)的Apache POI依賴(lài)是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è)計(jì)
我們使用EasyPOI的模板導(dǎo)出功能就是不想通過(guò)編碼的方式來(lái)設(shè)計(jì)Excel報(bào)表的樣式,所以工作的第一步就是設(shè)計(jì)Excel模板,分清楚哪些部分是固定的,哪些是需要循環(huán)填充的。EasyPOI有自己的表達(dá)式語(yǔ)言,每種表達(dá)式的詳細(xì)介紹請(qǐng)參考后文的參考鏈接。
一個(gè)簡(jiǎn)單的Excel報(bào)表模板
一些簡(jiǎn)單的模板就不在這里詳細(xì)解釋了,只放一下效果圖和模板配置內(nèi)容。等讀者明白了復(fù)雜的模板如何制作并如何填值的時(shí)候,簡(jiǎn)單的很快就能明白了。
先看看報(bào)表效果圖:
再看看實(shí)際的模板:
看了上述兩張圖,是不是已經(jīng)感受到模板導(dǎo)出功能的強(qiáng)大了呢?
一個(gè)復(fù)雜的Excel報(bào)表模板
下面要介紹的這個(gè)模板比較復(fù)雜,不像是常見(jiàn)的那種一行是一條記錄的情況,所以將詳細(xì)介紹該模板的配置,順帶對(duì)EasyPOI的部分表達(dá)式進(jìn)行簡(jiǎn)單介紹。
還是先看效果圖:
再看看模板:
這兩張圖一對(duì)比,是不是有種知識(shí)改變命運(yùn)的感覺(jué)?
復(fù)雜模板設(shè)計(jì)剖析
從貨品信息的模板圖及效果圖中我們發(fā)現(xiàn),整個(gè)模板實(shí)際上分為上下兩部分。其中上部分為不變的抬頭信息,下部分為循環(huán)插入的貨品明細(xì)信息。所以我們關(guān)注的重點(diǎn)是下半部分的語(yǔ)法。
下半部分的第一列圖中沒(méi)有顯示完整,實(shí)際上是{{!fe: list t.id。
注意,這里 沒(méi)有 }}符號(hào)!根據(jù)EasyPOI的官方文檔,{{}}代表的是表達(dá)式,根據(jù)表達(dá)式取里邊的值。仔細(xì)看圖可以發(fā)現(xiàn),表達(dá)式的閉合符號(hào){{}}出現(xiàn)在圖中的右下角。也就是說(shuō),從第一列{{開(kāi)始至右下角}}結(jié)束,這中間的所有內(nèi)容都是表達(dá)式的一部分。
因?yàn)檎麄€(gè)模板信息都是表達(dá)式的一部分,所以即使是普通字符串也需要專(zhuān)門(mén)標(biāo)明。下面對(duì)表達(dá)式中的子表達(dá)式進(jìn)行逐個(gè)說(shuō)明。
!fe: 遍歷數(shù)據(jù)不創(chuàng)建row。
官方文檔中的這句話(huà)大家理解起來(lái)可能有點(diǎn)費(fèi)解,什么叫不創(chuàng)建row?實(shí)際上,不創(chuàng)建row是相對(duì)于創(chuàng)建row而言的,創(chuàng)建row的表達(dá)式是fe:。
就像是數(shù)據(jù)庫(kù)中每條記錄對(duì)應(yīng)著一個(gè)實(shí)體對(duì)象,創(chuàng)建row表示每行就是一個(gè)實(shí)體對(duì)象Entity,這個(gè)實(shí)體對(duì)象的屬性用{{}}表達(dá)式包裹起來(lái)。
不創(chuàng)建row表示整個(gè)表達(dá)式中只有一個(gè)實(shí)體對(duì)象Object,只不過(guò)這個(gè)Object比較特別,它是由list中N個(gè)Entity拼接起來(lái)的。每一個(gè)Entity不僅僅是指模型本身,也包含了Excel的樣式,比如占用了幾個(gè)單元格,單元格的坐標(biāo)、排布順序等。
list 自定義的名稱(chēng),表示表達(dá)式中的數(shù)據(jù)集合,由代碼以list為鍵,從Map
中獲取值的集合。
list這個(gè)名字容易理解,就是一個(gè)占位符,可以隨便取。EasyPOI解析到list就知道Map
中存在著該鍵的值的集合,后邊解析到數(shù)據(jù)就從該集合中取即可。
搜索Java知音公眾號(hào),回復(fù)“后端面試”,送你一份Java面試題寶典.pdf
t 預(yù)定義值,表示集合中的任意對(duì)象。
從模板中我們大致能感覺(jué)到,list中每個(gè)對(duì)象叫做t,t.name就代表t的name屬性,所以t這個(gè)名字就可以隨便叫,反正它和list一樣,作用是占位符。
但實(shí)際上這是一個(gè)大坑!如果你把t換成了其他值比如g,模板中其他地方寫(xiě)g.name g.code等等,最終是解析不到的!官方文檔對(duì)這一點(diǎn)并沒(méi)有強(qiáng)調(diào),而是作者實(shí)際踩了坑之后才發(fā)現(xiàn)的!
]] 換行符 多行遍歷導(dǎo)出。
對(duì)于這個(gè)符號(hào)的官方解釋也是莫名其妙,什么叫換行符,多行遍歷導(dǎo)出?實(shí)際上它的意思就是,當(dāng)解析到表達(dá)式中含有這個(gè)符號(hào),該行后邊的內(nèi)容就不解析了,管你后邊有沒(méi)有其他內(nèi)容或者樣式。
該符號(hào)一定要寫(xiě)在每行的最后一列,不然會(huì)出現(xiàn)每行列數(shù)不一樣的情況,EasyPOI內(nèi)部做賦值的時(shí)候就會(huì)報(bào)空指針異常了。
‘’ 單引號(hào)表示常量值 ‘’ 比如’1’ 那么輸出的就是 1
官方文檔中這里的介紹也有坑。”是表示常量值,但實(shí)際上Excel中只是這么些是不對(duì)的,因?yàn)镋xcel的單元格中遇到’后會(huì)認(rèn)為后面都是字符串,所以得在單元格中寫(xiě)”庫(kù)別:’,這樣顯示出來(lái)的才是’庫(kù)別:’,而不是字符串庫(kù)別:’。
經(jīng)過(guò)上述分析,圖中的模板實(shí)際上就類(lèi)似以下內(nèi)容:
{{!fe: list t.id ‘庫(kù)別:’ t.bin 換行 ‘商品名稱(chēng):’ t.name 換行 ‘商品編號(hào):’ 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ì)模板。下面將對(duì)模板賦值進(jìn)行介紹。
準(zhǔn)備模板數(shù)據(jù)
從上節(jié)的描述中可知,只需要準(zhǔn)備一個(gè)Map
的對(duì)象即可,其中鍵為list,值為一個(gè)List數(shù)組,數(shù)組中元素類(lèi)型為Map
。代碼如下:
Map<String, Object> total = new HashMap<>(); List<Map<String, Object>> mapList = new ArrayList<>(); for (int i = 1; i <= 5; i++) { Map<String, Object> 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);
圖片數(shù)據(jù)導(dǎo)出
上述代碼中需要特殊關(guān)注的是圖片導(dǎo)出部分。EasyPOI導(dǎo)出圖片有兩種方式,一種是通過(guò)圖片的Url,還有一種是獲取圖片的byte[],畢竟圖片的本質(zhì)就是byte[]。因?yàn)楣P者的項(xiàng)目中圖片不是存放在數(shù)據(jù)庫(kù)之中,而是需要根據(jù)查詢(xún)結(jié)果動(dòng)態(tài)生成條碼,所以通過(guò)byte[]導(dǎo)出圖片。
ImageEntity是EasyPOI內(nèi)置的一個(gè)JavaBean,用于設(shè)定圖片的寬度和高度、導(dǎo)出方式、RowSpan和ColumnSpan等。調(diào)試EasyPOI的源碼可知,當(dāng)設(shè)置了RowSpan或者ColumnSpan之后,圖片的高度設(shè)置就失效了,圖片大小會(huì)自動(dòng)填充圖片所在的單元格。
圖片導(dǎo)出的坑點(diǎn)在于導(dǎo)出圖片的大小。假設(shè)我們將四個(gè)單元格合成為一個(gè),希望導(dǎo)出的圖片能填充合并之后的單元格,但是對(duì)不起,EasyPOI暫時(shí)做不到,它只會(huì)填充合并之前左上角的單元格,具體原因如下源碼所示:
//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è)置也會(huì)失效,最終導(dǎo)致導(dǎo)出的圖片非常小。
搜索Java知音公眾號(hào),回復(fù)“后端面試”,送你一份Java面試題寶典.pdf
個(gè)人認(rèn)為ImageEntity提供的RowSpan或者ColumnSpan的set方法并沒(méi)有什么用,因?yàn)槲覀儎?dòng)態(tài)創(chuàng)建的合并單元格并不能被賦值。所以,導(dǎo)出圖片的最好方式就是直接指定它的高度,因?yàn)閷挾葧?huì)自動(dòng)填充單元格,模板中單元格的寬度要合適。
//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)行組裝了,或者說(shuō)是渲染,代碼如下所示:
public static void exportByTemplate(String templateName, Map<String, Object> 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)上針對(duì)EasyPOI的介紹多限于最基本的行插入功能,但實(shí)際上Excel模板的需求可能各式各樣。本文只是拋磚引玉,對(duì)EasyPOI中的部分概念做了詳細(xì)介紹,希望幫助大家少踩坑。
如果想詳細(xì)了解EasyPOI的各種功能,參考鏈接中的文檔說(shuō)明及測(cè)試項(xiàng)目源碼就是最好的學(xué)習(xí)資料。希望大家都能得心應(yīng)手地使用EasyPOI,大大提升開(kāi)發(fā)效率!
參考鏈接
EasyPOI官方文檔
-
https://opensource.afterturn.cn/doc/easypoi.html
EasyPOI測(cè)試項(xiàng)目
-
https://gitee.com/lemur/easypoi-test
一些坑
近日有網(wǎng)友求助我解決EasyPOI的復(fù)雜模板配置問(wèn)題,通過(guò)解決該網(wǎng)友的問(wèn)題發(fā)現(xiàn)了EasyPOI中的幾個(gè)坑點(diǎn),補(bǔ)充說(shuō)明幾個(gè)問(wèn)題。
在復(fù)雜模板設(shè)計(jì)剖析一節(jié)中已經(jīng)描述了EasyPOI支持的復(fù)雜的模板該如何配置。該模板的配置是絕對(duì)正確的,但是有3個(gè)點(diǎn)沒(méi)有說(shuō)清楚,大家在照葫蘆畫(huà)瓢時(shí)容易出錯(cuò):
-
{{!fe: list需要在一個(gè)單獨(dú)的列中。EasyPOI源碼中是根據(jù)該單元格的行、列跨度來(lái)決定list中的每個(gè)元素需要多少行的。比如上述圖片中,該單元格的跨度是5行1列,也就是說(shuō),以后list中的每個(gè)元素都會(huì)占用5行。如果覺(jué)得該列不符合自定義模板的風(fēng)格,可以把該列的列寬設(shè)置為0,但一定需要有{{!fe: list。
-
在對(duì)象的起始和結(jié)束符號(hào){{}}之間不能有任何空的單元格!代碼中在解析到該單元格為空時(shí)會(huì)直接拋異常,如果就希望該單元格為空,得顯示寫(xiě)入空字符串:’’’。
-
換行符]]必須占用每行的最后一個(gè)單元格!比如說(shuō)第一行有10個(gè)單元格,第二行只用了前5個(gè),那么不能直接在第5個(gè)結(jié)束直接寫(xiě)換行符]],而是需要把6-10個(gè)單元格合并,然后寫(xiě)入]]。參考上述圖片中生產(chǎn)日期所在行的最后一列。這么設(shè)置的原因是EasyPOI要求每行的單元格數(shù)目完全一致,因?yàn)樵创a中判斷了每個(gè)單元格的列跨度,如果提前使用了]]換行符,那么該列的數(shù)目就和其他行不同,那么賦值的時(shí)候就亂掉了,會(huì)出現(xiàn)索引異常。
以上就是使用 EasyPOI 優(yōu)雅導(dǎo)出Excel模板數(shù)據(jù)(含圖片)的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注有卡有網(wǎng)