当前位置: 首页 > 工具软件 > poi-tl > 使用案例 >

poi-tl(word模板渲染)

诸葛品
2023-12-01

poi-tl(poi-template-language)

序言

方案移植性功能性易用性
Poi-tlJava跨平台Word模板引擎,基于Apache POI,提供更友好的API低代码,准备文档模板和数据即可
Apache POIJava跨平台Apache项目,封装了常见的文档操作,也可以操作底层XML结构文档不全,这里有一个教程:Apache POI Word快速入门
FreemarkerXML跨平台仅支持文本,很大的局限性不推荐,XML结构的代码几乎无法维护
OpenOffice部署OpenOffice,移植性较差-需要了解OpenOffice的API
HTML浏览器导出依赖浏览器的实现,移植性较差HTML不能很好的兼容Word的格式,样式糟糕-
Jacob、winlibWindows平台-复杂,完全不推荐使用

第一部分 什么是poi-tl

​ FreeMarker或Velocity基于文本模板和数据生成新的html页面或配置文件。poi-tl是一个基于Word模板和数据生成新文档的Word模板引擎。

​ Word模板样式丰富。Poi-tl将在生成的文档中完美地保留模板中的样式。您还可以设置标签的样式。标签的样式将应用于替换的文本,因此您可以专注于模板设计。

​ poi-tl是一个“无逻辑”的模板引擎。没有复杂的控制结构和变量赋值,只有标签,有些标签可以用文字、图片、表格等代替。,一些标签会隐藏某些文档内容,而另一些标签会循环一系列文档内容。

​ poi-tl支持自定义函数(插件),函数可以在Word模板的任何地方执行,在文档的任何地方做任何事情是poi-tl的目标。

特性描述
Text将标签呈现为文本
Picture将标签渲染为图片
Table将标签呈现为表格
Numbering将标签呈现为编号
Chart条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼状图(3D饼状图)等图表渲染
If Condition根据条件隐藏或显示某些文档内容(包括文本、段落、图片、表格、列表、图表等。)
Foreach Loop循环浏览某些文档内容(包括文本、段落、图片、表格、列表、图表等。)根据收藏
Loop table row循环来复制呈现的表格中的一行
Loop table column循环复制并呈现表中的一列
Loop ordered list支持有序列表的循环,同时支持多级列表
Highlight code代码块的单词高亮显示,支持26种语言和数百种颜色样式
Markdown将Markdown转换为word文档
Word attachment在Word中插入附件
Word Comments完成支持评论、创建评论、修改评论等。
Word SDT完全支持结构化文档标签
Textbox支持文本框中的标签
Picture replacement用另一张图片替换原始图片
bookmarks, anchors, hyperlinks支持在文档中设置书签、锚点和超链接
Expression Language完全支持SpringEL表达式,并可以扩展更多的表达式:OGNL,MVEL…
Style模板就是样式,代码也可以设置样式
Template nesting模板包含子模板,子模板又包含子模板
MergeWord 合并,也可以在指定位置合并
custom functions (plug-ins)插件设计,在文档的任何地方执行功能

第二部分 Maven

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

NOTE: poi-tl 1.12.x 要求 POI 版本在 5.2.2+.

Apache POI已经进入5.0.0+时代,如果你仍希望使用低版本的Apache POI,请查阅历史版本。

V1.12.0版本作了一个不兼容的改动,升级的时候需要注意:

  • 重构了PictureRenderData,改为抽象类,建议使用Pictures工厂方法来创建图片数据

第三部分 快速开始

简单的使用案例:我们使用poi-tl模板引擎替换word模板中的{{title}}

1、创建一个新的word文档template.docx,里面包含了一个标签{{title}}

2、TDO模式:Template + data-model = output

public static void main(String[] args) throws IOException {
    XWPFTemplate.compile("D:\\template.docx").render(new HashMap<String, Object>(){{
    	put("title", "poi-tl template engine");
    }}).writeToFile("D:\\out_template.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);

第四部分 Tags

​ 标签由两个花括号组成,{{title}}是标签,{{?title}}也是标签,title是标签的名称,而?标识标签的类型。接下来,我们来认识一下有哪些标签类型。

4.1 文本

文本标签{{var}}

数据模型:

  • String :文本
  • TextRenderData :有样式的文本
  • HyperlinkTextRenderData :超链接和锚点文本
  • Object :调用 toString() 方法转化为文本

4.1.1 代码示例

put("name", "Sayi");
put("author", new TextRenderData("000000", "Sayi"));
put("link", new HyperlinkTextRenderData("website", "http://deepoove.com"));
put("anchor", new HyperlinkTextRenderData("anchortxt", "anchor:appendix1"));

4.1.2 链式代码示例

除了new操作符,还提供了更加优雅的工厂 Texts 和链式调用的方式轻松构建文本模型。

put("author", Texts.of("Sayi").color("000000").create());
put("link", Texts.of("website").link("http://deepoove.com").create());
put("anchor", Texts.of("anchortxt").anchor("appendix1").create());

所见即所得,标签的样式会应用到替换后的文本上,也可以通过代码设定文本的样式。

4.1.3 TextRenderData的结构体

{
  "text": "Sayi",
  "style": {
    "strike": false, 
    "bold": true, 
    "italic": false, 
    "color": "00FF00", 
    "underLine": false, 
    "fontFamily": "微软雅黑", 
    "fontSize": 12, 
    "highlightColor": "green", 
    "vertAlign": "superscript", 
    "characterSpacing" : 20 
  }
}

4.2 图片

图片标签以@开始:{{@var}}

数据模型:

  • String :图片url或者本地路径,默认使用图片自身尺寸
  • PictureRenderData
  • ByteArrayPictureRenderData
  • FilePictureRenderData
  • UrlPictureRenderData

推荐使用工厂 Pictures 构建图片模型。

4.2.1 代码示例

// 指定图片路径
put("image", "logo.png");
// svg图片
put("svg", "https://img.shields.io/badge/jdk-1.6%2B-orange.svg");

// 设置图片宽高
put("image1", Pictures.ofLocal("logo.png").size(120, 120).create());

// 图片流
put("streamImg", Pictures.ofStream(new FileInputStream("logo.jpeg"), PictureType.JPEG)
  .size(100, 120).create());

// 网络图片(注意网络耗时对系统可能的性能影响)
put("urlImg", Pictures.ofUrl("http://deepoove.com/images/icecream.png")
  .size(100, 100).create());

// java图片
put("buffered", Pictures.ofBufferedImage(bufferImage, PictureType.PNG)
  .size(100, 100).create());

图片支持BufferedImage,这意味着我们可以利用Java生成图表插入到word文档中。

4.2.2 FilePictureRenderData的结构体

{
  "pictureType" : "PNG", 
  "path": "logo.png", 
  "pictureStyle": {
    "width": 100, 
    "height": 100 
  },
  "altMeta": "图片不存在" 
}

4.3 表格

表格标签以#开始:{{#var}}

数据模型:

  • TableRenderData

推荐使用工厂 TablesRowsCells 构建表格模型。

4.3.1 Example 1. 基础表格示例

// 一个2行2列的表格
put("table0", Tables.of(new String[][] {
                new String[] { "00", "01" },
                new String[] { "10", "11" }
            }).border(BorderStyle.DEFAULT).create());
0001
1011

4.3.2 Example 2. 表格样式示例

// 第0行居中且背景为蓝色的表格
RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF")
      .bgColor("4472C4").center().create();
RowRenderData row1 = Rows.create("李四", "博士");
put("table1", Tables.create(row0, row1));
姓名学历
李四博士

4.3.3 Example 3. 表格合并示例

// 合并第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());
列0列1列2
没有数据

TableRenderData表格模型在单元格内可以展示文本和图片,同时也可以指定表格样式、行样式和单元格样式,而且在N行N列渲染完成后可以应用单元格合并规则 MergeCellRule ,从而实现更复杂的表格。

4.3.4 TableRenderData的结构体

{
  "rows": [ 
    {
      "cells": [ 
        {
          "paragraphs": [ 
            {
              "contents": [
                {
                  [TextRenderData] 
                },
                {
                  [PictureRenderData] 
                }
              ],
              "paragraphStyle": null 
            }
          ],
          "cellStyle": { 
            "backgroundColor": "00000",
            "vertAlign": "CENTER"
          }
        }
      ],
      "rowStyle": { 
        "height": 2.0f
      }
    }
  ],
  "tableStyle": { 
    "width": 14.63f, 
    "colWidths": null
  },
  "mergeRule": { 
    "mapping": {
      "0-0": "1-2"
    }
  }
}

4.4 列表

列表标签以*开始:{{*var}}

数据模型:

  • List<String>
  • NumberingRenderData

推荐使用工厂 Numberings 构建列表模型。

4.4.1 代码示例

put("list", Numberings.create("Plug-in grammar",
                    "Supports word text, pictures, table...","Not just templates"));

编号样式支持罗马字符、有序无序等,可以通过 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.

4.5 区块对

区块对由前后两个标签组成,开始标签以?标识,结束标签以/标识:{{?sections}}{{/sections}}

区块对开始和结束标签中间可以包含多个图片、表格、段落、列表、图表等,开始和结束标签可以跨多个段落,也可以在同一个段落,但是如果在表格中使用区块对,开始和结束标签必须在同一个单元格内,因为跨多个单元格的渲染行为是未知的。

区块对在处理一系列文档元素的时候非常有用,位于区块对中的文档元素可以被渲染零次,一次或N次,这取决于区块对的取值。

  • False或空集合

    隐藏区块中的所有文档元素

  • 非False且不是集合

    显示区块中的文档元素,渲染一次

  • 非空集合

    根据集合的大小,循环渲染区块中的文档元素

4.5.1 False或空集合

如果区块对的值是 null 、false 或者空的集合,位于区块中的所有文档元素将不会显示,这就等同于if语句的条件为 false。

4.5.1.1 数据模型
{
  "announce": false
}
4.5.1.2 模板
template.docx
Made it,Ma!{{?announce}}Top of the world!{{/announce}}

Made it,Ma!

{{?announce}}

Top of the world!

{{/announce}}
4.5.1.3 结果
output.docx
Made it,Ma!

Made it,Ma!

4.5.2 非False且不是集合

如果区块对的值不为 null 、 false ,且不是集合,位于区块中的所有文档元素会被渲染一次,这就等同于if语句的条件为 true。

4.5.2.1 数据模型
{
  "person": { "name": "Sayi" }
}
4.5.2.2 模板
template.docx
{{?person}}

Hi {{name}}!

{{/person}}
4.5.2.3 结果
output.docx
Hi Sayi!

4.5.3 非空集合

如果区块对的值是一个非空集合,区块中的文档元素会被迭代渲染一次或者N次,这取决于集合的大小,类似于foreach语法。

4.5.3.1 数据模型
{
  "songs": [
    { "name": "Memories" },
    { "name": "Sugar" },
    { "name": "Last Dance" }
  ]
}
4.5.3.2 模板
template.docx
{{?songs}}

{{name}}

{{/songs}}
4.5.3.3 结果
output.docx
Memories

Sugar

Last Dance
4.5.3.4 循环内置变量

在循环中提供了一些内置变量,这些内置变量只能用于区块对中。

变量类型说明
_indexint返回当前迭代从0开始的索引
_is_firstboolean辨别循环项是否是当前迭代的第一项。
_is_lastboolean辨别循环项是否是当前迭代的最后一项。
_has_nextboolean辨别循环项是否是有下一项。
_is_even_itemboolean辨别循环项是否是当前迭代间隔1的奇数项。
_is_odd_itemboolean辨别循环项是否是当前迭代间隔1的偶数项。
#thisobject引用当前对象,由于#和已有表格标签标识冲突,所以在文本标签中需要使用=号标识来输出文本。
4.5.3.4.1 示例数据
{
  "produces": [
    "application/json",
    "application/xml"
  ]
}
4.5.3.4.2 模板
{{?produces}}
{{_index + 1}}. {{=#this}}
{{/produces}}
4.5.3.4.3 结果
1. application/json
2. application/xml

4.6 嵌套

嵌套又称为导入、包含或者合并,以+标识:{{+var}}

数据模型:

  • DocxRenderData

推荐使用工厂 Includes 构建嵌套模型。

4.6.1 代码示例

class AddrModel {
  private String addr;
  public AddrModel(String addr) {
    this.addr = addr;
  }
  // Getter/Setter
}

List<AddrModel> subData = new ArrayList<>();
subData.add(new AddrModel("Hangzhou,China"));
subData.add(new AddrModel("Shanghai,China"));
put("nested", Includes.ofLocal("sub.docx").setRenderModel(subData).create());  

4.7 引用标签

引用标签是一种特殊位置的特殊标签,提供了直接引用文档中的元素句柄的能力,这个重要的特性在我们只想改变文档中某个元素极小一部分样式和属性的时候特别有用,因为其余样式和属性都可以在模板中预置好,真正的所见即所得

4.7.1 图片

引用图片标签是一个文本:{{var}},标签位置在:设置图片格式—可选文字—标题或者说明(新版本Microsoft Office标签位置在:编辑替换文字-替换文字)。

引用图片标签只会替换图片而不会改变图片尺寸和布局,数据模型和图片标签一致:PictureRenderData

4.7.1.1 代码示例
put("img", Pictures.ofLocal("sayi.png").create());

4.7.2 多系列图表

多系列图表指的是条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、散点图等。

多系列图表的标签是一个文本:{{var}},标签位置在:图表区格式—可选文字—标题(新版本Microsoft Office标签位置在:编辑替换文字-替换文字)。

数据模型:

  • ChartMultiSeriesRenderData

推荐使用工厂 Charts 构建图表模型。

4.7.2.1 代码示例
ChartMultiSeriesRenderData chart = Charts
                .ofMultiSeries("ChartTitle", new String[] { "中文", "English" })
                .addSeries("countries", new Double[] { 15.0, 6.0 })
                .addSeries("speakers", new Double[] { 223.0, 119.0 })
                .create();
put("barChart", chart);

新的图表系列数据会完全替换原有图表数据,而原有图表的样式都会被保留。

4.7.2.2 ChartMultiSeriesRenderData的结构体
{
  "chartTitle": "ChartTitle", 
  "categories": [ 
    "中文", "English"
  ],
  "seriesDatas": [ 
    {
      "name": "countries", 
      "values": [ 
        15, 6
      ]
    },
    {
      "name": "speakers",
      "values": [
        223, 119
      ]
    }
  ]
}

4.7.3 单系列图表

单系列图表指的是饼图(3D饼图)、圆环图等。

单系列图表的标签是一个文本:{{var}},标签位置在:图表区格式—可选文字—标题(新版本Microsoft Office标签位置在:编辑替换文字-替换文字)。

数据模型:

  • ChartSingleSeriesRenderData

推荐使用工厂 Charts 构建图表模型。

4.7.3.1 代码示例
ChartSingleSeriesRenderData pie = Charts
                .ofSingleSeries("ChartTitle", new String[] { "美国", "中国" })
                .series("countries", new Integer[] { 9826675, 9596961 })
                .create();

put("pieChart", pie);
4.7.3.2 ChartSingleSeriesRenderData的结构体
{
  "chartTitle": "ChartTitle", 
  "categories": [ 
    "美国",
    "中国"
  ],
  "seriesData": { 
    "name": "countries", 
    "values": [ 
      9826675,
      9596961
    ]
  }
}

4.7.4 组合图表

组合图表指的是由多系列图表(柱形图、折线图、面积图)组合而成的图表。

组合图表的标签是一个文本:{{var}},标签位置在:图表区格式—可选文字—标题(新版本Microsoft Office标签位置在:编辑替换文字-替换文字)。

同多系列图表 ChartMultiSeriesRenderData 数据模型。

4.7.4.1 代码示例
ChartSingleSeriesRenderData comb = Charts
                .ofComboSeries("MyChart", new String[] { "中文", "English" })
                .addBarSeries("countries", new Double[] { 15.0, 6.0 })
                .addBarSeries("speakers", new Double[] { 223.0, 119.0 })
                .addBarSeries("NewBar", new Double[] { 223.0, 119.0 })
                .addLineSeries("youngs", new Double[] { 323.0, 89.0 })
                .addLineSeries("NewLine", new Double[] { 123.0, 59.0 }).create();

put("combChart", comb);
4.7.4.2 ChartMultiSeriesRenderData的结构体
{
  "chartTitle": "MyChart", 
  "categories": [ 
    "中文", "English"
  ],
  "seriesDatas": [ 
    {
      "name": "countries", 
      "comboType": "BAR", 
      "values": [ 
        15, 6
      ]
    },
    {
      "name": "speakers",
      "comboType": "LINE",
      "values": [
        223, 119
      ]
    }
  ]
}

第五部分 配置

poi-tl提供了类 Configure 来配置常用的设置,使用方式如下:

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

5.1 前后缀

poi-tl使用 {{}} 的方式来标识对Google CTemplate的尊敬,如果你更偏爱freemarker ${} 的方式,可以自定义:

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

5.2 标签类型

默认的图片标签是以@开始,如果你希望使用%开始作为图片标签:

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

如果你不是很喜欢默认的标签标识类型,你也可以自由更改:

builder.addPlugin('@', new TableRenderPolicy());
builder.addPlugin('#', new PictureRenderPolicy());

这样{{@var}}就变成了表格标签,{{#var}}变成了图片标签,虽然不建议改变默认标签标识,但是从中可以看到poi-tl插件的灵活度,在插件章节中我们将会看到如何自定义自己的标签。

5.3 标签正则

标签默认支持中文、字母、数字、下划线的组合,我们可以通过正则表达式来配置标签的规则,比如不允许中文:

builder.buildGrammerRegex("[\\w]+(\\.[\\w]+)*");

比如允许除了标签前后缀外的任意字符:

builder.buildGrammerRegex(RegexUtils.createGeneral("{{", "}}"));

5.4 计算标签值

计算标签值是指如何在数据模型中索引标签Key的值,可以完全自定义获取标签值的方式。

builder.setRenderDataComputeFactory(new RenderDataComputeFactory());

RenderDataComputeFactory是一个抽象工厂,你可以定义自己的工厂提供标签表达式计算接口 RenderDataCompute 的实现。

我们可以通过此方式支持任何的表达式引擎,Spring表达式正是通过 SpELRenderDataCompute 实现。

5.5 Spring表达式

Spring Expression Language 是一个强大的表达式语言,支持在运行时查询和操作对象图,可作为独立组件使用,需要引入相应的依赖:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-expression</artifactId>
  <version>5.3.18</version>
</dependency>

为了在模板标签中使用SpringEL表达式,需要将标签配置为SpringEL模式:

builder.useSpringEL();

5.5.1 基本使用

关于SpringEL的写法可以参见官方文档,下面给出一些典型的示例。

{{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日'))}} 

5.5.2 SpringEL作为区块对的条件

Spring表达式与区块对结合可以实现更强大的功能,示例如下:

5.5.2.1 数据模型
{
  "desc": "",
  "summary": "Find A Pet",
  "produces": [
    "application/xml"
  ]
}
5.5.2.2 模板
template.docx
{{?desc == null or desc == ''}}{{summary}}{{/}}

{{?produces == null or produces.size() == 0}}无{{/}}
5.5.2.3 结果
output.docx
Find A Pet

5.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());

5.7 错误处理

poi-tl支持在发生错误的时候定制引擎的行为。

5.7.1 标签无法被计算

标签无法被计算的场景有几种,比如模板中引用了一个不存在的变量,或者级联的前置结果不是一个哈希,如 {{author.name}} 中author的值为null,此时就无法计算name的值。

poi-tl可以在发生这种错误时对计算结果进行配置,默认会认为标签值为null。当我们需要严格校验模板是否有人为失误时,可以抛出异常:

builder.useDefaultEL(true); 

注意的是,如果使用SpringEL表达式,可以通过参数来配置是否抛出异常:

uilder.useSpringEL(true);

5.7.2 标签数据类型不合法

我们知道渲染图片、表格等标签时对数据模型是有要求的,如果数据不合法(为NULL或者是一个错误的数据类型),可以配置模板标签的渲染行为。

poi-tl默认的行为会清空标签,如果希望对标签不作任何处理:

builder.setValidErrorHandler(new DiscardHandler());

如果希望执行严格的校验,直接抛出异常:

builder.setValidErrorHandler(new AbortHandler());

5.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);

5.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);

5.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级别的日志会打印解析渲染过程中的信息,有利于程序调试,另外在模板引擎执行结束后会打印耗时信息:

第六部分 插件

插件,又称为自定义函数,它允许用户在模板标签位置处执行预先定义好的函数。由于插件机制的存在,我们几乎可以在模板的任何位置执行任何操作。

插件是poi-tl的核心,默认的标签和引用标签都是通过插件加载。

6.1 默认插件

poi-tl默认提供了八个策略插件,用来处理文本、图片、列表、表格、文档嵌套、引用图片、引用多系列图表、引用单系列图表等:

  • TextRenderPolicy
  • PictureRenderPolicy
  • NumberingRenderPolicy
  • TableRenderPolicy
  • DocxRenderPolicy
  • MultiSeriesChartTemplateRenderPolicy
  • SingleSeriesChartTemplateRenderPolicy
  • DefaultPictureTemplateRenderPolicy

由于这八个插件如此通用,因此将这些插件注册为不同的标签类型,从而搭建了poi-tl的标签体系,也构筑了poi-tl高度自由的插件机制。

6.2 开发一个插件

实现一个插件就是要告诉我们在模板的某个地方用某些数据做某些事情,我们可以通过实现RenderPolicy接口开发自己的插件:

/ **
  *	ElementTemplate是当前标签位置
  *	data是数据模型
  *	XWPFTemplate代表整个模板
  *
  */
public interface RenderPolicy {
  void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template);   
}

接下来我们写一个将标签替换为Hello, world的插件:

public class HelloWorldRenderPolicy implements RenderPolicy {

  @Override
  public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
    XWPFRun run = ((RunTemplate) eleTemplate).getRun(); 
    // String thing = String.valueOf(data);
    String thing = "Hello, world";
    run.setText(thing, 0); 
  }
}

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来操作表格。

6.3 使用插件

插件开发好后,为了让插件在某个标签处执行,我们需要将插件与标签绑定。

6.3.1 将插件应用到标签

当我们有个模板标签为 {{report}},默认是文本标签,如果希望在这个位置做些不一样或者更复杂的事情,我们可以将插件应用到这个模板标签:

ConfigureBuilder builder = Configure.builder();
builder.bind("report", new CustomTableRenderPolicy());

此时,{{report}} 将不再是一个文本标签,而是一个自定义标签。

ConfigureBuilder采用了链式调用的方式,可以一次性设置多个标签的插件:

builder.bind("report", new CustomTableRenderPolicy()).bind("name", new MyRenderPolicy());

6.3.2 将插件注册为新标签类型

当开发的插件具有一定的通用能力就可以将其注册为新的标签类型。比如增加%标识:{{%var}},对应自定义的渲染策略 HelloWorldRenderPolicy

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

此时,{{%var}} 将成为一种新的标签类型,它的执行函数是 HelloWorldRenderPolicy

6.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 { 
      // 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 { 
      // 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 { 
      // 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");

第七部分 更多插件

7.1 插件列表

除了八个通用的策略插件外,还内置了一些非常有用的插件。

ParagraphRenderPolicy渲染一个段落,可以包含不同样式文本,图片等
DocumentRenderPolicy渲染整个word文档
CommentRenderPolicy完整的批注功能示例-批注
AttachmentRenderPolicy插入附件功能示例-插入附件
LoopRowTableRenderPolicy循环表格行,下文会详细介绍示例-表格行循环
LoopColumnTableRenderPolicy循环表格列示例-表格列循环
DynamicTableRenderPolicy动态表格插件,允许直接操作表格对象示例-动态表格
BookmarkRenderPolicy书签和锚点示例-Swagger文档
AbstractChartTemplateRenderPolicy引用图表插件,允许直接操作图表对象
TOCRenderPolicyBeta实验功能:目录,打开文档时会提示更新域

同时有更多的独立插件可以使用(需要引入对应Maven依赖):

HighlightRenderPolicyWord支持代码高亮示例-代码高亮
MarkdownRenderPolicy使用Markdown来渲染word示例-Markdown
 类似资料: