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

支持DOCX格式的Word模板。

1. Maven

<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.3.1</version>
</dependency>

2. Gradle

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

3. 快速开始

3.1. 2min入门

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

{{title}}
代码示例
XWPFTemplate template = XWPFTemplate.compile("~/template.docx").render(new HashMap<String, Object>(){{ (1) (2)
        put("title", "Poi-tl 模板引擎");
}});
FileOutputStream out = new FileOutputStream("out_template.docx");
template.write(out); (3)
out.flush();
out.close();
template.close();
1 编译模板
2 渲染数据
3 输出到流

TDO模式:Template + data-model = output

3.2. Template:Word模板和样式

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

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

文档的样式继承模板标签的样式,这样我们只需要提前设计好模板样式即可,即如果模板{{title}}是蓝色微软雅黑加粗四号字体,则替换后的文本也是蓝色微软雅黑加粗四号字体。

style

3.3. Data-Model:数据源

数据源的结构体是个键值对的集合:[{标签名称, 数据模型}]。除了使用Map之外,还可以使用Java Object。

Map的Key是标签名称,默认Class中的field名称是标签名称,也可以通过注解@Name设置标签名称。

// 模板标签d_number
@Name("d_number")
private String dNumber;
// 模板标签m_vin
private String m_vin;

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

  • TextRenderData

  • PictureRenderData

  • MiniTableRenderData

  • NumbericRenderData

  • DocxRenderData

3.4. output:流

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

4. 语法

poi-tl內建了五种模板。

4.1. 文本模板{{var}}

{{var}}

TextRenderDataString 数据模型。

代码示例
put("author", new TextRenderData("000000", "Sayi卅一"));
put("introduce", "http://www.deepoove.com");

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

TextRenderData 的结构体
{
  "text": "Sayi",
  "style": {
    "strike": false, (1)
    "bold": true, (2)
    "italic": false, (3)
    "color": "00FF00", (4)
    "underLine": false, (5)
    "fontFamily": "微软雅黑", (6)
    "fontSize": 12 (7)
  }
}
1 删除线
2 粗体
3 斜体
4 颜色
5 下划线
6 字体
7 字号
结构体只是数据模型的可视化展示,数据模型不是文本型的,而是Java对象。下文中出现的所有结构体也都如此。
文本换行使用 \n 字符。

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

{{@var}}

PictureRenderData 数据模型。

代码示例
// 本地图片
put("localPicture", new PictureRenderData(120, 120, "./sayi.png"));

// 本地图片byte数据
byte[] localByteArray = BytePictureUtils.getLocalByteArray(new File("./logo.png"));
put("localBytePicture", new PictureRenderData(100, 120, ".png", localByteArray));

// 网络图片
put("urlPicture", new PictureRenderData(100, 100, ".png", BytePictureUtils.getUrlByteArray("https://avatars3.githubusercontent.com/u/1394854")));

// java 图片
put("bufferImagePicture", new PictureRenderData(100, 120, ".png", BytePictureUtils.getBufferByteArray(bufferImage)));

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

PictureRenderData 的结构体
{
  "path": "", (1)
  "data": [], (2)
  "width": 100, (3)
  "height": 100 (4)
}
1 图片路径
2 图片也可以是byte[]字节数组
3 宽度
4 高度

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

{{#var}}

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

table0
MiniTableRenderData 的结构体
{
  "datas": [ (1)
    {
      "rowData": [TextRenderData],
      "style": {
        "align": "center",
        "backgroundColor": "ff9800"
    }
    }
  ],
  "headers": { (2)
    "rowData": [TextRenderData],
    "style": { (3)
      "align": "center",
      "backgroundColor": "ff9800"
    }
  },
  "noDatadesc": "No Data Desc", (4)
  "style": { (5)
      "align": "center"
    }
  "width": 14.65 (6)
}
1 定义表格数据,单元格数据由 TextRenderData 指定。
2 定义表格头
3 行样式:行数据的对齐方式,行背景色
4 没有数据的展示文案
5 表格样式:表格居左、居中、居右对齐
6 表格宽度,单位cm
代码示例
RowRenderData header = RowRenderData.build(new TextRenderData("FFFFFF", "姓名"), new TextRenderData("FFFFFF", "学历"));

RowRenderData row0 = RowRenderData.build("张三", "研究生");
RowRenderData row1 = RowRenderData.build("李四", "博士");
RowRenderData row2 = RowRenderData.build("王五", "博士后");

put("table", new MiniTableRenderData(header, Arrays.asList(row0, row1, row2)));
表格的宽度(单位CM)怎么定义的: 页面宽度-页边距宽度*2=表格的最大宽度。 默认宽度为A4(20.99*29.6,页边距为3.17*2.54)的最大宽度14.65CM。可以根据需要指定表格宽度。

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

  1. 完全由自己掌控整个表格的生成:参见插件机制-自定义模板策略

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

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

{{*var}}

NumbericRenderData 数据模型。

代码示例
put("feature", new NumbericRenderData(new ArrayList<TextRenderData>() {
  {
    add(new TextRenderData("Plug-in grammar"));
    add(new TextRenderData("Supports word text, header..."));
    add(new TextRenderData("Not just templates, but also style templates"));
  }
}));

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

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

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

{{+var}}

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

代码示例
List<SegmentData> segments = new ArrayList<SegmentData>();
SegmentData s1 = new SegmentData();
s1.setTitle("经常抱怨的自己");
s1.setContent("每个人生活得都不容易,经常向别人抱怨的人,说白了就是把对方当做“垃圾场”,你一股脑地将自己的埋怨与不满倒给别人,自己倒是爽了,你有考虑过对方的感受吗?对方的脸上可能一笑了之,但是心里可能有一万只草泥马奔腾而过。");
segments.add(s1);

SegmentData s2 = new SegmentData();
s2.setTitle("拖拖拉拉的自己");
s2.setContent("能够今天做完的事情,不要拖到明天,你的事情没有任何人有义务去帮你做;不要做“宅男”、不要当“宅女”,放假的日子约上三五好友出去转转;经常动手做家务,既能分担伴侣的负担,又有一个干净舒适的环境何乐而不为呢?");
segments.add(s2);

put("docx_word", new DocxRenderData(new File("~/segment.docx"), segments)); (1)
1 segment.docx是一个包含了{{title}}和{{content}}的模板,使用segments集合数据渲染模板后合并

5. 插件机制(Plugin mechanism)

插件机制使得poi-tl具有高度扩展性,默认的五大內建模板语法是通过插件方式加载的,所以可以轻松的增加新的语法插件,也可以很轻松的处理任意模板标签。

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

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

5.1. 自定义模板渲染策略

比如我们有个模板标签为{{report}},如果希望在这个位置做些更复杂的事情,我们可以指定该模板对应的策略。你可以通过实现下面的接口实现新的渲染策略:

public interface RenderPolicy {
  void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template);
}

通过ElementTemplate获得当前模板位置,通过data获得渲染数据,通过XWPFTemplate获得Apache POI增强类NiceXWPFDocument,继而可以在指定位置插入段落,图片,表格等。

接下来可以通过构建器设定模板的渲染策略:

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

5.2. 新增语法插件

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

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

5.3. 自定义语法

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

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

6. 示例

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

6.1. 软件说明文档

output

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

example poitl output
template

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

example poitl template

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

6.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来实现类似的功能,通过持有table对象获得操作整个表格的能力。

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

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

public class DetailTablePolicy extends DynamicTableRenderPolicy {

  // 货品填充数据所在行数
  int goodsStartRow = 2;
  // 人工费填充数据所在行数
  int laborsStartRow = 5;

  @Override
  public void render(XWPFTable table, Object data) {
    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();

        // 合并单元格
        NiceXWPFDocument.mergeCellsHorizonal(table, laborsStartRow, 0, 3);
        // 渲染单行人工费数据
        MiniTableRenderPolicy.renderRow(table, 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();
        // 渲染单行货品明细数据
        MiniTableRenderPolicy.renderRow(table, goodsStartRow, goods.get(i));
      }
    }
  }
}

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

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

6.3. 一篇文章

output

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

example story output
template

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

example story template

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

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

源码参见Junit StoryExample

6.4. 个人简历

output

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

example resume output
template

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

example resume template

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

因为模版即样式,模版引擎无需考虑样式,只关心数据。

源码参见Junit ResumeExample

7. License

Apache License 2.0

8. 源码