使用 poi-tl 模板生成 MS Word 文档

1、概览

poi-tl 是一个基于 Apache POI 的开源 Java 库,用于简化使用模板生成 Word 文档的过程。它是一个 Word 模板引擎,可根据 Word 模板和数据创建新文档。

我们可以在模板中指定样式。从模板生成的文档将保留指定的样式。模板是声明式的,纯粹基于标签,图片、文本、表格等都有不同的标签模式。poi-tl 库还支持自定义插件,以根据需要构建文档。

本文将带你了解模板中可以使用的不同标签,以及模板中自定义插件的用法。

2、依赖

要使用 poi-tl 库,我们需要在项目中添加其 maven 依赖:

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

你在 Maven Central 中找到最新版本的 poi-tl 库。

3、配置

使用 ConfigureBuilder 类来构建配置:

ConfigureBuilder builder = Configure.builder();
...
XWPFTemplate template = XWPFTemplate.compile(...).render(templateData);
template.writeAndClose(...);

模板文件是以 Word .docx 文件格式保存的。首先,我们使用 XWPFTemplate 类的 compile 方法对模板进行编译。模板引擎编译模板,并根据 templateData 在新的 docx 文件中进行渲染。在这里,writeAndClose 创建一个新文件,并按照模板中指定的格式写入指定的数据。templateData 是一个 HashMap 的实例,其中 Key 为 String,值为 Object

我们可以根据自己的需要配置模板引擎。

3.1、标签前缀和后缀

模板引擎使用大括号 {{}} 表示标签。我们可以将其设置为 ${} 或其他任何形式:

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

3.2、标签类型

默认情况下,模板引擎为模板标签定义了标识符,如 @ 表示图像标签,# 表示表格标签等。标签的标识符可以配置:

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

3.3、标签名格式

标签名称默认支持不同字符、字母、数字和下划线的组合。标签名称的规则可以用正则表达式来配置:

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

后续章节会介绍插件和错误处理等配置,其余部分的配置假设都使用默认配置。

4、模板标签

poi-tl 库模板没有任何变量赋值或循环;它们完全基于标签。MapDataModel 会将要渲染的数据与 poi-tl 库中的模板标签关联起来。

标签由两个大括号括起来的标签名称组成:

{{tagName}}

让我们来了解一下基本标签。

4.1、文本

文本标签代表文档中的普通文本。标签名称用两个大括号括起来:

{{authorname}}

如上,我们声明一个名为 authorname 的文本标签,将其添加到 template.docx 文件中。然后,将数据值添加到 Map 中:

this.templateData.put("authorname", Texts.of("John")
  .color("000000")
  .bold()
  .create());

在生成的文档中,模板数据渲染器将文本标签 authorname 替换为指定值 John。此外,指定的样式(如颜色和粗体)也会应用到生成的文档中。

4.2、图片

对于图像标签,标签名称前要加上 @

{{@companylogo}}

如上,我们定义了图片类型的 companylogo 标签。要显示图片,需要将 companylogo(显示的图片路径)添加到数据 Map

templateData.put("companylogo", "logo.png");

4.3、有序列表

对于有序列表,标签名称前要加上 *

{{*bulletlist}}

在该模板中,我们声明了一个名为 bulletlist 的有序列表,并添加了列数据:

List<String> list = new ArrayList<String>();
list.add("Plug-in grammar");
// ...

NumberingBuilder builder = Numberings.of(NumberingFormat.DECIMAL);
for(String s:list) {
    builder.addItem(s);
}
NumberingRenderData renderData = builder.create();
this.templateData.put("list", renderData);

不同的序号格式,如 NumberingFormat.DECIMALNumberingFormat.LOWER_LETTERNumberingFormat.LOWER_ROMAN 等,可通过配置列表编号样式来修改。

4.4、章节

使用开始标签和结束标签来标记章节。在开始标签中,名称前加 ?,在结束标签中,名称前加 /

{{?students}} {{name}} {{/students}}

我们创建了一个名为 students 的章节,并在该部分中添加一个标签 name。在 map 中添加章节数据:

Map<String, Object> students = new LinkedHashMap<String, Object>();
students.put("name", "John");
students.put("name", "Mary");
students.put("name", "Disarray");
this.templateData.put("students", students);

4.5、表格

使用模板在 Word 文档中生成一个表格结构。对于表格结构,标签名称前面有一个 #

{{#table0}}

添加一个名为 table0 的表格标签,并使用 Tables 类方法添加表格数据:

templateData.put("table0", Tables.of(new String[][] { new String[] { "00", "01" }, new String[] { "10", "11" } })
  .border(BorderStyle.DEFAULT)
  .create());

方法 Rows.of() 可以单独定义行,并为表格行添加边框等样式:

RowRenderData row01 = Rows.of("Col0", "col1", "col2")
  .center()
  .bgColor("4472C4")
  .create();
RowRenderData row01 = Rows.of("Col10", "col11", "col12")
  .center()
  .bgColor("4472C4")
  .create();
templateData.put("table3", Tables.of(row01, row11)
  .create());

这里,table3 有两行,行边框设置为 4472C4

使用 MergeCellRule 创建单元格合并规则:

MergeCellRule rule = MergeCellRule.builder()
  .map(Grid.of(1, 0), Grid.of(1, 2))
  .build();
templateData.put("table3", Tables.of(row01, row11)
  .mergeRule(rule)
  .create());

如上,单元格合并规则合并了表格第二行的单元格 1 和单元格 2。同样,还可以对 table 标签进行其他定制。

4.5、嵌套

我们可以在另一个模板内添加一个模板,即模板嵌套。对于嵌套标签,标签名称前会加上一个 +

{{+nested}}

这就在模板中声明了一个嵌套标签,名称为 nested。然后,为 nested 文档设置要渲染的数据:

List<Address> subData = new ArrayList<>();
subData.add(new Address("Florida,USA"));
subData.add(new Address("Texas,USA"));
templateData.put("nested", Includes.ofStream(WordDocumentEditor.class.getClassLoader().getResourceAsStream("nested.docx")).setRenderModel(subData).create());

如上,从 nested.docx 中加载模板,并为嵌套模板设置渲染数据。poi-tl 模板引擎会将嵌套模板渲染为值或数据,以显示 nested 标签。

4.6、使用数据模型渲染模板

还可以使用数据模型类来渲染模板。

创建一个 Person 类:

public class Person {
    private String name;
    private int age;
    // ...
}

我们可以使用数据模型(data model)设置模板标签值:

templateData.put("person", new Person("Jimmy", 35));

如上,我们设置了一个名为 person 的数据模型,其属性为 nameage。模板使用 . 操作符访问属性值:

{{person.name}}
{{person.age}}

同样,我们也可以在数据模型中使用不同类型的数据。

5、插件

插件允许我们在模板标签位置执行预定义函数。使用插件,我们几乎可以在模板中需要的位置执行任何操作。poi-tl 库有默认插件,无需明确配置。默认插件处理文本、图像、表格等的渲染。

此外,有一些内置插件需要配置后才能使用,我们也可以开发自己的插件,称为自定义插件。接下来,让我们了解一下内置插件和自定义插件。

5.1、使用内置插件

对于注释,poi-tl 为 Word 文档中的注释提供了一个内置插件。

使用 CommentRenderPolicy 配置注释标签:

builder.bind("comment", new CommentRenderPolicy());

这会将 comment 标签注册为模板引擎中的注释渲染器(comment renderer)。

注释插件 CommentRenderPolicy 的使用示例如下:

CommentRenderData comment = Comments.of("SampleExample")
  .signature("John", "S", LocaleUtil.getLocaleCalendar())
  .comment("Authored by John")
  .create();
templateData.put("comment", comment);

模板引擎会识别 comment 标签,并将指定的注释放入生成的文档中。

其他的 可用插件 你可以查看相关文档。

5.2、自定义插件

我们可以为模板引擎创建自定义插件。数据可根据自定义要求在文档中渲染。

要定义自定义插件,需要实现 RenderPolicy 接口或继承 AbstractRenderPolicy 抽象类 :

public class SampleRenderPolicy implements RenderPolicy {
    @Override
    public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
        XWPFRun run = ((RunTemplate) eleTemplate).getRun(); 
        String text = "Sample plugin " + String.valueOf(data);
        run.setText(textVal, 0);
    }
}

如上,我们使用 SampleRenderPolicy 类创建了一个自定义插件示例。然后需要配置模板引擎以识别自定义插件:

ConfigureBuilder builder = Configure.builder();
builder.bind("sample", new SampleRenderPolicy());
templateData.put("sample", "custom-plugin");

该配置用名为 sample 的标签注册了我们的自定义插件。模板引擎会将模板中的 sample 标签替换为文本 Sample plugin custom-plugin

我们可以通过继承 AbstractRenderPolicy 开发更多自定义插件。

6、日志

要启用 poi-tl 库的日志记录,我们可以使用 Logback 库。

logback.xml 中为 poi-tl 添加一个 Logger:

<logger name="com.deepoove.poi" level="debug" additivity="false">
    <appender-ref ref="STDOUT" />
</logger>

此配置可启用 poi-tlcom.deepoove.poi 包的日志记录:

18:01:15.488 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document start...
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - {{title}}
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - [Start]:The run position of {{title}} is 0, Offset in run is 0
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - [End]:The run position of {{title}} is 0, Offset in run is 8
...

18:01:19.661 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document start...
18:01:19.685 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document end, resolve and create 0 MetaTemplates.
18:01:19.685 [main] INFO  c.deepoove.poi.render.DefaultRender - Successfully Render template in 4126 millis

我们可以通过日志来观察模板标签是如何被解析以及数据是如何被渲染的。

7、错误处理

poi-tl 支持自定义引擎出现异常时的行为。

在一些无法计算标签的情况下,例如模板中引用了不存在的变量,如渲染 {{student.name}}student 的值为 null,则无法计算 name 的值。

出现这种错误时,poi-tl 库可以配置计算结果。

默认情况下,标签值被视为 null 值。当我们需要严格检查模板是否存在人为错误时,我们可以抛出异常

builder.useDefaultEL(true);

poi-tl 的默认行为是清除标签。如果我们不想对标签做任何操作:

builder.setValidErrorHandler(new DiscardHandler());

要执行严格验证,可直接抛出异常:

builder.setValidErrorHandler(new AbortHandler());

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

如上,我们为 DocumentRenderPolicy 配置了一个名为 title 的标签,并创建了一个 DocumentRenderData 对象,从而形成了模板结构。

模板引擎会识别 title 标签,并生成一个 Word 文档,其中包含 document 结构文档的数据。

9、总结

本文介绍了如何使用 poi-tl 模板库创建 Word 文档,详细介绍了不同类型的标签、日志记录、插件以及 poi-tl 库的异常处理。


Ref:https://www.baeldung.com/poi-tl-ms-word