关于文档:poi-tl v1.10.x是构建在Apache poi4.1.2和JDK1.8+上的版本,会持续进行优化和bugfix,如果希望使用Apache poi5.1.0+,推荐使用poi-tl最新版本:poi-tl 最新文档。
poi-tl(poi template language)是Word模板引擎,使用Word模板和数据创建很棒的Word文档。
在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。
1. Why poi-tl
方案 | 移植性 | 功能性 | 易用性 |
---|---|---|---|
Poi-tl |
Java跨平台 |
Word模板引擎 |
基于Apache POI,更友好的API |
Apache POI |
Java跨平台 |
Apache项目,不仅封装了易用的文档API(文本、图片、表格、页眉、页脚、图表等),也可以在底层直接操作XML结构 |
文档不全,这里有一个教程:Apache POI Word快速入门 |
Freemarker |
XML跨平台 |
仅支持文本,很大的局限性 |
不推荐,需要维护XML结构,代码后期不可维护 |
OpenOffice |
部署OpenOffice,移植性较差 |
- |
需要了解OpenOffice的API |
HTML浏览器导出 |
依赖浏览器的实现,移植性较差 |
HTML不能很好的兼容Word的格式 |
- |
Jacob、winlib |
Windows平台 |
- |
复杂,完全不推荐使用 |
2. 特性
poi-tl正是一个基于Apache POI的Word模板引擎,并且拥有着让人喜悦的特性。
引擎功能 | 描述 |
---|---|
文本 |
将标签渲染为文本 |
图片 |
将标签渲染为图片 |
表格 |
将标签渲染为表格 |
列表 |
将标签渲染为列表 |
图表 |
条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染 |
If Condition判断 |
隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等) |
Foreach Loop循环 |
循环某些文档内容(包括文本、段落、图片、表格、列表、图表等) |
Loop表格行 |
循环复制渲染表格的某一行 |
Loop表格列 |
循环复制渲染表格的某一列 |
Loop有序列表 |
支持有序列表的循环,同时支持多级列表 |
代码高亮 |
word中代码块高亮展示,支持26种语言和上百种着色样式 |
Markdown |
将Markdown渲染为word文档 |
Word批注 |
完整的批注功能,创建批注、修改批注等 |
Word附件 |
Word中插入附件 |
SDT内容控件 |
内容控件内标签支持 |
图片替换 |
将原有图片替换成另一张图片 |
书签、锚点、超链接 |
支持设置书签,文档内锚点和超链接功能 |
Expression Language |
完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL… |
标签定制 |
支持自定义标签前后缀 |
文本框 |
文本框内标签支持 |
样式 |
模板即样式,同时代码也可以设置样式 |
模板嵌套 |
模板包含子模板,子模板再包含子模板 |
合并 |
Word合并Merge,也可以在指定位置进行合并 |
用户自定义函数(插件) |
在文档任何位置执行函数 |
poi-tl是一个免费开源的Java类库,你可以非常方便的加入到你的项目中。
3. 软件要求
-
Apache POI 4.1.2
-
JDK 1.8+
4. 历史版本
点击下方链接查阅poi-tl历史版本文档,其中v1.5.x是构建在Apache poi3.16+和JDK1.6+上的版本,适用于无法升级poi的古老项目。
V1.10.4+版本作了一个不兼容的改动,升级的时候需要注意:
-
重构了PictureRenderData,改为抽象类,建议使用Pictures工厂方法来创建图片数据
5. Getting Started
5.1. Maven
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.10.6</version>
</dependency>
5.2. Gradle
implementation 'com.deepoove:poi-tl:1.10.6'
5.3. 2分钟快速入门
新建Word文档template.docx,包含标签 {{title}}
{{title}}
XWPFTemplate template = XWPFTemplate.compile("template.docx").render(
new HashMap<String, Object>(){{
put("title", "Hi, poi-tl Word模板引擎");
}}); (1) (2)
template.writeAndClose(new FileOutputStream("output.docx")); (3)
1 | compile 编译模板 |
2 | render 渲染数据 |
3 | write 输出到流 |
TDO模式:Template + data-model = output
Hi, poi-tl Word模板引擎
5.4. Template:模板
模板是Docx格式的Word文档,你可以使用Microsoft office、WPS Office、Pages等任何你喜欢的软件制作模板,也可以使用Apache POI代码来生成模板。
所有的标签都是以{{
开头,以}}
结尾,标签可以出现在任何位置,包括页眉,页脚,表格内部,文本框等,表格布局可以设计出很多优秀专业的文档,推荐使用表格布局。
poi-tl模板遵循“所见即所得”的设计,模板和标签的样式会被完全保留。
5.5. Data-model:数据
数据类似于哈希或者字典,可以是Map结构(key是标签名称):
Map<String, Object> data = new HashMap<>();
data.put("name", "Sayi");
data.put("start_time", "2019-08-04");
可以是对象(属性名是标签名称):
public class Data {
private String name;
private String startTime;
private Author author;
}
数据可以是树结构,每级之间用点来分隔开,比如{{author.name}} 标签对应的数据是author对象的name属性值。
|
FreeMarker、Velocity文本模板中可以通过三个标签设置图片路径、宽和高:
<img src="{{path}}" width="{{width}}" height="{{height}}">
但是Word模板不是由简单的文本表示,所以在渲染图片、表格等元素时提供了数据模型,它们都实现了接口RenderData,比如图片数据模型PictureRenderData包含图片路径、宽、高三个属性。
5.6. Output:输出
以流的方式进行输出:
template.write(OutputStream stream);
可以写到任意输出流中,比如文件流:
template.write(new FileOutputStream("output.docx"));
比如网络流:
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition","attachment;filename=\""+"out_template.docx"+"\"");
// HttpServletResponse response
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
最后不要忘记关闭这些流。
6. 标签
poi-tl是一种无逻辑「logic-less」的模板引擎,没有复杂的控制结构和变量赋值,只有标签。标签由前后两个大括号组成,{{title}}是标签,{{?title}}也是标签,title是这个标签的名称,问号标识了标签类型,接下来我们来看看有哪些默认标签类型(用户可以创建新的标签类型,这属于更高级的话题)。
6.1. 文本
{{var}}
数据模型:
-
String
:文本 -
TextRenderData
:有样式的文本 -
HyperlinkTextRenderData
:超链接和锚点文本 -
Object
:调用 toString() 方法转化为文本
除了new操作符,还提供了更加优雅的工厂 Texts
和链式调用的方式轻松构建文本模型。
所见即所得,标签的样式会应用到替换后的文本上,也可以通过代码设定文本的样式。
文本换行使用 \n 字符。
|
6.2. 图片
数据模型:
-
String
:图片url或者本地路径,默认使用图片自身尺寸 -
PictureRenderData
-
ByteArrayPictureRenderData
-
FilePictureRenderData
-
UrlPictureRenderData
推荐使用工厂 Pictures
构建图片模型。
图片支持BufferedImage,这意味着我们可以利用Java生成图表插入到word文档中。
6.3. 表格
数据模型:
-
TableRenderData
推荐使用工厂 Tables
、 Rows
和 Cells
构建表格模型。
// 一个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 row0 = Rows.of("列0", "列1", "列2").center().bgColor("4472C4").create();
RowRenderData row1 = Rows.create("没有数据", null, null);
MergeCellRule rule = MergeCellRule.builder().map(Grid.of(1, 0), Grid.of(1, 2)).build();
put("table3", Tables.of(row0, row1).mergeRule(rule).create());
TableRenderData表格模型在单元格内可以展示文本和图片,同时也可以指定表格样式、行样式和单元格样式,而且在N行N列渲染完成后可以应用单元格合并规则 MergeCellRule ,从而实现更复杂的表格。
6.4. 列表
数据模型:
-
List<String>
-
NumberingRenderData
推荐使用工厂 Numberings
构建编号模型。
编号样式支持罗马字符、有序无序等,可以通过 Numberings.of(NumberingFormat)
来指定。
DECIMAL //1. 2. 3.
DECIMAL_PARENTHESES //1) 2) 3)
BULLET //● ● ●
LOWER_LETTER //a. b. c.
LOWER_ROMAN //i ⅱ ⅲ
UPPER_LETTER //A. B. C.
NumberingRenderData可以创建多级列表,但是推荐使用区块对:区块对的循环功能可以很好的循环列表,并且保持有序列表编号有序。 |
6.5. 区块对
区块对开始和结束标签中间可以包含多个图片、表格、段落、列表、图表等,开始和结束标签可以跨多个段落,也可以在同一个段落,但是如果在表格中使用区块对,开始和结束标签必须在同一个单元格内,因为跨多个单元格的渲染行为是未知的。
区块对在处理一系列文档元素的时候非常有用,位于区块对中的文档元素可以被渲染零次,一次或N次,这取决于区块对的取值。
- False或空集合
-
隐藏区块中的所有文档元素
- 非False且不是集合
-
显示区块中的文档元素,渲染一次
- 非空集合
-
根据集合的大小,循环渲染区块中的文档元素
集合是根据值的类型是否实现了 Iterable 接口来判断。
|
6.5.1. False或空集合
如果区块对的值是 null 、false 或者空的集合,位于区块中的所有文档元素将不会显示,这就等同于if语句的条件为 false。
{
"announce": false
}
Made it,Ma!{{?announce}}Top of the world!{{/announce}}
Made it,Ma!
{{?announce}}
Top of the world!🎋
{{/announce}}
Made it,Ma!
Made it,Ma!
6.5.2. 非False且不是集合
如果区块对的值不为 null 、 false ,且不是集合,位于区块中的所有文档元素会被渲染一次,这就等同于if语句的条件为 true。
{
"person": { "name": "Sayi" }
}
{{?person}}
Hi {{name}}!
{{/person}}
Hi Sayi!
区块对中标签的作用域会被限定在当前区块对内,当且仅当区块对的值是boolean 类型且为true 时,这些标签作用域才不会改变。
|
6.5.3. 非空集合
如果区块对的值是一个非空集合,区块中的文档元素会被迭代渲染一次或者N次,这取决于集合的大小,类似于foreach语法。
{
"songs": [
{ "name": "Memories" },
{ "name": "Sugar" },
{ "name": "Last Dance" }
]
}
{{?songs}}
{{name}}
{{/songs}}
Memories
Sugar
Last Dance
7. 引用标签
引用标签是一种特殊位置的特殊标签,提供了直接引用文档中的元素句柄的能力,这个重要的特性在我们只想改变文档中某个元素极小一部分样式和属性的时候特别有用,因为其余样式和属性都可以在模板中预置好,真正的所见即所得。
7.1. 图片
引用图片标签只会替换图片而不会改变图片尺寸和布局,数据模型和图片标签一致:PictureRenderData
。
put("img", Pictures.ofLocal("sayi.png").create());
7.2. 多系列图表
多系列图表指的是条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图等。
数据模型:
-
ChartMultiSeriesRenderData
推荐使用工厂 Charts
构建图表模型。
新的图表系列数据会完全替换原有图表数据,而原有图表的样式都会被保留。
7.3. 单系列图表
单系列图表指的是饼图(3D饼图)、圆环图等。
数据模型:
-
ChartSingleSeriesRenderData
推荐使用工厂 Charts
构建图表模型。
7.4. 组合图表
组合图表指的是由多系列图表(柱形图、折线图、面积图)组合而成的图表。
同多系列图表 ChartMultiSeriesRenderData
数据模型。
8. 配置
poi-tl提供了类 Configure
来配置常用的设置,使用方式如下:
ConfigureBuilder builder = Configure.builder();
XWPFTemplate.compile("template.docx", builder.buid());
8.1. 前后缀
我一直使用 {{}}
的方式来致敬Google CTemplate,如果你更偏爱freemarker ${}
的方式:
builder.buildGramer("${", "}");
8.2. 标签类型
默认的图片标签是以@开始,如果你希望使用%开始作为图片标签:
builder.addPlugin('%', new PictureRenderPolicy());
如果你不是很喜欢默认的标签标识类型,你也可以自由更改:
builder.addPlugin('@', new TableRenderPolicy());
builder.addPlugin('#', new PictureRenderPolicy());
这样{{@var}}就变成了表格标签,{{#var}}变成了图片标签,虽然不建议改变默认标签标识,但是从中可以看到poi-tl插件的灵活度,在插件章节中我们将会看到如何自定义自己的标签。
8.3. 标签匹配
标签默认支持中文、字母、数字、下划线的组合,我们可以通过正则表达式来配置标签的规则,比如不允许中文:
builder.buildGrammerRegex("[\\w]+(\\.[\\w]+)*");
比如允许除了标签前后缀外的任意字符:
builder.buildGrammerRegex(RegexUtils.createGeneral("{{", "}}"));
8.4. 标签值计算
标签值计算是指如何在数据模型中索引标签Key的值,可以完全自定义获取标签值的方式。
builder.setRenderDataComputeFactory(new RenderDataComputeFactory());
RenderDataComputeFactory是一个抽象工厂,你可以定义自己的工厂提供标签表达式计算接口 RenderDataCompute 的实现。
|
我们可以通过此方式支持任何的表达式引擎,Spring表达式正是通过 SpELRenderDataCompute
实现。
8.5. Spring表达式
Spring Expression Language 是一个强大的表达式语言,支持在运行时查询和操作对象图,可作为独立组件使用,需要引入相应的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.18</version>
</dependency>
为了在模板标签中使用SpringEL表达式,需要将标签配置为SpringEL模式:
builder.useSpringEL();
8.5.1. 基本使用
关于SpringEL的写法可以参见官方文档,下面给出一些典型的示例。
{{name}}
{{name.toUpperCase()}} (1)
{{name == 'poi-tl'}} (2)
{{empty?:'这个字段为空'}}
{{sex ? '男' : '女'}} (3)
{{new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(time)}} (4)
{{price/10000 + '万元'}} (5)
{{dogs[0].name}} (6)
{{localDate.format(T(java.time.format.DateTimeFormatter).ofPattern('yyyy年MM月dd日'))}} (7)
1 | 类方法调用,转大写 |
2 | 判断条件 |
3 | 三目运算符 |
4 | 类方法调用,时间格式化 |
5 | 运算符 |
6 | 数组列表使用下标访问 |
7 | 使用静态类方法 |
8.5.2. SpringEL作为区块对的条件
Spring表达式与区块对结合可以实现更强大的功能。
{
"desc": "",
"summary": "Find A Pet",
"produces": [
"application/xml"
]
}
{{?desc == null or desc == ''}}{{summary}}{{/}}
{{?produces == null or produces.size() == 0}}无{{/}}
Find A Pet
使用SpringEL时区块对的结束标签可以是:{{/}}。 |
8.6. 数据模型序列化
数据模型支持JSON字符串序列化,可以方便的构造远程HTTP或者RPC服务,需要引入相应依赖:
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl-jsonmodel-support</artifactId>
<version>1.0.0</version>
</dependency>
然后配置数据模型前置转化器即可:
builder.addPreRenderDataCastor(new GsonPreRenderDataCastor());
8.7. 错误处理
poi-tl支持在发生错误的时候定制引擎的行为。
8.7.1. 标签无法被计算
标签无法被计算的场景有几种,比如模板中引用了一个不存在的变量,或者级联的前置结果不是一个哈希,如 {{author.name}}
中author的值为null,此时就无法计算name的值。
poi-tl可以在发生这种错误时对计算结果进行配置,默认会认为标签值为null
。当我们需要严格校验模板是否有人为失误时,可以抛出异常:
builder.useDefaultEL(true);
注意的是,如果使用SpringEL表达式,可以通过参数来配置是否抛出异常:
builder.useSpringEL(true);
8.7.2. 标签数据类型不合法
我们知道渲染图片、表格等标签时对数据模型是有要求的,如果数据不合法(为空或者是一个错误的数据类型),可以配置模板标签的渲染行为。
poi-tl默认的行为会清空标签,如果希望对标签不作任何处理:
builder.setValidErrorHandler(new DiscardHandler());
如果希望执行严格的校验,直接抛出异常:
builder.setValidErrorHandler(new AbortHandler());
8.8. 模板生成模板
模板引擎不仅仅可以生成文档,也可以生成新的模板,比如我们把原先的一个文本标签分成一个文本标签和一个表格标签:
Configure config = Configure.builder().bind("title", new DocumentRenderPolicy()).build();
Map<String, Object> data = new HashMap<>();
DocumentRenderData document = Documents.of()
.addParagraph(Paragraphs.of("{{title}}").create())
.addParagraph(Paragraphs.of("{{#table}}").create())
.create();
data.put("title", document);
8.9. 无模板创建文档
使用 XWPFTemplate.create
在无需模板的情况下创建文档,可以充分利用poi-tl友好的API来生成文档元素。
String text = "this a paragraph";
DocumentRenderData data = Documents.of().addParagraph(Paragraphs.of(text).create()).create();
XWPFTemplate template = XWPFTemplate.create(data);
8.10. 日志
poi-tl使用slf4j作为日志门面,你可以自由选择日志实现,比如logback、log4j等。我们以logback为例:
首先在项目中添加logback依赖:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
然后配置logback.xml文件,可以配置日志级别和格式:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.deepoove.poi" level="debug" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
debug级别的日志会打印解析渲染过程中的信息,有利于程序调试,另外在模板引擎执行结束后会打印耗时信息:
Successfully Render the template file in 13 millis
9. 插件
插件,又称为自定义函数,它允许用户在模板标签位置处执行预先定义好的函数。由于插件机制的存在,我们几乎可以在模板的任何位置执行任何操作。
插件是poi-tl的核心,默认的标签和引用标签都是通过插件加载。
9.1. 默认插件
poi-tl默认提供了八个策略插件,用来处理文本、图片、列表、表格、文档嵌套、引用图片、引用多系列图表、引用单系列图表等:
-
TextRenderPolicy
-
PictureRenderPolicy
-
NumberingRenderPolicy
-
TableRenderPolicy
-
DocxRenderPolicy
-
MultiSeriesChartTemplateRenderPolicy
-
SingleSeriesChartTemplateRenderPolicy
-
DefaultPictureTemplateRenderPolicy
由于这八个插件如此通用,因此将这些插件注册为不同的标签类型,从而搭建了poi-tl的标签体系,也构筑了poi-tl高度自由的插件机制。
9.2. 开发一个插件
实现一个插件就是要告诉我们在模板的某个地方用某些数据做某些事情,我们可以通过实现RenderPolicy
接口开发自己的插件:
public interface RenderPolicy {
void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template); (1) (2) (3)
}
1 | ElementTemplate是当前标签位置 |
2 | data是数据模型 |
3 | XWPFTemplate代表整个模板 |
接下来我们写一个将标签替换为Hello, world的插件:
public class HelloWorldRenderPolicy implements RenderPolicy {
@Override
public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
XWPFRun run = ((RunTemplate) eleTemplate).getRun(); (1)
// String thing = String.valueOf(data);
String thing = "Hello, world";
run.setText(thing, 0); (2)
}
}
1 | XWPFRun是Apache POI的类,表示当前位置 |
2 | 渲染文本hello, world |
poi-tl提供了抽象模板类 AbstractRenderPolicy
,它定义了一些骨架步骤并且将数据模型的校验和渲染逻辑分开,使用泛型约束数据类型,让插件开发起来更简单,接下来我们再写一个更复杂的插件,在模板标签位置完完全全使用代码创建一个表格,这样我们就可以随心所欲的操作表格:
public class CustomTableRenderPolicy extends AbstractRenderPolicy<Object> {
@Override
protected void afterRender(RenderContext<Object> context) {
// 清空标签
clearPlaceholder(context, true);
}
@Override
public void doRender(RenderContext<Object> context) throws Exception {
XWPFRun run = context.getRun();
BodyContainer bodyContainer = BodyContainerFactory.getBodyContainer(run);
// 定义行列
int row = 10, col = 8;
// 插入表格
XWPFTable table = bodyContainer.insertNewTable(run, row, col);
// 表格宽度
TableTools.setWidth(table, UnitUtils.cm2Twips(14.63f) + "", null);
// 边框和样式
TableTools.borderTable(table, BorderStyle.DEFAULT);
// 1) 调用XWPFTable API操作表格
// 2) 调用TableRenderPolicy.Helper.renderRow方法快速方便的渲染一行数据
// 3) 调用TableTools类方法操作表格,比如合并单元格
// ......
TableTools.mergeCellsHorizonal(table, 0, 0, 7);
TableTools.mergeCellsVertically(table, 0, 1, 9);
}
}
通过 bodyContainer.insertNewTable
在当前标签位置插入表格,使用XWPFTable API来操作表格。
随心所欲的意思是原则上Apache POI支持的操作,都可以在当前标签位置进行渲染,Apache POI不支持的操作也可以通过直接操纵底层XML来实现。 |
9.3. 使用插件
插件开发好后,为了让插件在某个标签处执行,我们需要将插件与标签绑定。
9.3.1. 将插件应用到标签
当我们有个模板标签为 {{report}}
,默认是文本标签,如果希望在这个位置做些不一样或者更复杂的事情,我们可以将插件应用到这个模板标签:
ConfigureBuilder builder = Configure.builder();
builder.bind("report", new CustomTableRenderPolicy());
此时,{{report}}
将不再是一个文本标签,而是一个自定义标签。
ConfigureBuilder采用了链式调用的方式,可以一次性设置多个标签的插件:
builder.bind("report", new CustomTableRenderPolicy()).bind("name", new MyRenderPolicy());
9.3.2. 将插件注册为新标签类型
当开发的插件具有一定的通用能力就可以将其注册为新的标签类型。比如增加%标识:{{%var}}
,对应自定义的渲染策略 HelloWorldRenderPolicy
:
builder.addPlugin('%', new HelloWorldRenderPolicy());
此时,{{%var}}
将成为一种新的标签类型,它的执行函数是 HelloWorldRenderPolicy
。
9.4. Plugin Example
接下来用一个完整的代码示例向你展示 Do Anything Anywhere 的想法,它不使用任何poi-tl的默认插件,完全使用自定义函数完成。
插件是一个函数,它的入参是anywhere和anything,函数体就是do something。
// where绑定policy
Configure config = Configure.builder().bind("sea", new AbstractRenderPolicy<String>() {
@Override
public void doRender(RenderContext<String> context) throws Exception { (1)
// anywhere
XWPFRun where = context.getWhere();
// anything
String thing = context.getThing();
// do 文本
where.setText(thing, 0);
}
}).bind("sea_img", new AbstractRenderPolicy<String>() {
@Override
public void doRender(RenderContext<String> context) throws Exception { (2)
// anywhere delegate
WhereDelegate where = context.getWhereDelegate();
// any thing
String thing = context.getThing();
// do 图片
FileInputStream stream = null;
try {
stream = new FileInputStream(thing);
where.addPicture(stream, XWPFDocument.PICTURE_TYPE_JPEG, 400, 450);
} finally {
IOUtils.closeQuietly(stream);
}
// clear
clearPlaceholder(context, false);
}
}).bind("sea_feature", new AbstractRenderPolicy<List<String>>() {
@Override
public void doRender(RenderContext<List<String>> context) throws Exception { (3)
// anywhere delegate
WhereDelegate where = context.getWhereDelegate();
// anything
List<String> thing = context.getThing();
// do 列表
where.renderNumbering(Numberings.of(thing.toArray(new String[] {})).create());
// clear
clearPlaceholder(context, true);
}
}).build();
// 初始化where的数据
HashMap<String, Object> args = new HashMap<String, Object>();
args.put("sea", "Hello, world!");
args.put("sea_img", "sea.jpg");
args.put("sea_feature", Arrays.asList("面朝大海春暖花开", "今朝有酒今朝醉"));
args.put("sea_location", Arrays.asList("日落:日落山花红四海", "花海:你想要的都在这里"));
// 一行代码
XWPFTemplate.compile("sea.docx", config).render(args).writeToFile("out_sea.docx");
1 | 自定义文本插件 |
2 | 自定义图片插件 |
3 | 自定义列表插件 |
10. 可选插件
10.1. 插件列表
除了八个通用的策略插件外,还内置了一些非常有用的插件。
|
渲染一个段落,可以包含不同样式文本,图片等 |
|
|
渲染整个word文档 |
|
|
完整的批注功能 |
|
|
插入附件功能 |
|
|
循环表格行,下文会详细介绍 |
|
|
循环表格列 |
|
|
动态表格插件,允许直接操作表格对象 |
|
|
书签和锚点 |
|
|
引用图表插件,允许直接操作图表对象 |
|
|
Beta实验功能:目录,打开文档时会提示更新域 |
同时有更多的独立插件可以使用(需要引入对应Maven依赖):
|
Word支持代码高亮 |
|
|
使用Markdown来渲染word |
如果你写了一个不错的插件,欢迎分享。 |
10.2. 表格行循环
LoopRowTableRenderPolicy
是一个特定场景的插件,根据集合数据循环表格行。
货物明细和人工费在同一个表格中,货物明细需要展示所有货物,人工费需要展示所有费用。{{goods}}
是个标准的标签,将 {{goods}}
置于循环行的上一行,循环行设置要循环的标签和内容,注意此时的标签应该使用 []
,以此来区别poi-tl的默认标签语法。同理,{{labors}}
也置于循环行的上一行。
{{goods}}
和 {{labors}}
标签对应的数据分别是货物集合和人工费集合,如果集合为空则会删除循环行。
class Goods {
private int count;
private String name;
private String desc;
private int discount;
private int tax;
private int price;
private int totalPrice;
// getter setter
}
class Labor {
private String category;
private int people;
private int price;
private int totalPrice;
// getter setter
}
List<Goods> goods = new ArrayList<>();
List<Labor> labors = new ArrayList<>();
接下来我们将插件应用到这两个标签。
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("goods", policy).bind("labors", policy).build(); (1)
XWPFTemplate template = XWPFTemplate.compile(resource, config).render(
new HashMap<String, Object>() {{
put("goods", goods);
put("labors", labors);
}}
);
1 | 绑定插件 |
最终生成的文档列出了所有货物和人工费。
源码参见 JUnit LoopRowTableRenderPolicyTest,如果希望模板标签和循环行在同一行而不是在上一行,可以使用 new LoopRowTableRenderPolicy(true) 来构造插件。
|
10.3. 表格列循环
LoopColumnTableRenderPolicy
是一个特定场景的插件,根据集合数据循环表格列。要注意的是,由于文档宽度有限,因此模板列必须设置宽度,所有循环列将平分模板列的宽度。
LoopColumnTableRenderPolicy
循环列的使用方式和插件 LoopRowTableRenderPolicy
是一样的,需要将占位标签放在循环列的前一列。
LoopColumnTableRenderPolicy policy = new LoopColumnTableRenderPolicy();
Configure config = Configure.builder().bind("goods", policy).build();
XWPFTemplate template = XWPFTemplate.compile(resource, config).render(
new HashMap<String, Object>() {{
put("goods", goods);
}}
);
最终生成的文档列出了所有货物和人工费。
10.4. 动态表格
当需求中的表格更加复杂的时候,我们完全可以设计好那些固定的部分,将需要动态渲染的部分单元格交给自定义模板渲染策略。poi-tl提供了抽象表格策略 DynamicTableRenderPolicy
来实现这样的功能。
public abstract class DynamicTableRenderPolicy implements RenderPolicy {
public abstract void render(XWPFTable table, Object data);
}
{{detail_table}}标签可以在表格内的任意单元格内,DynamicTableRenderPolicy会获取XWPFTable对象进而获得操作整个表格的能力。
首先新建渲染策略DetailTablePolicy,继承于抽象表格策略。
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();
if (null != labors) {
table.removeRow(laborsStartRow);
// 循环插入行
for (int i = 0; i < labors.size(); i++) {
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), labors.get(i));
}
}
// 货物
List<RowRenderData> goods = detailData.getGoods();
if (null != goods) {
table.removeRow(goodsStartRow);
for (int i = 0; i < goods.size(); i++) {
XWPFTableRow insertNewTableRow = table.insertNewTableRow(goodsStartRow);
for (int j = 0; j < 7; j++) insertNewTableRow.createCell();
TableRenderPolicy.Helper.renderRow(table.getRow(goodsStartRow), goods.get(i));
}
}
}
}
然后将模板标签{{detail_table}}设置成此策略。
Configure config = Configure.builder().bind("detail_table", new DetailTablePolicy()).build();
最终生成的文档列出了所有货物和人工费。
源码参见 JUnit PaymentExample |
10.7. 代码高亮
HighlightRenderPolicy
插件对Word代码块进行高亮展示。
10.7.1. 引入依赖:
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl-plugin-highlight</artifactId>
<version>1.0.0</version>
</dependency>
10.7.4. 常用语言支持
-
apache
-
bash
-
cpp
-
cs
-
css
-
diff
-
go
-
groovy
-
http
-
ini
-
java
-
javascript
-
json
-
makefile
-
markdown
-
objectivec
-
perl
-
php
-
python
-
ruby
-
scala
-
shell
-
sql
-
xml
-
yaml
10.7.5. 常用主题样式
-
github
-
idea
-
zenburn
-
androidstudio
-
solarized- light
-
solarized- dark
-
xcode
-
vs
-
agate
-
darcula
-
dark
-
dracula
-
foundation
-
googlecode
-
monokai
-
mono- blue
-
far
-
gml
10.8. Markdown
MarkdownRenderPolicy
插件支持通过Markdown生成word文档。
10.8.1. 引入依赖:
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl-plugin-markdown</artifactId>
<version>1.0.3</version>
</dependency>
10.8.3. 示例
通过Markdown插件将poi-tl根目录下的README.md内容转为word文档的结果示例:markdown.docx
11. 示例
接下来的示例采取三段式output+template+data-model来说明,首先直接展示生成后的文档,然后一览模板的样子,最后我们对数据模型作个介绍。
11.1. 软件说明文档
需要生成这样的一份软件说明书:拥有封面和页眉,正文含有不同样式的文本,还有表格,列表和图片。poi_tl.docx
使用poi-tl标签制作模板,可以看到标签可以拥有样式。
这个示例向我们展示了poi-tl最基本的能力,它在模板标签位置,插入基本的数据模型,所见即所得。
11.2. 付款通知书
需要生成这样的一份流行的通知书:大部分数据是由表格构成的,需要创建一个订单的表格(图中第一个表格),还需要在一个已有表格中,填充货物明细和人工费数据(图中第二个表格)。下载最终生成的文件payment.docx
使用{{#order}}生成poi-tl提供的默认样式的表格,设置{{detail_table}}为自定义模板渲染策略(继承抽象表格策略DynamicTableRenderPolicy),自定义已有表格中部分单元格的渲染。
这个示例向我们展示了poi-tl在表格操作上的一些思考。示例中货物明细和人工费的表格就是一个相当复杂的表格,货物明细是由7列组成,行数不定,人工费是由4列组成,行数不定。
这个示例主要用来展示DynamicTableRenderPolicy的用法,货物明细和人工费仅仅是循环渲染表格行,使用LoopRowTableRenderPolicy 插件会更方便。
源码参见 JUnit PaymentExample |
11.3. 目标制定
需要制定一份OKR目标计划,业务目标和管理目标使用表格呈现,数量不等。下载最终生成的文件okr.docx
将表格放到区块对中,当区块对取值为空集合或者null则不会展示目标表格,当区块对是一个非空集合则循环展示表格。
这个示例展示了区块对的功能,它可以对文档内容进行循环渲染。
源码参见 JUnit OKRExample |
11.4. 野生动物现状
针对野生动物出具一份现状的调查报告,野生动物种类不确定,调查报告包含图片、文字和图表。下载最终生成的文件animal.docx
不确定动物种类使用区块对{{?animals}}的循环功能实现,图片和图表如模板所示,使用引用标签,在可选文字标题位置输入标签。
这个示例展示了区块对的循环功能,以及如何在循环中使用引用图片和引用图表的功能。
源码参见 JUnit AnimalExample |
11.5. 证书奖状
颁发一张由特殊样式图片、姓名、日期构成的证书奖状。下载最终生成的文件certificate.docx
图片格式和布局由模板指定,图片使用引用标签替换即可。
这个示例展示了引用图片和文本框的功能。
11.6. 个人简历
需要生成这样的一份个人简历:左侧是个人的基本信息,技术栈是个典型的列表,右侧是个人的工作经历,数量不定。下载最终生成的文件resume.docx
11.6.1. 方案一:使用区块对标签
工作经历是一个循环显示的内容,我们使用区块对标签{{?experiences}}{{/experiences}}。
11.6.2. 方案二:使用嵌套标签
工作经历可以使用嵌套标签,我们制作两个模板,一套主模板简历.docx(下图左侧),一套为文档模板segment.docx(下图右侧)。
看起来很复杂的简历,其实对于模版引擎来说,和普通的Word文档没有什么区别,我们只需要制作好一份简历,将需要替换的内容用模版标签代替。
因为模版即样式,模版引擎无需考虑样式,只关心数据,我们甚至可以制作10种不同样式的简历模板,用同一份数据去渲染。
源码参见 JUnit ResumeExample |
11.7. Swagger文档
这是一份非常专业的Swagger Word文档,样式优雅且有着清晰完整的文档结构,API列表需要循环展示,接口的请求参数需要循环展示,接口的返回值需要循环展示,数据类型支持锚点到具体的模型,模型支持代码块高亮展示。下载最终生成的文件swagger.docx
使用区块对标签完成所有循环功能,可以完美的支持有序和多级列表;表格使用 LoopRowTableRenderPolicy
插件的约定,可以非常方便的完成参数、返回值等表格的渲染;使用Spring表达式来支持丰富的条件判断;代码块高亮使用 HighlightRenderPolicy
插件。
SwaggerParser swaggerParser = new SwaggerParser();
Swagger swagger = swaggerParser.read("https://petstore.swagger.io/v2/swagger.json");
SwaggerView viewData = convert(swagger); (1)
LoopRowTableRenderPolicy LoopRowTableRenderPolicy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("parameters", hackLoopTableRenderPolicy)
.bind("responses", hackLoopTableRenderPolicy)
.bind("properties", hackLoopTableRenderPolicy)
.bind("definitionCode", new HighlightRenderPolicy())
.useSpringEL()
.build(); (2)
XWPFTemplate template = XWPFTemplate.compile("swagger.docx", config).render(viewData); (3)
template.writeToFile("out_example_swagger.docx");
1 | 解析Swagger.json |
2 | 配置模板引擎 |
3 | Swagger导出Word |
没错,一切都是如此简洁:简洁的导出代码 ,简洁的Word模板,甚至生成的Swagger文档都看起来那么简洁,愿一切如你所愿。
12. License
Apache License 2.0
14. 打赏个小费
poi-tl开源的初衷是希望让所有有需要的人享受Word模板引擎的功能,而且它可能是Java中最好的Word模板引擎。
如果你觉得它节省了你的时间,给你带来了方便和灵感,或者认同这个开源项目,可以为我的付出打赏点小费哦(在备注留言中附上你的微信号,让我可以加个好友,说句感谢❤️)。
poi-tl是给你的礼物!
11. VIP专属服务
你应该不需要VIP专属服务,除非:
-
Word技术需要具备一定的专业背景,你需要和作者进行1V1的专属高质量答疑
-
需要持续性的获得Word相关问题的解决方案
-
获得代码指导
-
单纯为知识付费
可以赞赏99元、199元,加作者微信成为VIP(请在赞赏留言中备注下你的微信号):
15. 常见问题
-
出现NoSuchMethodError 、ClassNotFoundException 、NoClassDefFoundError异常?
poi-tl依赖的apache-poi版本是4.1.2+,如果你的项目引用了低版本,请升级或删除。
-
是否支持Android客户端使用?
参考issue227。
-
有没有HTML转Word的插件?
参考issue219。
-
有没有公式的插件?
参考issue27。
-
如何通过标签指定格式化函数?
Spring表达式,应有尽有。
-
如何在一行中显示不同样式的文本?
可能你需要多个标签;或者使用区块对,区块对的集合数据是拥有不同样式的TextRenderData,还可以考虑使用ParagraphRenderPolicy插件。
-
我不是很熟悉Apache POI,我该怎么编写插件?
编写插件还是需要熟悉下POI,你可以参考现有插件的源码,或者Google下Apache POI的用法,这里有一个入门教程:Apache POI Word快速入门
-
Apache POI不支持的功能,我该怎么编写插件?
Apache POI底层的组件也是直接操作XML的,你可以使用POI背后的组件。