freemark+dom4j實現自動化word匯出
阿新 • • 發佈:2020-05-25
> 匯出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#if>
<#list c as c>
dosomethings()
#list>
```
- 上面兩段程式碼就是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#if>
```
### 部分原始碼
```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