我是如何使用freemarker生成Word文件的?
背景
一天,產(chǎn)品經(jīng)理遞給我了一份word報(bào)告,我定睛一看

這個(gè)文檔有大大小小的標(biāo)題層級(jí),還有排版好的段落、各種一目了然的餅圖、走勢(shì)圖,當(dāng)然還少不了顏色循環(huán)交替的報(bào)表。精致程度不亞于小明同學(xué)的學(xué)習(xí)報(bào)告。
準(zhǔn)備
魯迅:身為一名Java程序員,任何時(shí)候都不要忘記站在巨人的肩膀上。

通過(guò)某歌搜索關(guān)鍵詞:java+word+導(dǎo)出,我立馬得出了很多成熟的方案,通過(guò)橫向、縱向比較,再結(jié)合本次報(bào)告樣式比較多、用戶可靈活選擇不同模塊導(dǎo)出的特點(diǎn),最終,我決定使用Freemarker 動(dòng)態(tài)替換模版數(shù)據(jù)來(lái)導(dǎo)出word文檔。至于導(dǎo)出文檔的最終格式,有兩種選擇:

那到底使用doc還是docx格式的文檔?
每當(dāng)人生當(dāng)中每次面臨選擇我都很慎重。最終我選擇使用docx格式(原因文末會(huì)講),但是為了讓大家有更多的選擇,滿足更多的業(yè)務(wù)場(chǎng)景,借此機(jī)會(huì),小明會(huì)分別給大家介紹使用freemarker導(dǎo)出word文檔兩種格式的方式。
思路
FreeMarker是一個(gè)基于Java的模板引擎,最初專注于使用MVC軟件架構(gòu)生成動(dòng)態(tài)網(wǎng)頁(yè)。但是,它是一個(gè)通用的模板引擎,不依賴于servlets或HTTP或HTML,因此它通常還用于生成源代碼,配置文件或電子郵件。
此時(shí),我們用它動(dòng)態(tài)生成xml文件,進(jìn)而導(dǎo)出word文檔。

整體流程如下:
準(zhǔn)備
WPS
由金山軟件股份有限公司發(fā)布,用于辦公軟件最常用的文字編輯、表格、演示稿等功能。
對(duì),就是這個(gè)國(guó)產(chǎn)的辦公軟件。除了它強(qiáng)制登錄以外,我也是第一次發(fā)現(xiàn)在導(dǎo)出文檔這件事上,它如多年好友般友好。(word解析后的xml文件閱讀性很強(qiáng),一般人我不告訴他)
開(kāi)發(fā)工具(IDEA、Visual Studio Code等) 你喜歡的,順手的,就是最好的。
實(shí)現(xiàn)
集成Freemarker模版引擎
本次項(xiàng)目使用的框架依舊是Springboot,這個(gè)框架在集成各個(gè)組件表現(xiàn)都很便捷,不再贅述,這次集成Freemarker也不例外。
首先我們?cè)陧?xiàng)目中增添依賴 spring-boot-starter-freemarkerpom.xml文件如下所示:
<dependency>
????<groupId>org.springframework.bootgroupId>
????<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
按照默認(rèn)約定,我們可以在resources下創(chuàng)建一個(gè)templates文件夾(查看FreeMarkerProperties源碼可以發(fā)現(xiàn)默認(rèn)目錄就是這個(gè)),用于存放模版文檔。 
application.yml增加配置
spring:
??freemarker:
????template-loader-path:?classpath:/templates
????cache:?false?#?開(kāi)發(fā)環(huán)境緩存關(guān)閉
????suffix:?xml
????charset:?UTF-8
生成doc格式的文檔
這里先拿使用freemarker導(dǎo)出doc格式的word文檔舉例。
首先將docxTemplate.docx(調(diào)整好樣式的模版文檔)另存為WORD 2003 XML文檔(*.xml) 
此處命名為docTemplete.xml,使用編輯工具首次打開(kāi)時(shí),會(huì)發(fā)現(xiàn)這個(gè)文檔里面是壓縮的xml,因此我們首先需要格式化一下。
注意:如果你使用的是Visual Studio Code開(kāi)發(fā)工具,一定要檢查你所使用的xml格式化插件,是否會(huì)優(yōu)化你的xml標(biāo)簽 。比如:
會(huì)變成。使用Visual Studio Code的同學(xué),oh my god ! 小明在這里推薦大家使用這個(gè)插件:XML Language Support by Red Hat
現(xiàn)在,我們就使用freemarker語(yǔ)法編輯docTemplete.xml,比如使用占位符 ${}替換當(dāng)前文檔中的文本,以達(dá)到動(dòng)態(tài)生成文本的目的,直接上代碼。
public?static?Configuration?getConfiguration(){
????????//創(chuàng)建配置實(shí)例
????????Configuration?configuration?=?new?Configuration(Configuration.VERSION_2_3_28);
????????//設(shè)置編碼
????????configuration.setDefaultEncoding("utf-8");
????????configuration.setClassForTemplateLoading(WordUtil.class,?"/templates");
????????return?configuration;
}
????/**
?????*?生成doc文件
?????*
?????*?@param?ftlFileName?模板ftl文件的名稱
?????*?@param?params??????動(dòng)態(tài)傳入的數(shù)據(jù)參數(shù)
?????*?@param?outFilePath?生成的最終doc文件的保存完整路徑
?????*/
????public?void?ftlToDoc(String?ftlFileName,?Map?params,?String?outFilePath)?{
????????try?{
????????????/**?加載模板文件?**/
????????????Template?template?=?configuration.getTemplate(ftlFileName);
????????????/**?指定輸出word文件的路徑?**/
????????????File?docFile?=?new?File(outFilePath);
????????????FileOutputStream?fos?=?new?FileOutputStream(docFile);
????????????Writer?bufferedWriter?=?new?BufferedWriter(new?OutputStreamWriter(fos,?"utf-8"),?10240);
????????????template.process(params,?bufferedWriter);
????????????if?(bufferedWriter?!=?null)?{
????????????????bufferedWriter.close();
????????????}
????????}?catch?(TemplateException?e)?{
????????????e.printStackTrace();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}
????}
生成docx格式的文檔
高能預(yù)警! 在成功使用Freemarker動(dòng)態(tài)導(dǎo)出doc格式的文檔之后,相信大家和我的心情一樣非常激動(dòng)。但以上操作只是一個(gè)小鋪墊,接下來(lái)我們來(lái)看看如何實(shí)現(xiàn)docx格式的文檔導(dǎo)出,小明相信一定會(huì)讓各位看官大跌眼鏡!不,大開(kāi)眼界!首先,告訴大家一個(gè)秘密:docx格式的文檔其實(shí)是一個(gè)ZIP格式的壓縮文件哦! 什么?你不信?驗(yàn)證如下:
windows的小伙伴 將docx文檔修改為ZIP格式(修改.docx后綴名為.zip),然后通過(guò)解壓工具解壓。 MacOS的小伙伴 直接使用 unzip命令解壓word文檔,解壓過(guò)后我們會(huì)發(fā)現(xiàn)該文檔其實(shí)還有自己的目錄結(jié)構(gòu)!
當(dāng)然,這么多文件我們不必一一知悉,只需關(guān)注小明紅線標(biāo)注的文件和目錄即可: document.xml文件用于存放核心數(shù)據(jù),文字,表格,圖片引用等 media目錄用于存放所有文檔的圖片 _rels目錄下的document.xml.rels里存放的是配置信息,比如圖片引用關(guān)系,即在document.xml中引用id對(duì)應(yīng)media中的哪個(gè)圖片。 獲取zip里的document.xml文檔以及_rels文件夾下的document.xml.rels文檔 顯而易見(jiàn),如果我們要想根據(jù)數(shù)據(jù)動(dòng)態(tài)導(dǎo)出不同的word文檔,只需要:通過(guò)freemarker將本次數(shù)據(jù)填充到document.xml中,并將圖片配置信息填充至document.xml.rels文檔里,再用文件流把本次圖片寫入到media目錄下替換已經(jīng)存在的圖片,最后把填充過(guò)內(nèi)容的document.xml、document.xml.rels以及media用流的方式寫入zip即可輸出docx文檔!上代碼。
好吧,限于篇幅,代碼見(jiàn)文末 Github地址
問(wèn)題及解決方案
當(dāng)然,大家在第一次嘗試去干某一件事時(shí),都不一定是一蹴而就的。就比如在導(dǎo)出word時(shí),就可能會(huì)遇到以下問(wèn)題。
特殊字符
問(wèn)題:有些文本數(shù)據(jù)中難免含有特殊字符,如:< > @ ! $ & 等等。
解決方案:這些特殊字符如果不進(jìn)行轉(zhuǎn)義,就會(huì)引起word打不開(kāi)的現(xiàn)象,比如表格中的超鏈接的&符號(hào),就需要替換為&,如果你的文檔用office打開(kāi)時(shí)提示文件損壞,九成是因?yàn)樘厥夥?hào)引起的,我們可以打開(kāi)documet.xml定位報(bào)錯(cuò)位置;當(dāng)然還有終極方案,我們可以利用Freemarker的語(yǔ)法直接在模板中使用 處理。比如:
?${article.title}?]]>
圖片變形
問(wèn)題:因?yàn)閑charts生成的圖表是響應(yīng)式的,不同的屏幕大小、分辨率,會(huì)造成每次前端傳過(guò)來(lái)的圖片寬高比例不一致,如果還直接將圖片按照之前的比例放進(jìn)文檔,會(huì)造成生成后文檔中的圖片變形。
思路:首先將文檔中的圖片設(shè)置為原圖,然后鎖定寬高比,將圖片調(diào)整到合適大小,解壓文檔從document.xml,得到此時(shí)word中該圖片寬高對(duì)應(yīng)的值,如下所示:

要想保證不同像素比例的寬高在文檔中不變形,我們需要固定cy的值,然后根據(jù)固定比例動(dòng)態(tài)求得當(dāng)前像素比例圖片在word中代表的寬cx的值。計(jì)算方法如下所示:公式:
a/b?=?x/y
其中,a表示圖片在word中寬的數(shù)值,b代表圖片在word中高的數(shù)值,x表示前端傳過(guò)來(lái)圖片的寬(單位:像素),y表示前端傳過(guò)來(lái)圖片的高(單位:像素)。因此,已知b、x、y,根據(jù)公式,我們即可求出a;
我就是文末
當(dāng)然,還有用一些其他注意事項(xiàng):
如果word中的模塊比較多的話,使用Freemarker語(yǔ)法要仔細(xì)一點(diǎn); 為什么小明最終選擇導(dǎo)出docx格式的文檔呢?(還不是因?yàn)楫a(chǎn)品經(jīng)理提的需求嘛)因?yàn)閐oc格式的文檔,小明嘗試導(dǎo)出后,發(fā)現(xiàn)該文檔并不是一個(gè)合法的doc文檔,體現(xiàn)在:不能在手機(jī)上(微信、釘釘)正常預(yù)覽,office提示以xml形式打開(kāi)等。因此在導(dǎo)出doc文檔時(shí),通過(guò)Freemaker填充document.xml后得到的并不是一個(gè)合法的word文檔,查了相關(guān)資料,還需要借助第三方工具進(jìn)行簽名,而簽名還需要在windows系統(tǒng)下才能完成,但是我們平時(shí)用的生產(chǎn)環(huán)境都是Linux……因此,考慮再三,再三權(quán)衡,最終選擇導(dǎo)出docx格式的文檔。這種方式再適合不過(guò),而且還能保證在當(dāng)前主流APP上都能正常預(yù)覽。 敲黑板!導(dǎo)出docx文檔最重要的一個(gè)思想是將本次數(shù)據(jù)寫入并覆蓋模版文件(在商業(yè)中,相當(dāng)于借殼上市),重新輸出一個(gè)zip格式壓縮的文件,這個(gè)文件就是我們最終想要的文檔。
以上,就是小明本次word導(dǎo)出的前前后后,現(xiàn)在已經(jīng)在線上平穩(wěn)運(yùn)行數(shù)十個(gè)日夜。如果你也曾經(jīng)遇到過(guò)或者現(xiàn)在正好遇到word文檔導(dǎo)出開(kāi)發(fā)的問(wèn)題,歡迎一起討論交流。
相關(guān)鏈接
我上傳了工具類,包含doc、docx 的導(dǎo)出,以及導(dǎo)出word文檔時(shí)特殊符號(hào)轉(zhuǎn)義,還有圖片Base64轉(zhuǎn)換成文件輸出的方法。
GitHub地址:https://github.com/WhenCoding/coder-xiaoming/blob/master/src/main/java/com/xm/coder/util/WordUtil.java
