前言
最近有讀者在問(wèn)easypoi的問(wèn)題,抽空整理了一篇文章。
正文
EasyPOI功能如同名字Easy,主打的功能就是容易,讓一個(gè)沒(méi)接觸過(guò)POI的人員可以方便的寫(xiě)出Excel導(dǎo)出,Excel模板導(dǎo)出,Excel導(dǎo)入,Word範(fàn)本匯出。透過(guò)簡(jiǎn)單的註解和模板語(yǔ)言(熟悉的表達(dá)式語(yǔ)法),完成先前複雜的寫(xiě)法。
本文主要透過(guò)簡(jiǎn)單的分析讓讀者知道Excel模板該如何編寫(xiě),EasyPOI要如何使用才能導(dǎo)出滿(mǎn)足自己需要的Excel數(shù)據(jù),從而簡(jiǎn)化編碼。同時(shí)本文也會(huì)對(duì)一些不常見(jiàn)的功能如圖片匯出功能進(jìn)行說(shuō)明,讓讀者少踩坑。
版本及相依說(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,而該版本並不支援模板匯出圖片功能。說(shuō)到這裡又要吐槽以下JEECG團(tuán)隊(duì),既然自己不打算維護(hù)jeasypoi,那專(zhuān)案中直接使用官方的EasyPOI不就好了,2.2.0版本的jeasypoi給開(kāi)發(fā)者挖了多少坑??!
為了和舊版相容,又想使用EasyPOI帶來(lái)的圖片匯出功能,所以筆者最終採(cǎi)用的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的模板匯出功能就是不想透過(guò)編碼的方式來(lái)設(shè)計(jì)Excel報(bào)表的樣式,所以工作的第一步就是設(shè)計(jì)Excel模板,分清楚哪些部分是固定的,哪些是需要循環(huán)填入的。 EasyPOI有自己的表達(dá)式語(yǔ)言,每種表達(dá)式的詳細(xì)介紹請(qǐng)參考後文的參考連結(jié)。
一個(gè)簡(jiǎn)單的Excel報(bào)表範(fàn)本
#一些簡(jiǎn)單的範(fàn)本就不在這裡詳細(xì)解釋了,只放一下效果圖和模板配置內(nèi)容。等讀者明白了複雜的模板如何製作並如何填值的時(shí)候,簡(jiǎn)單的很快就能明白了。
先看看報(bào)表效果圖:
再看看實(shí)際的範(fàn)本:
看了上述兩張圖,是不是已經(jīng)感受到模板導(dǎo)出功能的強(qiáng)大了呢?
一個(gè)複雜的Excel報(bào)表範(fàn)本
下面要介紹的這個(gè)範(fàn)本比較複雜,不像是常見(jiàn)的那種一行是一筆記錄的情況,所以將詳細(xì)介紹該範(fàn)本的配置,順帶對(duì)EasyPOI的部分錶達(dá)式進(jìn)行簡(jiǎn)單介紹。
還是先看效果圖:
再看看範(fàn)本:
複雜範(fàn)本設(shè)計(jì)剖析
#從貨品資訊的範(fàn)本圖及效果圖中我們發(fā)現(xiàn),整個(gè)範(fàn)本實(shí)際上分為上下兩部分。其中上部分為不變的抬頭訊息,下部分為循環(huán)插入的貨品明細(xì)訊息。所以我們關(guān)注的重點(diǎn)是下半部的文法。 下半部的第一列圖中沒(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è)範(fàn)本資訊都是表達(dá)式的一部分,所以即使是普通字串也需要專(zhuān)門(mén)標(biāo)明。下面將表達(dá)式中的子表達(dá)式逐一說(shuō)明。
!fe: 遍歷資料不建立row。
官方文件中的這句話(huà)大家理解起來(lái)可能有點(diǎn)費(fèi)解,什麼叫不創(chuàng)建row?實(shí)際上,不建立row是相對(duì)於建立row而言的,建立row的表達(dá)式是fe:。
就像是資料庫(kù)中每筆記錄對(duì)應(yīng)著一個(gè)實(shí)體對(duì)象,創(chuàng)建row表示每行就是一個(gè)實(shí)體對(duì)象Entity,這個(gè)實(shí)體對(duì)象的屬性用{{}}表達(dá)式包裹起來(lái)。
不建立row表示整個(gè)表達(dá)式中只有一個(gè)實(shí)體物件Object,只不過(guò)這個(gè)Object比較特別,它是由list中N個(gè)Entity拼接起來(lái)的。每一個(gè)Entity不只是指模型本身,也包含了Excel的樣式,例如佔(zhàn)用了幾個(gè)單元格,單元格的座標(biāo)、排布順序等。
list 自訂的名稱(chēng),表示表達(dá)式中的資料集合,由程式碼以list為鍵,從Map
list這個(gè)名字很容易理解,就是一個(gè)佔(zhàn)位符,可以隨便取。 EasyPOI解析到list就知道Map
搜尋Java知音公眾號(hào),回覆“後端面試”,送你一份Java面試題寶典.pdf
t 預(yù)定義值,表示集合中的任意物件。
從模板中我們大致可以感覺(jué)到,list中每個(gè)物件叫做t,t.name就代表t的name屬性,所以t這個(gè)名字就可以隨便叫,反正它和list一樣,作用是佔(zhàn)位符。
但其實(shí)這是一個(gè)大坑!如果你把t換成了其他值例如g,模板中其他地方寫(xiě)g.name g.code等等,最後是解析不到的!官方文件對(duì)這一點(diǎn)並沒(méi)有強(qiáng)調(diào),而是作者實(shí)際踩了坑之後才發(fā)現(xiàn)的!
]] 換行符號(hà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)空指標(biāo)異常了。
‘’ 單引號(hào)表示常數(shù)值 ‘’ 例如’1’ 那麼輸出的就是 1
##官方文件中這裡的介紹也有坑。 ''是表示常數(shù)值,但實(shí)際上Excel中只是這麼些是不對(duì)的,因?yàn)镋xcel的單元格中遇到'後會(huì)認(rèn)為後面都是字符串,所以得在單元格中寫(xiě)''庫(kù)別:' ,這樣顯示出來(lái)的才是'庫(kù)別:',而不是字串庫(kù)別:'。 經(jīng)過(guò)上述分析,圖中的範(fàn)本其實(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
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)題。
在複雜模板設(shè)計(jì)剖析一節(jié)中已經(jīng)描述了EasyPOI支援的複雜的模板該如何配置。此模板的配置絕對(duì)是正確的,但有3個(gè)點(diǎn)沒(méi)有說(shuō)清楚,大家在照葫蘆畫(huà)瓢時(shí)容易出錯(cuò):
{!fe: list需要在一個(gè)單獨(dú)的列中。 EasyPOI原始碼中是根據(jù)該儲(chǔ)存格的行、列跨距來(lái)決定list中的每個(gè)元素需要多少行的。例如上述圖片中,該儲(chǔ)存格的跨距是5行1列,也就是說(shuō),以後list中的每個(gè)元素都會(huì)佔(zhàn)用5行。如果覺(jué)得該列不符合自訂模板的風(fēng)格,可以把該列的列寬設(shè)為0,但一定需要有{{!fe: list。 在物件的起始和結(jié)束符號(hào){{}}之間不能有任何空的單元格!程式碼中在解析到該單元格為空時(shí)會(huì)直接拋異常,如果就希望該單元格為空,則得顯示寫(xiě)入空字串:’’’。 換行符]]必須佔(zhàn)用每行的最後一個(gè)儲(chǔ)存格!比如說(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)雅匯出Excel模板資料(含圖片)的詳細(xì)內(nèi)容。更多資訊請(qǐng)關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

熱AI工具

Undress AI Tool
免費(fèi)脫衣圖片

Undresser.AI Undress
人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線(xiàn)上人工智慧工具。

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門(mén)文章

熱工具

記事本++7.3.1
好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

Dreamweaver CS6
視覺(jué)化網(wǎng)頁(yè)開(kāi)發(fā)工具

SublimeText3 Mac版
神級(jí)程式碼編輯軟體(SublimeText3)