1. 程式人生 > >freemark+dom4j實現自動化word匯出

freemark+dom4j實現自動化word匯出

> 匯出word我們常用的是通過POI實現匯出。POI最擅長的是EXCEL的操作。word操作起來樣式控制還是太繁瑣了。今天我們介紹下通過FREEMARK來實現word模板匯出。 [TOC] # 開發準備 - 本文實現基於springboot,所以專案中採用的都是springboot衍生的產品。首先我們在maven專案中引入freemark座標。 ```xml org.springframework.boot spring-boot-starter-freemarker ``` - 只需要引入上面的jar包 。 前提是繼承springboot座標。就可以通過freemark進行word的匯出了。 ## 模板準備 ![](http://oytmxyuek.bkt.clouddn.com/20200525001.jpg) - 上面是我們匯出的一份模板。填寫規則也很簡單。只需要我們提前準備一份樣本文件,然後將需要動態修改的通過`${}`進行佔位就行了。我們匯出的時候提供相應的資料就行了。這裡注意一下`${c.no}`這種格式的其實是我們後期為了做集合遍歷的。這裡先忽略掉。後面我們會著重介紹。 ## 開發測試 - 到了這一步說明我們的前期準備就已經完成了。剩下我們就通過freemark就行方法呼叫匯出就可以了。 - 首先我們構建freemark載入路徑。就是設定一下freemark模板路徑。模板路徑中存放的就是我們上面編寫好的模板。只不過這裡的模板不是嚴格意義的word.而是通過word另存為xml格式的檔案。 ![](http://oytmxyuek.bkt.clouddn.com/20200525002.jpg) - 配置載入路徑 ``` //建立配置例項 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); ``` - 就上面四步驟我們就可以實現匯出了。我們可以將載入配置路徑的放到全域性做一次。剩下也就是我們三行程式碼就可以搞定匯出了。當然我們該做的異常捕獲這些還是需要的。[點我獲取原始碼](https://gitee.com/zxhTom/office-multip.git) # 結果檢測 ![](http://oytmxyuek.bkt.clouddn.com/20200525003.jpg) # 功能通用化思考 - 上面我們只是簡單介紹一下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
<#list c as c> dosomethings() ``` - 上面兩段程式碼就是if 和 list語法 # Dom4j實現智慧化 - 上面ftl雖然解決了匯出的功能問題。但是還是不能實現智慧化。我們想做的其實想通過程式自動根據我們word的配置去進行生成ftl檔案。經過百度終究還是找到了對應的方法。Dom4j就是我們最終方法。我們可以通過在word進行特殊編寫。然後程式通過dom4j進行節點修改。通過dom4j我們的圖片問題也就迎刃而解了。下面主要說說針對以上三個問題的具體處理細節 ## 複選框 ![](http://oytmxyuek.bkt.clouddn.com/20200525checkbox.jpg) - 首先我們約定同一型別的複選框前需要`#{}`格式編寫。裡面就是控制複選框的欄位名。 - 然後我們通過dom4j解析xml。我們再看看複選框原本的格式在xml中 ```
``` - 那麼我們只需要通過dom4j獲取到w:sym標籤。在獲取到該標籤後對應的文字內容即#{zhuyaoweihaiyinsu}窒息;這個內容。 - 匹配出欄位名zhuyaoweihaiyinsu進行if標籤控制內容 ``` <#if checkbox ??&& checkbox?seq_contains('窒息')?string('true',false')=='true'>0052<#else>00A3 ``` ### 部分原始碼 ```java Element root = document.getRootElement(); List checkList = root.selectNodes("//w:sym"); List nameList = new ArrayList<>(); Integer indext = 1; for (Element element : checkList) { Attribute aChar = element.attribute("char"); String checkBoxName = selectCheckBoxNameBySymElement(element.getParent()); aChar.setData(chooicedCheckBox(checkBoxName)); } ``` ## 集合 ![](http://oytmxyuek.bkt.clouddn.com/20200525list.jpg) - 同樣的操作我們通過獲取到需要改變的標籤就可以了。集合和複選框不一樣。集合其實是我們認為規定出來的一種格式。在word中並沒有特殊標籤標示。所以我們約定的格式是`${a_b}`。首先我們通過遍歷word中所以文字通過正則驗證是否符合集合規範。符合我們獲取到當前的行然後在行標籤前新增#list標籤。 然後將${a_b}修改成${a.b} 至於為什麼一開始不設定a.b格式的。我這裡只想說是公司文化導致的。我建議搭建如果是自己實現這一套功能的話採用a.b格式最好。 ### 部分原始碼 ```java Element root = document.getRootElement(); //需要獲取所有標籤內容,判斷是否符合 List trList = root.selectNodes("//w:t"); //rowlist用來處理整行資料,因為符合標準的會有多列, 多列在同一行只需要處理一次。 List 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 ancestorTrList = element.selectNodes("ancestor::w:tr[1]"); /*List 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 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; } } } ``` ## 圖片 ![](http://oytmxyuek.bkt.clouddn.com/20200525img.jpg) - 圖片和複選框類似。因為在word的xml中是通過特殊標籤處理的。但是我們的佔位符不能通過以上佔位符佔位了。需要一張真實的圖片進行佔位。因為只有是一張圖片word才會有圖片標籤。我們可以在圖片後通過`@{imgField}`進行佔位。然後通過dom4j將圖片的base64位元組碼用${imgField}佔位。 ### 部分原始碼 ```java //圖片索引下表 Integer index = 1; //獲取根路徑 Element root = document.getRootElement(); //獲取圖片標籤 List imgTagList = root.selectNodes("//w:binData"); for (Element element : imgTagList) { element.setText(String.format("${img%s}",index++)); //獲取當前圖片所在的wp標籤 List 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 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自動化匯出(含原始碼) - 以上就是我們實現匯出的流程。通過上面的邏輯我們最終可以一套程式碼複用了。原始碼下載地址:https://gitee.com/zxhTom/office-multip.git ###### 參考網路文章 [dom操作xml](https://www.cnblogs.com/alsf/p/9278816.html) [dom生成xml](https://www.cnblogs.com/it-mh/p/11021716.html) [httpclient獲取反應流](https://blog.csdn.net/qw222pzx/article/details/97884917) [獲取jar路徑](https://blog.csdn.net/liangcha007/article/details/88526181) [itext實現套打](https://blog.csdn.net/flyfeifei66/article/details/6739950) [ftl常見語法](https://www.cnblogs.com/zhaoYuQing-java2015/p/6046697.html) [freemark官網](https://freemarker.apache.org/docs/ref_directive_list.html) [ftl判斷非空](https://www.iteye.com/blog/lj6684-1594769) [freemark自定義函式](https://blog.csdn.net/weixin_34174422/article/details/91867563) [freemark自定義函式java](https://blog.csdn.net/hzgzf/article/details/83399351) [freemark特殊字元轉義](https://blog.csdn.net/arsenic/article/details/8490098) [java實現word轉xml各種格式](https://www.cnblogs.com/Yesi/p/11195732.html) [加入戰隊](#addMe) # # 加入戰隊 ## 微信公眾號 ![微信公眾號](http://oytmxyuek.bkt.clouddn.com/weixi