導出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自動化導出(含源碼)
- 以上就是我們實現導出的流程。通過上面的邏輯我們最終可以一套代碼復用了。源碼下載地址:https://gitee.com/zxhTom/office-multip.git
參考網絡文章
dom操作xml
dom生成xml
httpclient獲取反應流
獲取jar路徑
itext實現套打
ftl常見語法
freemark官網
ftl判斷非空
freemark自定義函數
freemark自定義函數java
freemark特殊字符轉義
java實現word轉xml各種格式