我是如何使用freemarker生成Word文件的?

  • 2020 年 9 月 14 日
  • 筆記

推薦:親身體驗,數次踩坑,遂撰寫此文,以備各位不時之需。

背景

一天,產品經理遞給我了一份word報告,我定睛一看

這個文檔有大大小小的標題層級,還有排版好的段落、各種一目了然的餅圖、走勢圖,當然還少不了顏色循環交替的報表。精緻程度不亞於小明同學的學習報告。

準備

魯迅:身為一名Java程式設計師,任何時候都不要忘記站在巨人的肩膀上。


通過某歌搜索關鍵詞:java+word+導出,我立馬得出了很多成熟的方案,通過橫向、縱向比較,再結合本次報告樣式比較多、用戶可靈活選擇不同模組導出的特點,最終,我決定使用Freemarker 動態替換模版數據來導出word文檔。至於導出文檔的最終格式,有兩種選擇:

那到底使用doc還是docx格式的文檔?
每當人生當中每次面臨選擇我都很慎重。最終我選擇使用docx格式(原因文末會講),但是為了讓大家有更多的選擇,滿足更多的業務場景,藉此機會,小明會給大家分別介紹使用freemarker導出兩種格式的word文檔方式。

思路

FreeMarker是一個基於Java的模板引擎,最初專註於使用MVC軟體架構生成動態網頁。但是,它是一個通用的模板引擎,不依賴於servlets或HTTP或HTML,因此它通常還用於生成源程式碼,配置文件或電子郵件。

此時,我們用它動態生成xml文件,進而導出word文檔。

整體流程如下:

準備

  • WPS

由金山軟體股份有限公司發布,用於辦公軟體最常用的文字編輯、表格、演示稿等功能。

對,就是這個國產的辦公軟體。我也是第一次發現在導出文檔這件事上,它如多年好友般友好。(word解析後的xml文件閱讀性很強,一般人我不告訴他)

  • 開發工具(IDEA、Visual Studio Code等)
    你喜歡的,順手的,就是最好的。

實現

集成Freemarker模版引擎

本次項目使用的框架依舊是Springboot,這個框架在集成各個組件表現都很便捷,不再贅述,這次集成Freemarker也不例外。

  • 首先我們在項目中增添依賴spring-boot-starter-freemarker
    pom.xml文件如下所示:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
  • 按照默認約定,我們可以在resources下創建一個templates文件夾(查看FreeMarkerProperties源碼可以發現默認目錄就是這個),用於存放模版文檔。
  • application.yml增加配置
spring:
  freemarker:
    template-loader-path: classpath:/templates
    cache: false # 開發環境快取關閉
    suffix: xml
    charset: UTF-8

生成doc格式的文檔

這裡先拿使用freemarker導出doc格式的word文檔舉例。

  • 首先將docxTemplate.docx(調整好樣式的模版文檔)另存為WORD 2003 XML文檔(*.xml)

    此處命名為docTemplete.xml,使用編輯工具首次打開時,會發現這個文檔裡面是壓縮的xml,因此我們首先需要格式化一下。

注意:如果你使用的是Visual Studio Code開發工具,一定要檢查你所使用的xml格式化插件,是否會優化你的xml標籤 。比如:<w:rPr>會變成<rPr>。使用Visual Studio Code的同學,oh my god ! 小明在這裡推薦大家使用這個插件:XML Language Support by Red Hat

  • 現在,我們就使用freemarker語法編輯docTemplete.xml,比如使用佔位符${}替換當前文檔中的文本,以達到動態生成文本的目的,直接上程式碼。
public static Configuration getConfiguration(){
        //創建配置實例
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
        //設置編碼
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassForTemplateLoading(WordUtil.class, "/templates");
        return configuration;
}

    /**
     * 生成doc文件
     *
     * @param ftlFileName 模板ftl文件的名稱
     * @param params      動態傳入的數據參數
     * @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格式的文檔

高能預警! 在成功使用Freemarker動態導出doc格式的文檔之後,相信大家和我的心情一樣非常激動。但以上操作只是一個小鋪墊,接下來我們來看看如何實現docx格式的文檔導出,小明相信一定會讓各位看官大跌眼鏡!不,大開眼界!
首先,告訴大家一個秘密:docx格式的文檔其實是一個ZIP格式的壓縮文件哦! 什麼?你不信?驗證如下:

  • windows的小夥伴
    將docx文檔修改為ZIP格式(修改.docx後綴名為.zip),然後通過解壓工具解壓。
  • MacOS的小夥伴
    直接使用unzip命令解壓word文檔,解壓過後我們會發現該文檔其實還有自己的目錄結構


    當然,這麼多文件我們不必一一知悉,只需關注小明紅線標註的文件和目錄即可:
  • document.xml文件用於存放核心數據,文字,表格,圖片引用等
  • media目錄用於存放所有文檔的圖片
  • _rels目錄下的document.xml.rels里存放的是配置資訊,比如圖片引用關係,即在document.xml中引用id對應media中的哪個圖片。
  • 獲取zip里的document.xml文檔以及_rels文件夾下的document.xml.rels文檔
  • 顯而易見,如果我們要想根據數據動態導出不同的word文檔,只需要:通過freemarker將本次數據填充到document.xml中,並將圖片配置資訊填充至document.xml.rels文檔里,再用文件流把本次圖片寫入到media目錄下替換已經存在的圖片,最後把填充過內容的document.xml、document.xml.rels以及media用流的方式寫入zip即可輸出docx文檔!上程式碼。

好吧,限於篇幅,程式碼見文末 Github地址

問題及解決方案

當然,大家在第一次嘗試去干某一件事時,都不一定是一蹴而就的。就比如在導出word時,就可能會遇到以下問題。​

特殊字元

問題:有些文本數據中難免含有特殊字元,如:< > @ ! $ & 等等。

解決方案:這些特殊字元如果不進行轉義,就會引起word打不開的現象,比如表格中的超鏈接的&符號,就需要替換為&amp;,如果你的文檔用office打開時提示文件損壞,九成是因為特殊符號引起的,我們可以打開documet.xml定位報錯位置;當然還有終極方案,我們可以利用Freemarker的語法直接在模板中使用<![CDATA[ ]]> 處理。比如:

 <w:t><![CDATA[ ${article.title} ]]></w:t>

圖片變形

問題:因為echarts生成的圖表是響應式的,不同的螢幕大小、解析度,會造成每次前端傳過來的圖片寬高比例不一致,如果還直接將圖片按照之前的比例放進文檔,會造成生成後文檔中的圖片變形。

思路:首先將文檔中的圖片設置為原圖,然後鎖定寬高比,將圖片調整到合適大小,解壓文檔從document.xml,得到此時word中該圖片寬高對應的值,如下所示:

要想保證不同像素比例的寬高在文檔中不變形,我們需要固定cy的值,然後根據固定比例動態求得當前像素比例圖片在word中代表的寬cx的值。計算方法如下所示:
公式:

a/b = x/y

其中,a表示圖片在word中寬的數值,b代表圖片在word中高的數值,x表示前端傳過來圖片的寬(單位:像素),y表示前端傳過來圖片的高(單位:像素)。因此,已知b、x、y,根據公式,我們即可求出a;

我就是文末

當然,還有用一些其他注意事項:

  • 如果word中的模組比較多的話,使用Freemarker語法要仔細一點;
  • 為什麼小明最終選擇導出docx格式的文檔呢?(還不是因為產品經理的需求嘛)因為doc格式的文檔,小明嘗試導出後,發現該文檔並不是一個合法的doc文檔,體現在:不能在手機上(微信、釘釘)正常預覽,office提示以xml形式打開等。因此在導出doc文檔時,通過Freemaker填充document.xml後得到的並不是一個合法的word文檔,查了相關資料,還需要藉助第三方工具進行簽名,而簽名還需要在windows系統下才能完成,但是我們平時用的生產環境都是Linux……因此,考慮再三,再三權衡,最終選擇導出docx格式的文檔。這種方式再適合不過,而且還能保證在當前主流APP上都能正常預覽。
  • 敲黑板!導出docx文檔最重要的一個思想是將本次數據寫入覆蓋模版文件(在商業中,相當於借殼上市),重新輸出一個zip格式壓縮的文件,這個文件就是我們最終想要的文檔。

以上,就是小明word導出的前前後後,如果你也曾經遇到過或者現在正好遇到word文檔導出開發的問題,歡迎一起討論交流。

相關鏈接

我上傳了工具類,包含doc、docx 的導出,以及導出word文檔時特殊符號轉義,還有圖片Base64轉換成文件輸出的方法。
GitHub地址://github.com/WhenCoding/coder-xiaoming/blob/master/src/main/java/com/xm/coder/util/WordUtil.java

本文可轉載,但需聲明原文出處。 程式設計師小明,一個很少加班的程式設計師。歡迎關注微信公眾號,獲取更多優質文章。

Exit mobile version