freemark+dom4j实现自动化word导出
suiw9 2024-12-01 04:01 16 浏览 0 评论
> 导出word我们常用的是通过POI实现导出。POI最擅长的是EXCEL的操作。word操作起来样式控制还是太繁琐了。今天我们介绍下通过FREEMARK来实现word模板导出。
[TOC]
# 开发准备
- 本文实现基于springboot,所以项目中采用的都是springboot衍生的产品。首先我们在maven项目中引入freemark坐标。
```xml
<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);
```
- 就上面四步骤我们就可以实现导出了。我们可以将加载配置路径的放到全局做一次。剩下也就是我们三行代码就可以搞定导出了。当然我们该做的异常捕获这些还是需要的。[点我获取源码](https://gitee.com/zxhTom/office-multip.git)
# 结果检测

# 功能通用化思考
- 上面我们只是简单介绍一下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>
```
### 部分源码
```java
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格式最好。 ### 部分源码
```java
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}占位。
### 部分源码
```java
//图片索引下表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](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)
# # <span id="addMe">加入战队</span>
## 微信公众号

相关推荐
- 10款超实用JavaScript音频库(js播放音频代码)
-
HTML5提供了一种新的音频标签实现和规范用一个简单的HTML对象而无需音频插件来控制音频。这只是一个简单的整合这些新的HTML5音频特征及使用JavaScript来创建各种播放控制。下面将介绍10款...
- PROFINET转Modbus网关——工业协议融合的智能枢纽
-
三格电子SG-PNh750-MOD-221,无缝连接Profinet与Modbus,赋能工业物联产品概述...
- 简单实用的Modbus类库,支持从站和DTU
-
一、简介...
- [西门子PLC] S7-200 SMART PROFINET :通过GSD组态PLC设备
-
从S7-200SMARTV2.5版本开始,S7-200SMART开始支持做PROFINETIO通信的智能设备。从而,两个S7-200SMART之间可以进行PROFINETI...
- Modbus(RTU / TCP)有什么异同(modbus tcp和tcp)
-
Modbus是一种广泛使用的工业自动化通信协议,它支持设备之间的数据交换。Modbus协议有两个主要的变体:ModbusRTU(二进制模式)和ModbusTCP(基于TCP/IP网络的模式)。尽管...
- Modbus通信调试步骤详解(modbus调试工具怎么用)
-
Modbus通信调试步骤详解 Modbus通信分为串口和以太网,无论是串口还是以太网,只要是标准Modbus,就可以用Modbus模拟器进行调试。按以下几步进行调试。...
- 理解Intel手册汇编指令(intel 汇编指令手册)
-
指令格式...
- 「西门子PLC」S7-200 SMART的Modbus RTU通讯
-
S7-200SMART集成的RS485端口(端口0)以及SBCM01RS485/232信号板(端口1)两个通信端口可以同时做MODBUSRTU主站,或者一个做MODBUSRTU主站一个做MO...
- InfiniBand网络运维全指南:从驱动安装到故障排查
-
一、InfiniBand网络概述InfiniBand(直译为“无限带宽”技术,缩写为IB)是一种用于高性能计算的计算机网络通信标准,具有极高的吞吐量和极低的延迟,用于计算机与计算机之间的数据互连。它...
- 一加回归 OPPO,背后的秘密不可告人
-
有这样一个手机品牌,它诞生于互联网品牌。在大众群体看来,它的身世似乎模糊不清,许多人以为它是国外品牌。它的产品定位是极客群体,深受国内发烧友,甚至国外极客玩家喜爱。...
- [西门子PLC] S7-200SMART快速高效的完成Modbus通信程序的设计
-
一、导读Modbus通信是一种被广泛应用的通信协议,在变频器、智能仪表还有其他一些智能设备上都能见到它的身影。本文呢,就把S7-200SMART系列PLC当作Modbus主站,把...
- 狂肝10个月手搓GPU,他们在我的世界中玩起我的世界,梦想成真
-
梦晨衡宇萧箫发自凹非寺量子位|公众号QbitAI自从有人在《我的世界》里用红石电路造出CPU,就流传着一个梗:...
- [西门子PLC] 博途TIA portal SCL编程基础入门:1-点动与自锁
-
一、S7-SCL编程语言简介...
- 工作原理系列之:Modbus(modbus工作过程)
-
MODBUS是一种在自动化工业中广泛应用的高速串行通信协议。该协议是由Modion公司(现在由施耐德电气公司获得)于1979年为自己的可编程逻辑控制器开发的。该协议充当了PLCS和智能自动化设备之间的...
你 发表评论:
欢迎- 一周热门
-
-
Linux:Ubuntu22.04上安装python3.11,简单易上手
-
宝马阿布达比分公司推出独特M4升级套件,整套升级约在20万
-
MATLAB中图片保存的五种方法(一)(matlab中保存图片命令)
-
别再傻傻搞不清楚Workstation Player和Workstation Pro的区别了
-
Linux上使用tinyproxy快速搭建HTTP/HTTPS代理器
-
如何提取、修改、强刷A卡bios a卡刷bios工具
-
Element Plus 的 Dialog 组件实现点击遮罩层不关闭对话框
-
MacOS + AList + 访达,让各种云盘挂载到本地(建议收藏)
-
日本组合“岚”将于2020年12月31日停止团体活动
-
SpringCloud OpenFeign 使用 okhttp 发送 HTTP 请求与 HTTP/2 探索
-
- 最近发表
-
- 10款超实用JavaScript音频库(js播放音频代码)
- Howler.js,一款神奇的 JavaScript 开源网络音频工具库
- PROFINET转Modbus网关——工业协议融合的智能枢纽
- 简单实用的Modbus类库,支持从站和DTU
- [西门子PLC] S7-200 SMART PROFINET :通过GSD组态PLC设备
- Modbus(RTU / TCP)有什么异同(modbus tcp和tcp)
- Modbus通信调试步骤详解(modbus调试工具怎么用)
- 理解Intel手册汇编指令(intel 汇编指令手册)
- 「西门子PLC」S7-200 SMART的Modbus RTU通讯
- InfiniBand网络运维全指南:从驱动安装到故障排查
- 标签列表
-
- dialog.js (57)
- importnew (44)
- windows93网页版 (44)
- yii2框架的优缺点 (45)
- tinyeditor (45)
- qt5.5 (60)
- windowsserver2016镜像下载 (52)
- okhttputils (51)
- android-gif-drawable (53)
- 时间轴插件 (56)
- docker systemd (65)
- slider.js (47)
- android webview缓存 (46)
- pagination.js (59)
- loadjs (62)
- openssl1.0.2 (48)
- velocity模板引擎 (48)
- pcre library (47)
- zabbix微信报警脚本 (63)
- jnetpcap (49)
- pdfrenderer (43)
- fastutil (48)
- uinavigationcontroller (53)
- bitbucket.org (44)
- python websocket-client (47)