近期参与的多个项目中,均涉及根据预定义模板生成Word文档以供前端下载的需求。以往,我们通常采用将Word文档转换为XML格式,并通过代码赋值变量的方式来实现这一功能。尽管此方法在技术层面可行,但当面对篇幅较长且包含大量变量的文档时,其弊端便显露无遗:代码冗长繁杂,模板维护困难,不利于后续的修改与扩展。
鉴于此,近期对市场上现有的解决方案进行了深入调研,旨在寻找一种更为高效、简洁的Word文档生成方式。经过综合评估,推荐以下两款优秀的组件,旨在为开发者提供更为便捷的开发体验。
方案 | 移植性 | 功能性 | 易用性 |
---|---|---|---|
XDocReport | Java跨平台 | 支持多种文档格式,强大的模板引擎,易于集成 | 模板与代码分离,易于管理与修改,适用于复杂文档,处理速度快 |
Poi-tl | Java跨平台 | 轻量级模板引擎,专注于Word文档生成,简洁易用 | 模板语法简洁,降低维护成本,针对Word文档优化,性能稳定 |
Apache POI | Java跨平台 | Apache项目,封装了常见的文档操作,也可以操作底层XML结构 | 文档不全,这里有一个教程:Apache POI Word快速入门 |
Freemarker | XML跨平台 | 需要手动转换Word为XML,代码量大 | 模板与代码紧密耦合,修改复杂,变量多时长文档生成效率低 |
OpenOffice | 部署OpenOffice,移植性较差 | - | 需要了解OpenOffice的API |
HTML浏览器导出 | 依赖浏览器的实现,移植性较差 | HTML不能很好的兼容Word的格式,样式糟糕 | - |
Jacob、winlib | Windows平台 | - | 复杂,完全不推荐使用 |
综上所述,XDocReport与Poi-tl两款组件在Word文档生成方面均表现出色,各有千秋。XDocReport功能全面,适用于大型企业级应用;而Poi-tl则以其轻量级、简洁易用的特点,更适合中小型项目及快速迭代开发场景。开发者可根据项目实际需求选择合适的组件,以提升开发效率与代码质量。
一、XDocReport
1. 简介
xdocreport是一个基于Apache POI和Velocity/Freemarker的Java库,主要用于生成和处理各种文档格式,如DOCX、ODT、PDF等。它通过模板引擎语法(如Freemarker、Velocity)将数据动态插入到文档中,支持多种格式的转换和文档生成。
2. 主要功能
- 支持多种模板引擎,如 Velocity、Freemarker 和 Mustache。
- 支持表格、图表、页眉和页脚等复杂布局。
- 支持在 Word、Excel 和 PowerPoint 文档中插入图片。
- 支持在 Excel 文档中创建数据透视表。
- 支持在 Word 文档中创建目录。
- 支持在 Word 文档中创建书签和超链接。
- 支持在 Word 文档中创建水印。
- 支持在 Word 文档中创建页码。
- 支持在 Word 文档中创建分节符。
- 支持在 Word 文档中创建页面背景。
3.基本原理
4. 快速开始
4.1 引入依赖
<dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.core</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.document</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.template</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.document.docx</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId> <version>2.0.6</version> </dependency>
4.2 创建模板
插入能被正常替换的占位符是模板的核心,创建占位符:插入 - 文档部件 - 域 - 邮件合并 - 域代码 - 确定
插入域:Ctrl + F9
显示域:Alt + F9
4.3 生成文档
/** * 使用 xdocreport 生成 word * 一共需要5步,其中只有第4步需要开发者自行实现 */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); context.put("name", "张三"); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } }
5. Demo演示
5.1 文本输出
import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; /** * 文本输出 * 创建域 占位符使用 ${var} * 域的创建方式:插入 - 文档部件 - 域 - 邮件合并 - 域代码 - 确定 * 注意:占位符中的 MERGEFIELD 不可删除 * 会保留模板中的样式 * @author dafeng * @date 2024/12/27 9:59 */ @Slf4j public class Demo2 { // 模板路径 private static final String TEMP_PATH = "D:\deployment\test\xdoc-report\demo2-temp.docx"; // 输出文档路径 private static final String OUT_PATH = "D:\deployment\test\xdoc-report\demo2-out.docx"; /** * 使用 xdocreport 生成 word */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); // 填充变量 setContext(context); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } } /** * 自定义变量填充 */ private static void setContext(IContext context) { context.put("name", "张三"); } }
5.2 对象输出
import com.xajw.xdocreport.vo.User; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Date; import java.util.HashMap; /** * 复杂对象 * ${var.v} ${var.v.x} * * @author dafeng * @date 2024/12/27 9:59 */ @Slf4j public class Demo3 { // 模板路径 private static final String TEMP_PATH = "D:\deployment\test\xdoc-report\demo3-temp.docx"; // 输出文档路径 private static final String OUT_PATH = "D:\deployment\test\xdoc-report\demo3-out.docx"; /** * 使用 xdocreport 生成 word */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); // 填充变量 setContext(context); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } } /** * 自定义变量填充 */ private static void setContext(IContext context) { User user = new User("张三", 18, new Date(), new User.Address("中国", "陕西", "西安")); context.put("user", user); HashMap<String, Object> map = new HashMap<String, Object>(){{ put("name", "李四"); put("age", 19); put("birthday", new Date()); put("address", new User.Address("中国", "四川", "成都")); }}; context.put("map", map); } }
5.3 列表循环
import com.xajw.xdocreport.vo.User; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 列表循环 * [#list varList as var] ${var} [/#list] * 占位符需要有开始和结束 * * @author dafeng * @date 2024/12/27 9:59 */ @Slf4j public class Demo4 { // 模板路径 private static final String TEMP_PATH = "D:\deployment\test\xdoc-report\demo4-temp.docx"; // 输出文档路径 private static final String OUT_PATH = "D:\deployment\test\xdoc-report\demo4-out.docx"; /** * 使用 xdocreport 生成 word */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); // 填充变量 setContext(context); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } } /** * 自定义变量填充 */ private static void setContext(IContext context) { List<String> varList = new ArrayList<String>() {{ add("张三"); add("李四"); add("王五"); }}; context.put("varList", varList); List<User> userList = new ArrayList<User>(){{ add(new User("张三", 18, new Date(), new User.Address("中国", "陕西", "西安"))); add(new User("李四", 19, new Date(), new User.Address("中国", "四川", "成都"))); add(new User("王五", 20, new Date(), new User.Address("中国", "河南", "郑州"))); }}; context.put("userList", userList); List<List<String>> table = new ArrayList<List<String>>(){{ add(new ArrayList<String>(){{ add("第一行:第一列"); add("第一行:第二列"); }}); add(new ArrayList<String>(){{ add("第二行:第一列"); add("第二行:第二列"); }}); add(new ArrayList<String>(){{ add("第三行:第一列"); add("第三行:第二列"); }}); }}; context.put("table", table); } }
5.4 表格输出
import com.xajw.xdocreport.vo.Dtl; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; /** * 表格输出 * * @author dafeng * @date 2024/12/27 9:59 */ @Slf4j public class Demo5 { // 模板路径 private static final String TEMP_PATH = "D:\deployment\test\xdoc-report\demo5-temp.docx"; // 输出文档路径 private static final String OUT_PATH = "D:\deployment\test\xdoc-report\demo5-out.docx"; /** * 使用 xdocreport 生成 word */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); // 填充变量 setContext(context); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } } /** * 自定义变量填充 */ private static void setContext(IContext context) { List<Dtl> list2 = new ArrayList<Dtl>(){{ add(new Dtl("001", "国铁", "中国", new ArrayList<Dtl.Tender>(){{ add(new Dtl.Tender("张三", 18.0, "无")); add(new Dtl.Tender("李四", 19.0, "无")); add(new Dtl.Tender("王五", 20.0, "无")); }})); add(new Dtl("002", "电子所", "北京", new ArrayList<Dtl.Tender>(){{ add(new Dtl.Tender("赵六", 21.0, "无")); add(new Dtl.Tender("钱七", 22.0, "无")); add(new Dtl.Tender("孙八", 23.0, "无")); }})); add(new Dtl("003", "经纬公司", "陕西", new ArrayList<Dtl.Tender>(){{ add(new Dtl.Tender("周九", 24.0, "无")); add(new Dtl.Tender("吴十", 25.0, "无")); add(new Dtl.Tender("郑十一", 26.0, "无")); }})); }}; context.put("dtlList", list2); } }
开头编辑域 "@before-row[# list dtlList as dtl]"
结尾编辑域 "@after-row[/# list]"
@before-row和@after-row需要成对出现
编号 | 单位 | 地址 |
---|---|---|
«@before-row[#list itemList as item»«${item.code}» | «${item.unit}» | «${item.address}»«@after-row[/#list]» |
子表格开头编辑域和结尾编辑域不添加 @before-row和@after-row
这种实现方式子表格会多出一行空行
编号 | 单位 | 中标人 | 单价 |
---|---|---|---|
«@before-row[#list itemList as item»«${item.code}» | «${item.unit}» | «[#list item.tender as tender]»«${tender.spName}» | «${tender.price}» |
«[/#list]»«@after-row[/#list]» |
移除多余的空行 xxx为子表格循环对象
"[#if (!xxx_has_next)]"
"[#else]"
"[/#if]"
如下:在子表格最后一行只保留一个单元格,其他的单元格需删掉(如上图所示)
编号 | 单位 | 中标人 | 单价 |
---|---|---|---|
«@before-row[#list itemList as item»«${item.code}» | «${item.unit}» | «[#list item.tender as tender]»«${tender.spName}» | «${tender.price}»«[#if (!tender_has_next)]»«[#else]» |
«[/#if]»«[/#list]»«@after-row[/#list]» |
如下:
红色为最外层循环
蓝色为子表格循环
绿色为隐藏子表格空白行
编号 | 中标人 |
---|---|
«@before-row[#list itemList as item»«${item.code}» | «[#list item.tender as tender]»«${tender.spName}»«[#if (!tender_has_next)]»«[#else]» |
«[/#if]»«[/#list]»«@after-row[/#list]» |
5.5 图片输出
import fr.opensagres.xdocreport.core.XDocReportException; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider; import fr.opensagres.xdocreport.document.images.FileImageProvider; import fr.opensagres.xdocreport.document.images.IImageProvider; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import fr.opensagres.xdocreport.template.formatter.FieldsMetadata; import fr.opensagres.xdocreport.template.formatter.NullImageBehaviour; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 图片输出 * * @author dafeng * @date 2024/12/27 9:59 */ @Slf4j public class Demo6 { // 模板路径 private static final String TEMP_PATH = "D:\deployment\test\xdoc-report\demo6-temp.docx"; // 输出文档路径 private static final String OUT_PATH = "D:\deployment\test\xdoc-report\demo6-out.docx"; /** * 使用 xdocreport 生成 word */ public static void main(String[] args) { try ( // 1. 定义输入流,读取模板 InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH)); // 2. 定义输出流,输出文件 OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH)) ) { // 3. 读取模板,创建 IXDocReport 对象 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker); // 4. 创建上下文,设置变量 IContext context = report.createContext(); // 填充变量 setContext(report, context); // 5. 生成 word report.process(context, out); } catch (Exception e) { log.error("生成word失败", e); } } /** * 自定义变量填充 */ private static void setContext(IXDocReport report, IContext context) throws XDocReportException { FieldsMetadata metadata = report.createFieldsMetadata(); // 1. 单个图片 File img = new File("D:\deployment\test\xdoc-report\1.png"); context.put("img", new FileImageProvider(img)); metadata.addFieldAsImage("img"); // 2. 循环图片 IImageProvider p1 = new FileImageProvider(new File("D:\deployment\test\xdoc-report\1.png")); IImageProvider p2 = new FileImageProvider(new File("D:\deployment\test\xdoc-report\2.png")); IImageProvider p3 = new FileImageProvider(new File("D:\deployment\test\xdoc-report\3.png")); IImageProvider p4 = new FileImageProvider(new File("D:\deployment\test\xdoc-report\4.png")); List<Map<String, IImageProvider>> list = new ArrayList<>(); list.add(Map.of("pic", p1)); list.add(Map.of("pic", p2)); list.add(Map.of("pic", p3)); list.add(Map.of("pic", p4)); context.put("picList", list); //映射:picture为模板中书签名,item.pic为word模板循环中的变量名 metadata.addFieldAsImage("picture","item.pic", NullImageBehaviour.RemoveImageTemplate); } private static void setContext1(IXDocReport report, IContext context) { File file = new File("D:\deployment\test\xdoc-report\a.jpg"); try (FileInputStream in = new FileInputStream(file)){ FieldsMetadata metadata = report.createFieldsMetadata(); metadata.addFieldAsImage("img"); context.put("img", new ByteArrayImageProvider(in)); }catch (Exception e){ log.error("获取图片失败", e); } } }
二、Poi-tl
1. 简介
poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中。模板是Docx格式的Word文档,你可以使用Microsoft office、WPS Office、Pages等任何你喜欢的软件制作模板,也可以使用Apache POI代码来生成模板。
所有的标签都是以
{{
开头,以}}
结尾,标签可以出现在任何位置,包括页眉,页脚,表格内部,文本框等,表格布局可以设计出很多优秀专业的文档,推荐使用表格布局。poi-tl模板遵循“所见即所得”的设计,模板和标签的样式会被完全保留。
代码托管地址:https://github.com/Sayi/poi-tl
指导文档地址:https://deepoove.com/poi-tl/
2. 主要功能
Word模板引擎功能 | 描述 |
---|---|
文本 | 将标签渲染为文本 |
图片 | 将标签渲染为图片 |
表格 | 将标签渲染为表格 |
列表 | 将标签渲染为列表 |
图表 | 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染 |
If Condition判断 | 根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等) |
Foreach Loop循环 | 根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等) |
Loop表格行 | 循环复制渲染表格的某一行 |
Loop表格列 | 循环复制渲染表格的某一列 |
Loop有序列表 | 支持有序列表的循环,同时支持多级列表 |
Highlight代码高亮 | word中代码块高亮展示,支持26种语言和上百种着色样式 |
Markdown | 将Markdown渲染为word文档 |
Word批注 | 完整的批注功能,创建批注、修改批注等 |
Word附件 | Word中插入附件 |
SDT内容控件 | 内容控件内标签支持 |
Textbox文本框 | 文本框内标签支持 |
图片替换 | 将原有图片替换成另一张图片 |
书签、锚点、超链接 | 支持设置书签,文档内锚点和超链接功能 |
Expression Language | 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL… |
样式 | 模板即样式,同时代码也可以设置样式 |
模板嵌套 | 模板包含子模板,子模板再包含子模板 |
合并 | Word合并Merge,也可以在指定位置进行合并 |
用户自定义函数(插件) | 插件化设计,在文档任何位置执行函数 |
3. 开发环境和依赖
- JDK1.8+
- Apache POI5.2.2+
4. 快速开始
4.1 引入依赖
<dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.2</version> </dependency>
4.2 创建模板
注:Poi-tl 创建模板相较于XDocReport比较简单,无需使用域,直接使用 {{var}} 即可
4.3 生成文档
public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render( new HashMap<String, Object>(){{ put("title", "Hi, poi-tl Word模板引擎"); }}); template.writeAndClose(new FileOutputStream(OUT_PATH)); }
5. Demo演示
5.1 文本输出
- 模板代码
{{title}}
- Java代码
import com.deepoove.poi.XWPFTemplate; import java.io.FileNotFoundException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; /** * poi-tl Word模板引擎 * * @author dafeng * @date 2024/12/20 13:41 */ public class PoiTlUtil { private static final String TEMP_PATH = "D:\deployment\test\tl\template.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\output.docx"; public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() { return new HashMap<String, Object>() {{ // 文本 put("title", "Hi, poi-tl Word模板引擎"); }}; } }
5.2 图片输出
- 模板代码
{{@image}} {{@svg}} {{@image1}} {{@streamImg}} {{@urlImg}} {{@buffered}}
- Java代码
/** * poi-tl Word模板引擎 * * @author dafeng * @date 2024/12/20 13:41 */ public class PoiTlUtil { private static final String TEMP_PATH = "D:\deployment\test\tl\template.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\output.docx"; public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() throws FileNotFoundException { return new HashMap<String, Object>() {{ // 指定图片路径 put("image", "F:\Z-图片\a.jpg"); // svg图片 put("svg", "https://img1.baidu.com/it/u=1960110688,1786190632&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281"); // 图片文件 put("image1", Pictures.ofLocal("F:\a.jpg").size(120, 120).create()); // 图片流 put("streamImg", Pictures.ofStream(new FileInputStream("F:\logo.png"), PictureType.PNG) .size(100, 120).create()); // 网络图片(注意网络耗时对系统可能的性能影响) put("urlImg", Pictures.ofUrl("http://xxx.com/icecream.png").size(100, 100).create()); // java图片,我们可以利用Java生成图表插入到word文档中 put("buffered", Pictures.ofBufferedImage(new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), PictureType.PNG).size(100, 100).create()); }}; } }
5.3 列表输出
- 模板代码
{{*list}}
- Java代码
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.data.Numberings; import java.io.FileNotFoundException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; /** * poi-tl Word模板引擎 * * @author dafeng * @date 2024/12/20 13:41 */ public class PoiTlUtil { private static final String TEMP_PATH = "D:\deployment\test\tl\template.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\output.docx"; public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() throws FileNotFoundException { return new HashMap<String, Object>() {{ // 列表 put("list", Numberings.create("Plug-in grammar", "Supports word text, pictures, table...", "Not just templates")); }}; } }
5.4 表格输出
- 模板代码
{{#table0}} {{#table1}} {{#table2}}
- Java代码
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.data.MergeCellRule; import com.deepoove.poi.data.RowRenderData; import com.deepoove.poi.data.Rows; import com.deepoove.poi.data.Tables; import com.deepoove.poi.data.style.BorderStyle; import java.io.FileNotFoundException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; /** * poi-tl Word模板引擎 * * @author dafeng * @date 2024/12/20 13:41 */ public class PoiTlUtil2 { private static final String TEMP_PATH = "D:\deployment\test\tl\template.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\output.docx"; public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() throws FileNotFoundException { return new HashMap<String, Object>() {{ // 3. 表格 // 一个2行2列的表格 put("table0", Tables.of(new String[][]{ new String[]{"00", "01"}, new String[]{"10", "11"} }).border(BorderStyle.DEFAULT).create()); // 第0行居中且背景为蓝色的表格 RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF") .bgColor("4472C4").center().create(); RowRenderData row1 = Rows.create("李四", "博士"); put("table1", Tables.create(row0, row1)); // 合并第1行所有单元格的表格 RowRenderData row2 = Rows.of("列0", "列1", "列2").center().bgColor("4472C4").create(); RowRenderData row3 = Rows.create("没有数据", null, null); MergeCellRule rule = MergeCellRule.builder().map(MergeCellRule.Grid.of(1, 0), MergeCellRule.Grid.of(1, 2)).build(); put("table2", Tables.of(row2, row3).mergeRule(rule).create()); }}; } }
5.5 表格行循环
货物明细和人工费在同一个表格中,货物明细需要展示所有货物,人工费需要展示所有费用。
{{goods}}
是个标准的标签,将{{goods}}
置于循环行的上一行,循环行设置要循环的标签和内容,注意此时的标签应该使用[]
,以此来区别 poi-tl 的默认标签语法。同理,{{labors}}
也置于循环行的上一行。
-
模板
-
Java代码
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.data.RowRenderData; import com.deepoove.poi.data.Rows; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; import com.xajw.export.app.tl.vo.DetailData; import com.xajw.export.app.tl.vo.Goods; import com.xajw.export.app.tl.vo.Labor; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; /** * Poi-tl 表格行循环 * @author dafeng * @date 2024/12/20 17:06 */ public class PoiTlTableUtil1 { private static final String TEMP_PATH = "D:\deployment\test\tl\template_table.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\template_table_out.docx"; public static void main(String[] args) throws Exception { // 绑定插件 LoopRowTableRenderPolicy rowPolicy = new LoopRowTableRenderPolicy(); // 表格行循环 Configure config = Configure.builder() .bind("goods", rowPolicy) .bind("labors", rowPolicy) .build(); XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() { return new HashMap<String, Object>() {{ List<Goods> goods = new ArrayList<>(); List<Labor> labors = new ArrayList<>(); for (int i = 0; i < 5; i++) { goods.add(new Goods(i + 1, "商品" + i, "描述" + i, 10, 20, 30, 40)); labors.add(new Labor("类别" + i, 10, 20, 30)); } put("goods", goods); put("labors", labors); put("total", 1220); }}; } }
- 生成文档
5.6 表格列循环
-
模板
-
Java代码
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.data.RowRenderData; import com.deepoove.poi.data.Rows; import com.deepoove.poi.plugin.table.LoopColumnTableRenderPolicy; import com.xajw.export.app.tl.vo.DetailData; import com.xajw.export.app.tl.vo.Goods; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; /** * Poi-tl 表格列循环 * * @author dafeng * @date 2024/12/20 17:06 */ public class PoiTlTableUtil1 { private static final String TEMP_PATH = "D:\deployment\test\tl\template_table.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\template_table_out.docx"; public static void main(String[] args) throws Exception { // 绑定插件 LoopColumnTableRenderPolicy columnPolicy = new LoopColumnTableRenderPolicy(); // 表格列循环 Configure config = Configure.builder() .bind("products", columnPolicy) .build(); XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() { return new HashMap<String, Object>() {{ List<Goods> products = new ArrayList<>(); for (int i = 0; i < 5; i++) { products.add(new Goods(i + 1, "商品" + i, "描述" + i, 10, 20, 30, 40)); } put("products", products); put("total", 1220); }}; } }
- 生成文档
5.7 动态表格
当需求中的表格更加复杂的时候,我们完全可以设计好那些固定的部分,将需要动态渲染的部分单元格交给自定义模板渲染策略。poi-tl提供了抽象表格策略
DynamicTableRenderPolicy
来实现这样的功能。
-
模板
-
Java代码
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.data.RowRenderData; import com.deepoove.poi.data.Rows; import com.deepoove.poi.plugin.table.LoopColumnTableRenderPolicy; import com.xajw.export.app.tl.vo.DetailData; import com.xajw.export.app.tl.vo.Goods; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; /** * Poi-tl 动态表格 * * @author dafeng * @date 2024/12/20 17:06 */ public class PoiTlTableUtil { private static final String TEMP_PATH = "D:\deployment\test\tl\template_table.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\template_table_out.docx"; public static void main(String[] args) throws Exception { // 绑定插件 DetailTablePolicy detailTablePolicy = new DetailTablePolicy(); // 动态表格表格 Configure config = Configure.builder() .bind("detailTable", detailTablePolicy) .build(); XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() { DetailData detailTable = new DetailData(); RowRenderData good = Rows.of("4", "墙纸", "书房+卧室", "1500", "/", "400", "1600").center().create(); List<RowRenderData> goods = Arrays.asList(good, good, good); detailTable.setGoods(goods); RowRenderData labor = Rows.of("油漆工", "2", "200", "400").center().create(); List<RowRenderData> labors = Arrays.asList(labor, labor, labor, labor); detailTable.setLabors(labors); return new HashMap<String, Object>() {{ put("detailTable", detailTable); put("total", 1220); }}; } }
import com.deepoove.poi.data.RowRenderData; import com.deepoove.poi.policy.DynamicTableRenderPolicy; import com.deepoove.poi.policy.TableRenderPolicy; import com.deepoove.poi.util.TableTools; import com.xajw.export.app.tl.vo.DetailData; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import java.util.List; public class DetailTablePolicy extends DynamicTableRenderPolicy { // 货品填充数据所在行数 int goodsStartRow = 2; // 人工费填充数据所在行数 int laborsStartRow = 5; @Override public void render(XWPFTable table, Object data) throws Exception { if (null == data) return; DetailData detailData = (DetailData) data; // 人工费 先创建表格后面的行数据 List<RowRenderData> labors = detailData.getLabors(); table.removeRow(laborsStartRow); // 循环插入行 for (RowRenderData labor : labors) { XWPFTableRow insertNewTableRow = table.insertNewTableRow(laborsStartRow); for (int j = 0; j < 7; j++) { insertNewTableRow.createCell(); } // 合并单元格 TableTools.mergeCellsHorizonal(table, laborsStartRow, 0, 3); // 单行渲染 TableRenderPolicy.Helper.renderRow(table.getRow(laborsStartRow), labor); } // 货物 List<RowRenderData> goods = detailData.getGoods(); table.removeRow(goodsStartRow); for (RowRenderData good : goods) { XWPFTableRow insertNewTableRow = table.insertNewTableRow(goodsStartRow); for (int j = 0; j < 7; j++) { insertNewTableRow.createCell(); } TableRenderPolicy.Helper.renderRow(table.getRow(goodsStartRow), good); } } }
- 输出文档
5.8 区块对
- 模板代码
{{?announce}} Top of the world! {{/announce}} {{?person}} Hi {{name}}!,{{age}} {{/person}} {{?paragraphList}} {{content}} {{/paragraphList}} {{?produces}} {{_index+1}}. {{=#this}} {{_is_first}} {{_is_last}} {{_has_next}} {{_is_even_item}} {{_is_odd_item}} {{/produces}}
- Java代码
import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; import com.deepoove.poi.XWPFTemplate; import java.io.FileNotFoundException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * poi-tl Word模板引擎 * * @author dafeng * @date 2024/12/20 13:41 */ public class PoiTlUtil { private static final String TEMP_PATH = "D:\deployment\test\tl\template.docx"; private static final String OUT_PATH = "D:\deployment\test\tl\output.docx"; public static void main(String[] args) throws Exception { XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData()); template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH))); } private static Object genData() throws FileNotFoundException { return new HashMap<String, Object>() {{ // 5. 区块对 put("announce", false); Map<String, String> person = MapUtil .builder("name", "张三") .put("age", "18") .build(); put("person", person); List<Map> wordDataList = new ArrayList<>(); wordDataList.add(MapUtil.builder("content", "明月几时有,把酒问青天。").build()); wordDataList.add(MapUtil.builder("content", "不知天上宫阙,今夕是何年?").build()); wordDataList.add(MapUtil.builder("content", "我欲乘风归去,又恐琼楼玉宇,高处不胜寒。").build()); wordDataList.add(MapUtil.builder("content", "大江东去,浪淘尽,千古风流人物。").build()); put("paragraphList", wordDataList); put("produces", ListUtil.of("application/json", "application/xml", "text/html", "text/plain")); }}; } }
6. 配置
poi-tl提供了类
Configure
来配置常用的设置,使用方式如下:
ConfigureBuilder builder = Configure.builder(); XWPFTemplate.compile("template.docx", builder.buid());
6.1 前后缀
组件默认使用
{{}}
的方式来致敬Google CTemplate,如果你更偏爱freemarker${}
的方式:
builder.buildGramer("${", "}");
6.2 标签类型
组件默认的图片标签是以@开始,若希望使用%开始作为图片标签:
builder.addPlugin('%', new PictureRenderPolicy());
也可以自由更改的标签标识类型
builder.addPlugin('@', new TableRenderPolicy()); builder.addPlugin('#', new PictureRenderPolicy());
这样{{@var}}就变成了表格标签,{{#var}}变成了图片标签,虽然不建议改变默认标签标识,但是从中可以看到poi-tl插件的灵活度,在插件章节中我们将会看到如何自定义自己的标签。
6.3 标签格式
标签默认支持中文、字母、数字、下划线的组合,但可以通过正则表达式来配置标签的规则,如不允许中文:
builder.buildGrammerRegex("[\w]+(\.[\w]+)*");
若允许除了标签前后缀外的任意字符:
builder.buildGrammerRegex(RegexUtils.createGeneral("{{", "}}"));
6.4 EL表达式
Spring Expression Language 是一个强大的表达式语言,支持在运行时查询和操作对象图,可作为独立组件使用,需要引入相应的依赖:
官方文档:https://docs.spring.io/spring-framework/docs/5.3.18/reference/html/core.html#expressions
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.3.18</version> </dependency>
为了在模板标签中使用SpringEL表达式,需要将标签配置为SpringEL模式:
builder.useSpringEL();
{{name}} {{name.toUpperCase()}} {{name == 'poi-tl'}} {{empty?:'这个字段为空'}} {{sex ? '男' : '女'}} {{new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(time)}} {{price/10000 + '万元'}} {{dogs[0].name}} {{localDate.format(T(java.time.format.DateTimeFormatter).ofPattern('yyyy年MM月dd日'))}}