freemark+dom4j實現自動化word導出


導出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各種格式

加入戰隊

# 加入戰隊

微信公眾號

微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM