首页 > 极客资料 博客日记
Java生成Word文档之 XDocReport 和 Poi-tl
2025-01-10 16:30:06极客资料围观2次
近期参与的多个项目中,均涉及根据预定义模板生成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日'))}}
标签: