如何通過 Freemark 優雅地生成那些花裡胡哨的複雜樣式 Excel 檔案?
歡迎關注個人微信公眾號: 小哈學Java, 文末分享阿里 P8 高階架構師吐血總結的 《Java 核心知識整理&面試.pdf》資源連結!!
個人網站: https://www.exception.site/essay/how-to-create-complex-style-excel-with-freemark
一、背景
小哈最近這段時間開始負責一個新的產品:下載中心。啥玩意這是?
產品的目的其實就是統一管控各業務組檔案下載功能(包括一些海量資料的匯出,檔案合併上傳等),專案組不用自己再去實現各式各樣的檔案(PDF, Word, Excel)生成, 統一對接下載中心,由下載中心統一完成檔案的生成、合併、上傳、下載流程。
問題來了,這裡麵包括一些複雜檔案的生成,如帶有複雜樣式的 Excel 檔案,比如下面這個樣子的:
這種複雜樣式的 Excel, 如果說放到各個業務線去實現還是好辦的,因為站在各個業務組的角度,場景變化不會太多,按照檔案格式,程式碼寫死即可。
但是站在下載中心的角度,因為需要對接各個業務中心,每個業務中心生成的樣式都不一樣,不可能每個業務組接進來,我都得定製的寫一套生成程式碼吧!這顯然也不合常理!
那麼,有沒有什麼一勞永逸的辦法呢?答案是肯定的!
二、實現思路
要說實現方式,你的腦海裡可能第一會想到傳統的 Apache poi,jxl ,亦或者是阿里出品 EasyExcel 等等。
PS: 關於阿里的 EasyExcel, 小哈之前有分享過 ,沒看過的小夥伴們,可以看下《7 行程式碼優雅地實現 Excel 檔案生成&下載功能》。
對於這種複雜樣式,要是用 Apache poi, jxl, 阿里 EasyExcel 去實現,不可避免的,程式碼肯定會非常繁瑣。
有沒有啥優雅(偷懶的)的方式呢?
其實我們可以通過檢視引擎 Freemark、Velocity 來幫我們生成複雜樣式 Excel 檔案,無需關心花裡胡哨的複雜樣式,只關注於填充資料即可。接下來,我們以 Freemark 作為示例來講解,如何生成這個複雜樣式的 Excel 檔案。
拓展閱讀: 什麼是 Freemark ?
FreeMarker 是一款 模板引擎: 即一種基於模板和要改變的資料, 並用來生成輸出文字(HTML網頁,電子郵件,配置檔案,原始碼等)的通用工具。 它不是面向終端使用者的,而是一個Java類庫,是一款程式設計師可以嵌入他們所開發產品的元件。
其實,對於Java 後端來說,它更常被用來服務端動態渲染 html 頁面返回給瀏覽器。前些年還比較火熱,近些年因為前後端分離的火熱,也開始慢慢淡出視野了。
三、快速上手
3.1 新增依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
注意: 小哈這裡基於 Spring Boot 寫的測試程式碼,版本號可以無需指定,否則,你需要手動指定好版本號。
3.2 匯出 xml 模板檔案
首先,將複雜樣式的 Excel 檔案另存為 .xml
檢視模板,如下圖所示:
開啟 xml 模板檔案,可以清晰的看到裡面定義了各種節點,節點描述了整個 Excel 的樣式結構, 如下圖所示:
3.3 填充佔位符
再回過頭來看下之前那個複雜 Excel 檔案, 觀察一下哪些單元格的值需要動態設定:
圖中用紅色特意標註出來了。
在剛剛另存為的 xml 模板檔案中填寫 freemark 表示式,考慮到這裡只是個示例 Demo, 僅僅選取幾個示例單元格來填寫佔位符,如下所示:
訂單標題:
其他需要動態填充的單元格:
PS: xml 檔案中,
<Row>
節點代表一行,<Cell>
代表一個單元格。
在需要動態填充資料的地方,加上相關 freemark 表示式,如 ${commodity.name!}
,如下所示:
<Row ss:AutoFitHeight="0" ss:Height="54">
<Cell ss:StyleID="s18"><Data ss:Type="String">1</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.name!}</Data></Cell>
<Cell ss:StyleID="s18"/>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.num!}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.num1!}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">22</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">44</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">55</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">盒</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price!}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price2!}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price3!}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.timestamp}</Data></Cell>
<Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.createTime?string('yyyy-MM-dd HH:mm:ss')}</Data></Cell>
<Cell ss:StyleID="s18"/>
</Row>
按照服務端資料模型的定義,填寫好相應的欄位名稱,再對照下後臺 Commodity 商品類的定義:
這個商品類中,我們定義了不同型別的欄位,如 String、int、Integer、Double、Float、
金額型別 BigDecimal
、日期型別 Date
等,用以測試對不同資料型別的相容性。
確認相關屬性欄位名無誤後,再來看下 freemark 生成 Excel 的核心程式碼:
package site.exception.springbootfreemarkexcel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import site.exception.springbootfreemarkexcel.entity.Commodity;
import java.io.File;
import java.io.FileWriter;
import java.math.BigDecimal;
import java.util.*;
/**
* @author 犬小哈 (微信公眾號: 小哈學Java)
* @site www.exception.site
* @date 2019/5/21
* @time 上午10:57
* @discription
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootFreemarkExcelApplicationTests {
@Test
public void createExcelByFreemark() throws Exception {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
// 設定模板檔案的父目錄
configuration.setDirectoryForTemplateLoading(new File("/Users/a123123/Work/tmp_files"));
// 載入模板檔案
Template template = configuration.getTemplate("/excel_template.xml", "UTF-8");
// 資料準備,可以是從資料庫中查詢,這裡為了方便演示,手動 new 了
Map<String, Object> data = new HashMap<>();
data.put("title", "測試標題1");
List<Commodity> commodities = new ArrayList<>();
Commodity commodity = Commodity.builder()
.name("name1").num(11).num1(111)
.price(new BigDecimal(11.1)).price2(new Double(11.11))
.price3(new Float(11.1111))
.createTime(new Date())
.timestamp(System.currentTimeMillis())
.build();
commodities.add(commodity);
// 生成 excel 檔案
template.process(data, new FileWriter("/Users/a123123/Work/tmp_files/excelByFreemark.xls"));
}
}
可以看到生成複雜樣式的 Excel 的程式碼非常簡潔。關於每一行程式碼什麼意思,註釋已經說得很清楚了,這裡就不加以說明了。
執行單元測試,看下效果:
完美,在需要填充內容的地方都已經動態設定上了內容。
四、多行資料如何生成?
如何做到動態生成多行呢?其實也很簡單,重新開啟剛剛修改的 xml 模板檔案,在需要動態生成多行的地方,新增 freemark 迴圈表示式即可:
PS: 關於 Freemark 更多表達式的使用,小夥伴們可以自行在各大搜索引擎中搜索,因為如何使用 Freemark 不是本文關注的重點~
上圖中,我們對後臺的 commodities
欄位做了迴圈,所以對應的,後臺程式碼也需要做相關修改:
我們在 commodities
中添加了兩個商品物件。趕快程式碼跑起來,看看效果!
別急,還有個地方需要做下修改,不然會報錯!!
找到 <Table>
節點,有個屬性叫 ExpandedRowCount
, 它定義了表格行的總數,如果數值與實際的行數對應不上的話,會出問題。
這裡我們新增 Freemark 表示式,總行數為商品 commodites
集合的大小加上 16, 注意:16 為除了動態生成的行數外,固定不變的行數大小,小夥伴們如果使用的是不同的 xml 模板,需要自行確認好這個數值的大小。
修改完了以後,再次執行單元測試,效果如下:
OK! 大功告成!
五、侷限性
通過檢視解析器來生成 Excel 的確很優雅(偷懶),同時兼具靈活性。但是它同樣存在一些侷限性!小夥伴們在技術選型時,需要結合實際的業務場景審視它是否適合。
- 版本問題:
目前個人測試結果是,在 MAC 系統上僅支援生成 03 版本 Excel, 07 版本存在打不開的情況;
- 無法寫入大批量資料:
檢視引擎生成檔案無法往 Excel 裡面追加資料,所以僅僅適用於資料量不大的個性化 Excel 生成,否則寫入大批量資料時,存在記憶體溢位(OOM)的情況發生;
- MAC 系統存在生成的 Excel 檔案無法編輯儲存的情況:
小哈在測試中發現,生成 excel 在 MAC 系統上存在編輯後,無法儲存的情況;而 Windows 系統 Microsoft Excel 和 WPS 均能夠正常編輯儲存;
六、總結
本文中,小哈給大家介紹瞭如何通過檢視引擎優雅的生成 Excel 檔案,演示了相關示例程式碼,以及它的相關侷限性,希望大家看完本文後能夠有所收穫,下期見喲~
GitHub 示例程式碼
- https://github.com/weiwosuoai/spring-boot-tutorial/tree/master/spring-boot-freemark-excel
免費分享 | 面試&學習福利資源
最近在網上發現一個不錯的 PDF 資源《Java 核心知識&面試.pdf》分享給大家,不光是面試,學習,你都值得擁有!!!
獲取方式: 關注公眾號: 小哈學Java, 後臺回覆資源,既可免費無套路獲取資源連結,下面是目錄以及部分截圖:
重要的事情說兩遍,關注公眾號: 小哈學Java, 後臺回覆資源,既可免費無套路獲取資源連結 !!