poi-tl(poi template language)是基于Apache POI的Word模板引擎,纯Java组件,跨平台,代码短小精悍,通过插件机制使其具有高度扩展性。

在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。

1. Maven

poi-tl分为两个版本,其中v1.5.x支持Apache poi3.16+版本和JDK1.6+。

  1. <dependency>
  2. <groupId>com.deepoove</groupId>
  3. <artifactId>poi-tl</artifactId>
  4. <version>1.5.0</version>
  5. </dependency>

Apache poi4.0.0+要求JDK1.8以上,需要引入最新的poi-tl版本:

  1. <dependency>
  2. <groupId>com.deepoove</groupId>
  3. <artifactId>poi-tl</artifactId>
  4. <version>1.6.0-beta1</version>
  5. </dependency>

2. Gradle

  1. compile group: 'com.deepoove', name: 'poi-tl', version: '1.5.0'

3. 快速开始

3.1. 2min入门

新建Word模板template.docx,包含内容{{title}}

{{title}}

0. 介绍 - 图1

3.2. Template:Word模板和样式

Word文档支持DOCX格式,所有的模板标签都是以 {{ 开头,以 }} 结尾,模板标签可以出现在任何非文本框的位置,包括页眉,页脚,表格内部等等。

表格布局可以设计出很多优秀专业的文档,模板文档请使用表格布局,不支持文本框。

poi-tl的一个核心特点是 数据模型与样式的分离,同样的数据模型可以用来渲染各种不同样式的模板。

文档的样式继承模板标签的样式,即如果模板{{title}}是蓝色微软雅黑加粗四号字体,则替换后的文本也是蓝色微软雅黑加粗四号字体。

style

3.3. Data-Model:数据源

数据源的结构体是一个标签名称和数据模型映射的集合,即[{标签名称, 数据模型}]。数据源可以是一个Map,其中key是标签名称;可以是一个JavaBean,field名称是标签名称,也可以通过注解@Name设置标签名称。

  1. // 模板标签d_number
  2. @Name("d_number")
  3. private String dNumber;
  4. // 模板标签m_vin
  5. private String m_vin;
poi-tl默认的表达式支持对象点缀式访问,比如名称为{{author.name}}的标签对应的数据是author对象的name属性值。表达式也支持中文,比如{{客户手机号}}。

数据模型实现了接口 public interface RenderData {} , 有以下几种数据模型:

  • TextRenderData、HyperLinkTextRenderData

  • PictureRenderData

  • MiniTableRenderData

  • NumbericRenderData

  • DocxRenderData

3.4. output:流

可以将最终结果渲染到任意输出流中,比如输出到文件流FileOutputStream生成新文档,输出到网络流ServletOutputStream供浏览器下载。

  1. // 输出到任何流
  2. template.write(OutputStream stream)
  3. // 便捷的输出到文件
  4. template.writeToFile(String path)

4. 语法

poi-tl內建了五种模板。

4.1. 文本模板{{var}}

{{var}}

TextRenderDataString 数据模型。

0. 介绍 - 图3

除了继承模板标签样式,也提供了通过代码设定文本样式的方式。

0. 介绍 - 图4

结构体只是数据模型的可视化展示,数据模型不是文本型的,而是Java对象。下文中出现的所有结构体也都如此。
文本换行使用 \n 字符。

4.2. 图片模板{{@var}}

{{@var}}

PictureRenderData 数据模型。

0. 介绍 - 图5

可以指定图片的宽度和高度,也支持 BufferedImage,这样我们可以利用Java生成任意图表插入到word文档中。

0. 介绍 - 图6

4.3. 表格模板{{#var}}

{{#var}}

poi-tl默认实现了N行N列的样式(如下图),同时提供了当数据为空时,展示一行空数据的文案(如下图中的No Data Descs),数据模型是 MiniTableRenderData

table0

0. 介绍 - 图8

0. 介绍 - 图9

表格的宽度(单位CM)怎么定义的: 页面宽度 - 页边距宽度 2 = 表格的最大宽度。页面宽度为A4(20.99 29.6,页边距为3.17 * 2.54)的文档最大表格宽度14.65CM。可以根据需要指定表格宽度。

需求的丰富多彩往往是默认表格样式无法满足的,我们通常会遇到以下两个场景:

场景一: 完全由自己掌控整个表格的生成:参见插件机制-新增RenderPolicy策略

场景二: 在一个已有的表格中,动态处理某些单元格数据:提供了抽象表格策略DynamicTableRenderPolicy,参见示例-付款通知书

4.4. 列表模板{{*var}}

{{*var}}

NumbericRenderData 数据模型。

0. 介绍 - 图10

列表样式支持罗马字符、有序无序等。参见NumbericRenderData.FMT_*。

  1. FMT_DECIMAL //1. 2. 3.
  2. FMT_DECIMAL_PARENTHESES //1) 2) 3)
  3. FMT_BULLET //● ● ●
  4. FMT_LOWER_LETTER //a. b. c.
  5. FMT_LOWER_ROMAN //i ⅱ ⅲ
  6. FMT_UPPER_LETTER //A. B. C.

4.5. 文档模板{{+var}}

{{+var}}

DocxRenderData 数据模型,可以是另一个docx文档的合并,或者是数据集合针对同一个模板的多个渲染结果的合并。

0. 介绍 - 图11

参见示例-一篇文章

5. 插件机制(Plugin mechanism)

插件机制是poi-tl的核心,默认的五大內建模板语法是通过插件方式加载的。插件的核心逻辑是在模板的基础上通过poi-tl和poi提供的API操作word文档,实现插件就是实现自己的渲染策略。

由于需要操作Word文档,我们需要掌握一些Apache POI API的知识,可以参见Apache官方API,这里也有一个快速入门的教程:Apache POI Word(docx) 入门示例教程。如果你写了一个不错的插件,欢迎提交Pull Request。

5.1. 新增RenderPolicy策略

內建策略是poi-tl自带的一些渲染策略,用来处理文本、图片、列表、表格、文档合并等:

  • TextRenderPolicy

  • PictureRenderPolicy

  • NumbericRenderPolicy

  • MiniTableRenderPolicy

  • DocxRenderPolicy

我们可以通过实现 RenderPolicy 接口扩展自己的渲染策略:

  1. public interface RenderPolicy {
  2. void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template); (1) (2) (3)
  3. }
1ElementTemplate是当前模板标签所在位置
2data是数据模型
3通过XWPFTemplate获得Apache POI增强类NiceXWPFDocument,继而可以在当前模板标签位置插入段落,图片,表格等
原则上Apache POI支持的操作,都可以在当前模板位置进行渲染。

示例:我们创建一个自己的表格渲染策略CustomTableRenderPolicy,使用表格API来操作表格,doc.insertNewTable() 是在当前模板位置插入表格,正如可以实现任何渲染逻辑一样,我们可以随心所欲的操作表格了。

AbstractRenderPolicy是一个抽象模板类,定义了一些骨架步骤并且将数据模型的校验和渲染逻辑分开,新的策略继承AbstractRenderPolicy类不是必须的。

  1. public class CustomTableRenderPolicy extends AbstractRenderPolicy<Object> {
  2. @Override
  3. protected void afterRender(RenderContext context) {
  4. // 清空模板标签所在段落
  5. clearPlaceholder(context, true);
  6. }
  7. @Override
  8. public void doRender(RunTemplate runTemplate, Object data, XWPFTemplate template)
  9. throws Exception {
  10. NiceXWPFDocument doc = template.getXWPFDocument();
  11. XWPFRun run = runTemplate.getRun();
  12. // 定义行列
  13. int row = 10, col = 8;
  14. // 插入表格
  15. XWPFTable table = doc.insertNewTable(run, row, col);
  16. // 定义表格宽度、边框和样式
  17. TableTools.widthTable(table, MiniTableRenderData.WIDTH_A4_FULL, col);
  18. TableTools.borderTable(table, 4);
  19. // 调用XWPFTable API操作表格:data对象可以包含任意你想要的数据,包括图片文本等
  20. // 调用MiniTableRenderPolicy.Helper.renderRow方法快速方便的渲染一行数据
  21. // 调用TableTools类方法操作表格,比如合并单元格
  22. // ......
  23. TableTools.mergeCellsHorizonal(table, 0, 0, 7);
  24. TableTools.mergeCellsVertically(table, 0, 1, 9);
  25. }
  26. }

5.2. 自定义模板策略

所有的插件都是通过如下构建器来配置:

  1. ConfigureBuilder builder = Configure.newBuilder();
  2. XWPFTemplate.compile("~/template.docx", builder.buid());

当我们有个模板标签为{{report}},它本身是一个文本模板,如果希望在这个位置做些不一样或者更复杂的事情,我们可以通过构建器设定模板的渲染策略:

  1. builder.customPolicy("report", new MyRenderPolicy());

5.3. 新增语法插件

比如增加%语法:{{%var}},对应自定义的渲染策略 PercentRenderPolicy,加载插件代码如下:

  1. builder.addPlugin('%', new PercentRenderPolicy());

由于內建模板也是通过插件方式加载的,我们甚至可以改变它们的语法:

  1. builder.addPlugin('@', new MiniTableRenderPolicy());
  2. builder.addPlugin('#', new PictureRenderPolicy());

这样{{@var}}就变成了表格模板,{{#var}}变成了图片模板,虽然不建议改变內建模板,但是从中可以看到poi-tl插件的设计思想,深藏功与名。

5.4. Helper辅助类

在內建策略中,通常会提供一个静态Helper辅助类,在我们实现自己的RenderPolicy时,也可以通过这些辅助类操作文档。

  1. // 某个位置渲染文本
  2. TextRenderPolicy.Helper.renderTextRun(XWPFRun, Object);
  3. // 某个位置渲染图片
  4. PictureRenderPolicy.Helper.renderPicture(XWPFRun, PictureRenderData);
  5. // 某个位置渲染列表
  6. NumbericRenderPolicy.Helper.renderNumberic(XWPFRun, NumbericRenderData);
  7. // 渲染表格的一行数据
  8. MiniTableRenderPolicy.Helper.renderRow(XWPFTable, int, RowRenderData);
  9. // 渲染单元格
  10. MiniTableRenderPolicy.Helper.renderCell(XWPFTableCell, CellRenderData, TableStyle)

6. 扩展功能

6.1. EL表达式:SpingEL

Spring Expression Language (SpEL)是一个强大的表达式语言,支持在运行时查询和操作对象图。poi-tl的表达式模板支持切换到SpEL模式。

  1. builder.setElMode(ELMode.SPEL_MODE);

关于SpEL的写法可以参见官网,下面给出一些典型的示例。

  1. {{name}}
  2. {{name.toUpperCase()}} (1)
  3. {{empty?:'这个字段为空'}}
  4. {{sex ? '男' : '女'}} (2)
  5. {{new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(time)}} (3)
  6. {{new java.text.SimpleDateFormat('yyyy-MM-dd hh:mm').format(time)}}
  7. {{price/10000 + '万元'}} (4)
  8. {{dogs[0].name}} (5)
  9. {{dogs[0].age}}
1方法调用
2三目运算符
3方法调用
4运算符
5数组列表
poi-tl提供了表达式计算接口 RenderDataCompute,它是一个很重要的扩展点,支持扩展任何的表达式引擎。SpEL模式是通过 SpELRenderDataCompute 实现。

6.2. 自定义语法

高度扩展性表现在其本身的语法也可以自定义,如果你不喜欢 {{}} 的方式,更偏爱freemarker ${} 的方式:

  1. builder.buildGramer("${", "}");

6.3. 模板生成模板

模板引擎不仅仅可以生成文档,也可以生成新的模板,比如我们想构造这样的新模板:把原先的一个模板标签分成两个模板标签:

  1. put("title", "{{title}}\n{{subtitle}}");

7. 示例

接下来的示例采取三段式output+template+data-model来说明,首先直接展示生成后的文档,然后一览模板的样子,最后我们对数据模型做个介绍。

7.1. 软件说明文档

output

需要生成这样的一份软件说明书:拥有封面和页眉,正文含有不同样式的文本,还有表格,列表和图片。下载最终生成的文件poi_tl.docx

example poitl output

template

使用poi-tl语法制作模板,可以看到模板标签不仅仅是模板,同样也是样式标签。

example poitl template

这个示例向我们展示了poi-tl最基本的能力,它在模板标签位置,插入基本的数据模型。同时也向我们展示了无需编码设置样式:模板,不仅仅是标签模板,还是样式模板。

源码参见Junit XWPFTemplateTest

7.2. 付款通知书

output

需要生成这样的一份流行的通知书:大部分数据是由表格构成的,需要创建一个订单的表格,还需要在一个已有表格中,填充货物明细和人工费数据。下载最终生成的文件payment.docx

example payment output

template

使用{{#order}}生成poi-tl提供的默认样式的表格,设置{{detail_table}}为自定义模板渲染策略(继承抽象表格策略DynamicTableRenderPolicy),自定义已有表格中部分单元格的渲染。

example payment template

这个示例向我们展示了poi-tl在表格操作上的一些思考。示例中货物明细和人工费的表格就是一个相当复杂的表格,货物明细是由7列组成,行数不定,人工费是由4列组成,行数不定。

默认表格数据模型(MiniTableRenderData)实现了最基本的样式,当需求中的表格更加复杂的时候,我们完全可以设计好那些固定的部分,将需要动态渲染的部分单元格交给自定义模板渲染策略。

poi-tl提供了抽象表格策略DynamicTableRenderPolicy来实现这样的功能,{{detail_table}}标签可以在表格内的任意单元格内,DynamicTableRenderPolicy会获取XWPFTable对象进而获得操作整个表格的能力。

  1. public abstract class DynamicTableRenderPolicy implements RenderPolicy {
  2. public abstract void render(XWPFTable table, Object data);
  3. }

新建渲染策略DetailTablePolicy,继承于抽象表格策略。

  1. public class DetailTablePolicy extends DynamicTableRenderPolicy {
  2. // 货品填充数据所在行数
  3. int goodsStartRow = 2;
  4. // 人工费填充数据所在行数
  5. int laborsStartRow = 5;
  6. @Override
  7. public void render(XWPFTable table, Object data) {
  8. if (null == data) return;
  9. DetailData detailData = (DetailData) data;
  10. // 人工费循环渲染
  11. List<RowRenderData> labors = detailData.getLabors();
  12. if (null != labors) {
  13. table.removeRow(laborsStartRow);
  14. // 循环插入行
  15. for (int i = 0; i < labors.size(); i++) {
  16. XWPFTableRow insertNewTableRow = table.insertNewTableRow(laborsStartRow);
  17. for (int j = 0; j < 7; j++) insertNewTableRow.createCell();
  18. // 合并单元格
  19. TableTools.mergeCellsHorizonal(table, laborsStartRow, 0, 3);
  20. // 渲染单行人工费数据
  21. MiniTableRenderPolicy.Helper.renderRow(table, laborsStartRow, labors.get(i));
  22. }
  23. }
  24. // 货品明细
  25. List<RowRenderData> goods = detailData.getGoods();
  26. if (null != goods) {
  27. table.removeRow(goodsStartRow);
  28. for (int i = 0; i < goods.size(); i++) {
  29. XWPFTableRow insertNewTableRow = table.insertNewTableRow(goodsStartRow);
  30. for (int j = 0; j < 7; j++) insertNewTableRow.createCell();
  31. // 渲染单行货品明细数据
  32. MiniTableRenderPolicy.Helper.renderRow(table, goodsStartRow, goods.get(i));
  33. }
  34. }
  35. }
  36. }

将模板标签{{detail_table}}设置成此策略。

  1. Configure config = Configure.newBuilder().customPolicy("detail_table", new DetailTablePolicy()).build();
源码参见Junit PaymentExample

7.3. 一篇文章

output

需要生成这样的一系列文章:除了标题作者之外,它的内容是有规律的,内容是由一行蓝色的标题,一段文字,一张图片构成。下载最终生成的文件story.docx

example story output

template

文章的内容是个典型的文档模板类型,我们制作一个待合并的文档模板segment.docx(下图右侧),主模板story.docx看起来很简单,其中{{+segment}}标签将会被文档模板循环合并。

example story template

这个示例充分展示了poi-tl的文档模板和循环功能。当有一段固定样式的段落,根据集合数据循环填充后展示。示例中标题+文字+图片就是这样的可重复段落。

基本原理是后台提供数据模型的集合,不断渲染segment.docx,将渲染结果合并到story.docx文档中。

源码参见Junit StoryExample

7.4. 个人简历

output

需要生成这样的一份流行的个人简历:左侧是个人的基本信息,技术栈是个典型的列表,右侧是个人的工作经历,数量不定。下载最终生成的文件resume.docx

example resume output

template

工作经历是个典型的文档模板类型,我们制作两个模板,一套主模板简历.docx(下图左侧),一套为文档模板segment.docx(下图右侧)。

example resume template

看起来很复杂的简历,其实对于模版引擎来说,和普通的Word文档没有什么区别,我们只需要制作好一份简历,将需要替换的内容用模版标签代替。

因为模版即样式,模版引擎无需考虑样式,只关心数据,我们甚至可以制作10种不同样式的简历模板,用同一份数据去渲染。

源码参见Junit ResumeExample

8. License

Apache License 2.0

9. 源码

GitHub

10. 打赏个小费

poi-tl开源的初衷是希望让所有有需要的人享受Word模板引擎的功能,如果你觉得它节省了你的时间,给你带来了方便和灵感,或者认同这个开源项目,可以为我的付出打赏点小费哦。

pay

11. 常见问题

  • 出现NoSuchMethodError 、ClassNotFoundException 、NoClassDefFoundError异常?

poi-tl依赖的apache-poi版本是3.16+,如果你的项目引用了低版本,请升级或删除。

  • 是否支持文本框?

不支持,表格布局可以设计出几乎所有优秀专业的文档,请使用表格。

  • 是否支持生成目录?

暂不支持,参考原生POI API自行扩展。

  • 是否支持Android客户端使用?

未知,有些朋友尝试成功,但我尚未在Android环境中验证过。

  • 是否支持设置多种标题样式?

不能够直接设置,目前支持设置文字字体大小等,或者在已经应用了标题样式的模板中替换文本。

  • 有没有提供图表、数学公式模板?

暂不支持,如果是简单的图表,可以考虑通过Java提供的 BufferedImage 类创建图片后插入。