Spring Boot 中使用 Poi-tl 渲染数据并生成 Word 文档

本文 Demo 已收录到 demo-for-all-in-java 项目中,欢迎大家 star 支持!后续将持续更新!

前言

产品经理急冲冲地走了过来。「现在需要将按这些数据生成一个 Word 报告文档,你来安排下」

项目中有这么一个需求,需要将用户填写的数据填充到一个 Word 文档中,而这个 Word 文档是人家给定了的。换句话说,让你按照这个文档的内容格式生成新的文档。

什么是 Poi-tl ?

官网:deepoove.com/poi-tl/1.9....

poi-tl(poi template language)是一种 Word 模板引擎,可以基于 Word 模板和数据生成新的文档,它的底层是通过 Apache POI 来实现的。

Apache POI 不仅封装了易用的文档 API (文本、图片、表格、页眉、页脚、图表等),也可以在底层直接操作文档XML结构。

poi-tl 拥有如下特性(了解瞄一眼就行):

内容 描述
文本 将标签渲染为文本
图片 将标签渲染为图片
表格 将标签渲染为表格
列表 将标签渲染为列表
图表 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)等图表渲染
If Condition判断 隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环 循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行 循环渲染表格的某一行
Loop表格列 循环渲染表格的某一列
Loop有序列表 支持有序列表的循环,同时支持多级列表
图片替换 将原有图片替换成另一张图片
书签、锚点、超链接 支持设置书签,文档内锚点和超链接功能
强大的表达式 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL...
标签定制 支持自定义标签前后缀
文本框 文本框内标签支持
样式 模板即样式,同时代码也可以设置样式
模板嵌套 模板包含子模板,子模板再包含子模板
合并 Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件) 在文档任何位置执行函数

我们就可以使用这个它来实现这个需求。

如何使用 Poi-tl ?

本篇文章将以 Spring Boot 项目作为演示,屏幕前的朋友们可以一起跟着我的步骤来,实践一番!

  1. 首先创建一个 Spring Boot 项目,版本目前我的 Demo 是 2.2.1,你可以更改你的 Spring Boot 版本,那现在我这里已经创建好了。

其中, pom.xml 只有两个依赖项,一个 web 和一个 test :

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  1. 接着在 pom.xml 中引入 Poi-tl 的依赖项
xml 复制代码
<!-- Poi-tl Word 模板引擎-->
<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.9.1</version>
</dependency>
  1. 准备一个 Word 模板

这一步你可以自己动手做一个 Word 模板,这里我先演示下。就先创建一个名为 Hello World.docx 的 Word 文档,模板内容如下:

找一个你喜欢的位置存放这个模板,我现在把它放到项目的 resource 目录。

{{title}} 这种由两个大括号包住的,目前可以看成占位符,这个模板中有 4 个占位符,后续的数据就渲染到这些地方上。

  1. 获取模板所在的路径,并将数据渲染到模板上

渲染只需一行代码,就是使用 XWPFTemplate 的 API 就可以了,通过 complierender 方法,就可以将数据渲染到模板中,得到渲染好的新文档。

java 复制代码
@SpringBootTest
public class PoiTlApplicationTest {

    @Test
    public void test() {
        // 获取 Word 模板所在路径
        String filepath = this.getClass().getClassLoader().getResource("hello-world.docx").getPath();
        // 通过 XWPFTemplate 编译文件并渲染数据到模板中
        XWPFTemplate template = XWPFTemplate.compile(filepath).render(
                new HashMap<String, Object>(){{
                    put("title", "Hello, poi-tl Word模板引擎");
                    put("text", "Hello World");
                    put("author", "god23bin");
                    put("description", "这还不关注 god23bin ?再不关注我可要求你关注了!");
                }});
        try {
            // 将完成数据渲染的文档写出
            template.writeAndClose(new FileOutputStream("output.docx"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
}

执行这个单元测试,就可以看到在项目所在目录下输出了新的文档 output.docx,打开这个文档,我们就可以看到如下图所示的内容:

大功告成,这就是渲染好的新文档了,是不是很简单,一行代码就完成了根据模板进行数据的渲染!

相关概念

模板

模板是 Docx 格式的 Word 文档,我们可以使用 Microsoft office、WPS Office 等软件来制作模板。

标签

上面说到 {{title}} 这种理解成占位符,实际上,官方是称之为「标签」。

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

表格布局可以设计出很多优秀专业的文档,推荐使用表格布局。

poi-tl 模板遵循 所见即所得 的设计,模板和标签的样式会被完全保留,就如我上面演示的,一级标题和字体颜色的样式就被保留下来了。

数据模型

数据模型,也就是我们需要渲染到模板中的数据,可以是哈希表,也可以是普通的 Java 对象。

  1. 哈希表(key 名是标签名):
java 复制代码
Map<String, Object> data = new HashMap<>();
data.put("title", "Hello, poi-tl Word模板引擎");
data.put("text", "Hello World");
data.put("author", "god23bin");
data.put("description", "这还不关注 god23bin ?再不关注我可要求你关注了!");
  1. Java 对象(属性名是标签名):
java 复制代码
public class DataModel {
    private String title;
    private String text;
    private String author;
    private String description;
    // 省略 getter 和 setter
}

DataModel data = new DataModel();
data.setTitle("Hello, poi-tl Word模板引擎");
data.setText("Hello World");
data.setAuthor("god23bin");
data.setDescription("这还不关注 god23bin ?再不关注我可要求你关注了!");

有了哈希表和或者 Java 对象的数据模型后,将这个数据丢给渲染的 API,就可以完成数据的渲染了。

标签的写法

poi-tl 里只有标签,那么我们需要知道标签的写法是怎样的。在 Word 文档里,可以有:文本、图片、表格、列表等元素,那么对应的,咱们的标签也有这些。

文本标签 {{var}}

简单粗暴,直接 {{标签名}} 就是文本标签了。

图片标签 {{@var}}

{{@标签名}} 就是图片标签,@ 标识了这个标签的类型是图片,其他的也是同理,不同的符号标识不同类型的标签。

表格标签 {{#var}}

使用 # 标识这是一个表格标签。

列表标签 {{*var}}

使用 * 标识这是一个列表标签。

其余的标签

剩下的标签还有很多,详细的内容你可以阅读官方文档,这里就不一一介绍了。

下面我将写下我用过的内容。

插件

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

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

默认插件

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

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

由于这 8 个插件是经常用到的,所以这些插件被注册为不同的标签类型,也就是我们看到过的 {{var}}、{{@var}}、{{#var}} 等不同类型的标签,从而搭建了 poi-tl 的标签体系。

除了这 8 个通用的策略插件外,还内置了一些额外用途的插件:

DynamicTableRenderPolicy 动态表格插件,允许直接操作表格对象 示例-动态表格
HackLoopTableRenderPolicy 循环表格行,下文会详细介绍 示例-表格行循环
LoopColumnTableRenderPolicy 循环表格列 示例-表格列循环
BookmarkRenderPolicy 书签和锚点 示例-Swagger文档
JSONRenderPolicy 高亮显示JSON代码块 示例-Swagger文档
AbstractChartTemplateRenderPolicy 引用图表插件,允许直接操作图表对象
ParagraphRenderPolicy 渲染一个段落,可以包含不同样式文本,图片等
DocumentRenderPolicy 渲染多个段落和表格
TOCRenderPolicy Beta实验功能:目录,打开文档时需要更新域

使用插件

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

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

java 复制代码
ConfigureBuilder builder = Configure.builder();
builder.bind("description", new HtmlRenderPolicy());

此时,{{description}} 将不再是一个文本标签,而是一个自定义的支持 HTML 渲染的标签。

当然,这里的 HTML 渲染的插件,默认是没有提供的,需要引入以下的依赖项,才能支持 HTML 的渲染。

xml 复制代码
<!-- 支持渲染 HTML 的插件 -->
<dependency>
    <groupId>io.github.draco1023</groupId>
    <artifactId>poi-tl-ext</artifactId>
    <version>0.3.3</version>
</dependency>

示例:我们对 {{author}} 这个标签绑定上支持 HTML 渲染的插件,这样就能渲染 HTML 的文本了。

java 复制代码
@SpringBootTest
public class PoiTlApplicationTest {

    @Test
    public void test() {
        // 获取 Word 模板所在路径
        String filepath = this.getClass().getClassLoader().getResource("hello-world.docx").getPath();
        // 给标签绑定插件
        Configure configure = Configure.builder().bind("author", new HtmlRenderPolicy()).build();
        // 通过 XWPFTemplate 编译文件并渲染数据到模板中
        XWPFTemplate template = XWPFTemplate.compile(filepath, configure).render(
                new HashMap<String, Object>(){{
                    put("title", "Hello, poi-tl Word模板引擎");
                    put("text", "Hello World");
                    put("author", "<h2>god23bin</h2>");
                    put("description", "这还不关注 god23bin ?再不关注我可要求你关注了!");
                }});
        try {
            // 将完成数据渲染的文档写出
            template.writeAndClose(new FileOutputStream("output.docx"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
}

生成的 Word 文档如下图所示,可以看到 {{author}} 这个标签的 HTML 文本已经渲染成功了!(蓝框框)

表格行循环

当有类似如下需求的时候,在表格里展示多行同类型的数据,那么就需要用到表格的行循环了。

这里也是涉及到插件的,具体就是使用 HackLoopTableRenderPolicy 这个插件策略,这个策略能够根据集合数据进行循环渲染,这样就渲染数据到表格的行上了,集合有多少个元素,那么就有多少行。

我们来看下这个表格行循环的模板是怎样写的,是这样的:

可以看到 {{articles}}{{columns}} 是标准的文本标签,我们这里将这两个标签置于循环行的上一行 ,循环行里设置要循环的标签和内容,注意这里的标签是使用 [] 的,以此来区分标准的标签语法。

同时 {{articles}}{{columns}} 标签对应的数据就是文章和专栏的集合。

我们写一个该模板的数据模型,以 Java 对象来写,同时模拟数据从数据库中读取。

数据模型:AcWordModel

java 复制代码
public class AcWordModel {
    /**
     * 文章明细数据模型-表格行循环
     */
    private List<Article> articles;
    /**
     * 专栏明细数据模型
     */
    private List<SpecialColumn> columns;
    // 省略 getter 和 setter
}

其中的 Article 和 SpecialColumn 模型如下:

java 复制代码
public class Article {
    private String title;
    private String tags;
    private Integer reading;
    private Integer likes;
    // 省略 getter 和 setter
}
java 复制代码
public class SpecialColumn {
    private String name;
    private Integer subscription;
    private Integer nums;
    // 省略 getter 和 setter
}

进行测试,获取数据和模板,让标签和表格行循环的插件进行绑定

java 复制代码
    @Test
    public void rowLoopTest() {
        // 获取数据,这里假装是从数据库中查询得到的
        AcWordModel data = getFromDB();
        // 获取 Word 模板所在路径
        String filepath = this.getClass().getClassLoader().getResource("table-row-loop.docx").getPath();
        // 给标签绑定插件,这里就绑定表格行循环的插件
        Configure configure = Configure.builder()
                .bind("articles", new HackLoopTableRenderPolicy())
                .bind("columns", new HackLoopTableRenderPolicy())
                .build();
        // 通过 XWPFTemplate 编译文件并渲染数据到模板中
        XWPFTemplate template = XWPFTemplate.compile(filepath, configure).render(data);
        try {
            // 将完成数据渲染的文档写出
            template.writeAndClose(new FileOutputStream("ac-word.docx"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

这样,就能实现表格的行循环了!

封装 Word 渲染生成新文档的工具

我们可以再封装下这个 API,写一个工具类,如下:

java 复制代码
package cn.god23bin.demo.util;

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;

public class WordUtil {

    /**
     * 生成 word 文档
     * @param wordTemplatePath    word 模板路径
     * @param targetWordFilePath  生成目标文档路径
     * @param data                待渲染的数据模型-哈希表形式
     */
    public static void generateWordFile(String wordTemplatePath, String targetWordFilePath, Map<String, Object> data) {
        XWPFTemplate template = XWPFTemplate.compile(wordTemplatePath).render(data);
        try {
            template.writeAndClose(new FileOutputStream(targetWordFilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成 word 文档
     * @param wordTemplatePath    word 模板路径
     * @param targetWordFilePath  生成目标文档路径
     * @param data                待渲染的数据模型-Java对象形式
     */
    public static void generateWordFile(String wordTemplatePath, String targetWordFilePath, Object data) {
        XWPFTemplate template = XWPFTemplate.compile(wordTemplatePath).render(data);
        try {
            template.writeAndClose(new FileOutputStream(targetWordFilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成 word 文档
     * @param wordTemplatePath    word 模板路径
     * @param targetWordFilePath  生成目标文档路径
     * @param data                待渲染的数据模型-哈希表形式
     * @param configure           渲染配置
     */
    public static void generateWordFile(String wordTemplatePath, String targetWordFilePath, Map<String, Object> data, Configure configure) {
        XWPFTemplate template = XWPFTemplate.compile(wordTemplatePath, configure).render(data);
        try {
            template.writeAndClose(new FileOutputStream(targetWordFilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成 word 文档
     * @param wordTemplatePath    word 模板路径
     * @param targetWordFilePath  生成目标文档路径
     * @param data                待渲染的数据模型-Java对象形式
     * @param configure           渲染配置
     */
    public static void generateWordFile(String wordTemplatePath, String targetWordFilePath, Object data, Configure configure) {
        XWPFTemplate template = XWPFTemplate.compile(wordTemplatePath, configure).render(data);
        try {
            template.writeAndClose(new FileOutputStream(targetWordFilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

最后的最后

希望各位屏幕前的 靓仔靓女们给个三连!你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!

咱们下期再见!

相关推荐
懒羊羊不懒@1 天前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss2731 天前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父1 天前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛1 天前
Spring面试
java·spring·面试
舒一笑1 天前
🚀 PandaCoder 2.0.0 - ES DSL Monitor & SQL Monitor 震撼发布!
后端·ai编程·intellij idea
Java中文社群1 天前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme1 天前
java.nio 包详解
java·python·nio
零千叶1 天前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝1 天前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表
li3714908901 天前
nginx报400bad request 请求头过大异常处理
java·运维·nginx