导出 word 我们常用的是通过 POI 实现导出。POI 最擅长的是 EXCEL 的操作。word 操作起来样式控制还是太繁琐了。今天我们介绍下通过 FREEMARK 来实现 word 模板导出。
[TOC]
# 开发准备
- 本文实现基于 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/offi…
参考网络文章
dom 操作 xml
dom 生成 xml
httpclient 获取反应流
获取 jar 路径
itext 实现套打
ftl 常见语法
freemark 官网
ftl 判断非空
freemark 自定义函数
freemark 自定义函数 java
freemark 特殊字符转义
java 实现 word 转 xml 各种格式
加入战队
<span id=”addMe”> 加入战队 </span>
微信公众号