freemark+dom4j實現自動化word匯出

煙花散盡13141發表於2020-05-25

匯出word我們常用的是通過POI實現匯出。POI最擅長的是EXCEL的操作。word操作起來樣式控制還是太繁瑣了。今天我們介紹下通過FREEMARK來實現word模板匯出。

開發準備

  • 本文實現基於springboot,所以專案中採用的都是springboot衍生的產品。首先我們在maven專案中引入freemark座標。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

  • 只需要引入上面的jar包 。 前提是繼承springboot座標。就可以通過freemark進行word的匯出了。

模板準備

  • 上面是我們匯出的一份模板。填寫規則也很簡單。只需要我們提前準備一份樣本文件,然後將需要動態修改的通過${}進行佔位就行了。我們匯出的時候提供相應的資料就行了。這裡注意一下${c.no}這種格式的其實是我們後期為了做集合遍歷的。這裡先忽略掉。後面我們會著重介紹。

開發測試

  • 到了這一步說明我們的前期準備就已經完成了。剩下我們就通過freemark就行方法呼叫匯出就可以了。

  • 首先我們構建freemark載入路徑。就是設定一下freemark模板路徑。模板路徑中存放的就是我們上面編寫好的模板。只不過這裡的模板不是嚴格意義的word.而是通過word另存為xml格式的檔案。

  • 配置載入路徑
//建立配置例項
Configuration configuration = new Configuration();
//設定編碼
configuration.setDefaultEncoding("UTF-8");
//ftl模板檔案
configuration.setClassForTemplateLoading(OfficeUtils.class, "/template");

  • 獲取模板類

Template template = configuration.getTemplate(templateName);

  • 構建輸出物件

Writer out = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));

  • 匯出資料到out

template.process(dataMap, out);

  • 就上面四步驟我們就可以實現匯出了。我們可以將載入配置路徑的放到全域性做一次。剩下也就是我們三行程式碼就可以搞定匯出了。當然我們該做的異常捕獲這些還是需要的。點我獲取原始碼

結果檢測

功能通用化思考

  • 上面我們只是簡單介紹一下freemark匯出word的流程。關於細節方面我們都沒有進行深究。
  • 細心的朋友會發現上面的圖片並沒有進行動態的設定。這樣子功能上肯定是說不過去的。圖片我們想生成我們自己設定的圖片。
  • 還有一個細節就是核取方塊的問題。仔細觀察會發現核取方塊也沒有欄位去控制。肯定也是沒有辦法進行動態勾選的。
  • 最後就是我們上面提到的就是主要安全措施那塊。那塊是我們的集合資料。通過模板我們是沒法控制的。
  • 上面的問題我們freemark的word模板是無法實現的。有問題其實是好事。這樣我們才能進步。實際上freemark匯出真正是基於ftl格式的檔案的。只不過xml和ftl語法很像所以上面我們才說匯出模板是xml的。實際上我們需要的ftl檔案。如果是ftl檔案那麼上面的問題的核取方塊和集合都很好解決了。一個通過if標籤一個通過list標籤就可以解決了。圖片我們還是需要通過人為去替換

<#if checkbox ??&& checkbox?seq_contains('窒息;')?string('true','false')=='true'>0052<#else>00A3</#if>

<#list c as c>
dosomethings()
</#list>

  • 上面兩段程式碼就是if 和 list語法

Dom4j實現智慧化

  • 上面ftl雖然解決了匯出的功能問題。但是還是不能實現智慧化。我們想做的其實想通過程式自動根據我們word的配置去進行生成ftl檔案。經過百度終究還是找到了對應的方法。Dom4j就是我們最終方法。我們可以通過在word進行特殊編寫。然後程式通過dom4j進行節點修改。通過dom4j我們的圖片問題也就迎刃而解了。下面主要說說針對以上三個問題的具體處理細節

核取方塊

  • 首先我們約定同一型別的核取方塊前需要#{}格式編寫。裡面就是控制核取方塊的欄位名。
  • 然後我們通過dom4j解析xml。我們再看看核取方塊原本的格式在xml中
<w:sym w:font="Wingdings 2" w:char="0052"/>

  • 那麼我們只需要通過dom4j獲取到w:sym標籤。在獲取到該標籤後對應的文字內容即#{zhuyaoweihaiyinsu}窒息;這個內容。
  • 匹配出欄位名zhuyaoweihaiyinsu進行if標籤控制內容

<#if checkbox ??&& checkbox?seq_contains('窒息')?string('true',false')=='true'>0052<#else>00A3</#if>

部分原始碼


Element root = document.getRootElement();
List<Element> checkList = root.selectNodes("//w:sym");
List<String> nameList = new ArrayList<>();
Integer indext = 1;
for (Element element : checkList) {
    Attribute aChar = element.attribute("char");
    String checkBoxName = selectCheckBoxNameBySymElement(element.getParent());
    aChar.setData(chooicedCheckBox(checkBoxName));
}

集合

  • 同樣的操作我們通過獲取到需要改變的標籤就可以了。集合和核取方塊不一樣。集合其實是我們認為規定出來的一種格式。在word中並沒有特殊標籤標示。所以我們約定的格式是${a_b}。首先我們通過遍歷word中所以文字通過正則驗證是否符合集合規範。符合我們獲取到當前的行然後在行標籤前新增#list標籤。 然後將\({a_b}修改成\){a.b} 至於為什麼一開始不設定a.b格式的。我這裡只想說是公司文化導致的。我建議搭建如果是自己實現這一套功能的話採用a.b格式最好。

部分原始碼


Element root = document.getRootElement();
    //需要獲取所有標籤內容,判斷是否符合
    List<Element> trList = root.selectNodes("//w:t");
    //rowlist用來處理整行資料,因為符合標準的會有多列, 多列在同一行只需要處理一次。
    List<Element> rowList = new ArrayList<>();
    if (CollectionUtils.isEmpty(trList)) {
        return;
    }
    for (Element element : trList) {
        boolean matches = Pattern.matches(REGEX, element.getTextTrim());
        if (!matches) {
            continue;
        }
        //符合約定的集合格式的才會走到這裡
        //提取出tableId 和columnId
        Pattern compile = Pattern.compile(REGEX);
        Matcher matcher = compile.matcher(element.getTextTrim());
        String tableName = "";
        String colName = "";
        while (matcher.find()) {
            tableName = matcher.group(1);
            colName = matcher.group(2);
        }
        //此時獲取的是w:t中的內容,真正需要迴圈的是w:t所在的w:tr,這個時候我們需要獲取到當前的w:tr
        List<Element> ancestorTrList = element.selectNodes("ancestor::w:tr[1]");
        /*List<Element> tableList = element.selectNodes("ancestor::w:tbl[1]");
        System.out.println(tableList);*/
        Element ancestorTr = null;
        if (!ancestorTrList.isEmpty()) {
            ancestorTr = ancestorTrList.get(0);
            //獲取表頭資訊
            Element titleAncestorTr = DomUtils.getInstance().selectPreElement(ancestorTr);
            if (!rowList.contains(ancestorTr)) {
                rowList.add(ancestorTr);
                List<Element> foreachList = ancestorTr.getParent().elements();
                if (!foreachList.isEmpty()) {
                    Integer ino = 0;
                    Element foreach = null;
                    for (Element elemento : foreachList) {
                        if (ancestorTr.equals(elemento)) {
                            //此時ancestorTr就是需要遍歷的行 , 因為我們需要將此標籤擴容到迴圈標籤匯中
                            foreach = DocumentHelper.createElement("#list");
                            foreach.addAttribute("name", tableName+" as "+tableName);
                            Element copy = ancestorTr.createCopy();
                            replaceLineWithPointForeach(copy);
                            mergeCellBaseOnTableNameMap(titleAncestorTr,copy,tableName);
                            foreach.add(copy);
                            break;
                        }
                        ino++;
                    }
                    if (foreach != null) {
                        foreachList.set(ino, foreach);
                    }
                }
            } else {
                continue;
            }
        }
    }

圖片

  • 圖片和核取方塊類似。因為在word的xml中是通過特殊標籤處理的。但是我們的佔位符不能通過以上佔位符佔位了。需要一張真實的圖片進行佔位。因為只有是一張圖片word才會有圖片標籤。我們可以在圖片後通過@{imgField}進行佔位。然後通過dom4j將圖片的base64位元組碼用${imgField}佔位。

部分原始碼


//圖片索引下表
Integer index = 1;
//獲取根路徑
Element root = document.getRootElement();
//獲取圖片標籤
List<Element> imgTagList = root.selectNodes("//w:binData");
for (Element element : imgTagList) {
    element.setText(String.format("${img%s}",index++));
    //獲取當前圖片所在的wp標籤
    List<Element> wpList = element.selectNodes("ancestor::w:p");
    if (CollectionUtils.isEmpty(wpList)) {
        throw new DomException("未知異常");
    }
    Element imgWpElement = wpList.get(0);
    while (imgWpElement != null) {
        try {
            imgWpElement = DomUtils.getInstance().selectNextElement(imgWpElement);
        } catch (DomException de) {
            break;
        }
        //獲取對應圖片欄位
        List<Element> imgFiledList = imgWpElement.selectNodes("w:r/w:t");
        if (CollectionUtils.isEmpty(imgFiledList)) {
            continue;
        }
        String imgFiled = getImgFiledTrimStr(imgFiledList);
        Pattern compile = Pattern.compile(REGEX);
        Matcher matcher = compile.matcher(imgFiled);
        String imgFiledStr = "";
        while (matcher.find()) {
            imgFiledStr = matcher.group(1);
            boolean remove = imgWpElement.getParent().elements().remove(imgWpElement);
            System.out.println(remove);
        }
        if (StringUtils.isNotEmpty(imgFiledStr)) {
            element.setText(String.format("${%s}",imgFiledStr));
            break;
        }
    }

}

基於word自動化匯出(含原始碼)

參考網路文章

dom操作xml
dom生成xml
httpclient獲取反應流
獲取jar路徑
itext實現套打
ftl常見語法
freemark官網
ftl判斷非空
freemark自定義函式
freemark自定義函式java
freemark特殊字元轉義
java實現word轉xml各種格式

加入戰隊

# 加入戰隊

微信公眾號

微信公眾號

相關文章