poi-tl

integrated-development

# 一、处理Word工具

下表对一些处理Word的解决方案作了一些比较:

方案 跨平台 样式处理 易用性
Poi-tl 纯Java组件,跨平台 不需要编码,模板即样式 简单:模板引擎,对POI进行封装,支持Word文档合并、表格处理等
Apache POI 纯Java组件,跨平台 编码 简单,没有模板引擎功能
Freemarker XML操作,跨平台 复杂,需要理解XML结构,基于XML构造模板
OpenOffice 需要安装OpenOffice软件 编码 复杂,需要了解OpenOffice的API
Jacob、winlib Windows平台 编码 复杂,不推荐使用

参考官方比较说明 (opens new window)

# 二、poi-tl模板引擎

poi-tl官方使用文档API地址:http://deepoove.com/poi-tl (opens new window)

poi-tl中文文档 (opens new window) or English-tutorial Wiki (opens new window)

Java Word的模板引擎,对docx格式的文档增加模板语法,支持对段落、页眉、页脚、表格等模板替换,并且提供了插件机制,在文档的任何地方做任何事情。

poi-tl是基于Apache POI的一套拥有简洁API的跨平台的模板引擎,纯Java组件,是一个免费开源的Java类库。

根据poi-tl 可以操作含有多种类型的复杂 Word 文档,包括:

  • 文本
  • 表格
  • 图片
  • 附件
  • markdown

并且支持表格行循环表格列循环动态表格批注附件高亮等等。

# poi-tl功能点

Word模板引擎功能 描述
文本 将标签渲染为文本
图片 将标签渲染为图片
表格 将标签渲染为表格
列表 将标签渲染为列表
图表 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染
If Condition判断 根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环 根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行 循环复制渲染表格的某一行
Loop表格列 循环复制渲染表格的某一列
Loop有序列表 支持有序列表的循环,同时支持多级列表
Highlight代码高亮 word中代码块高亮展示,支持26种语言和上百种着色样式
Markdown 将Markdown渲染为word文档
Word批注 完整的批注功能,创建批注、修改批注等
Word附件 Word中插入附件
SDT内容控件 内容控件内标签支持
Textbox文本框 文本框内标签支持
图片替换 将原有图片替换成另一张图片
书签、锚点、超链接 支持设置书签,文档内锚点和超链接功能
样式 模板即样式,同时代码也可以设置样式
模板嵌套 模板包含子模板,子模板再包含子模板
合并 Word合并Merge,也可以在指定位置进行合并
Expression Language 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
用户自定义函数(插件) 插件化设计,在文档任何位置执行函数

# 架构设计

模板和插件构建了整个Poi-tl的核心。 Poi-tl通过极简的架构实现了模板功能并且支持最大的扩展性,JAR包体积仅有几十KB。

整体设计采用了Template + data-model = output模式。

Configure提供了模板配置功能,比如语法配置和插件配置;

Visitor提供了模板解析功能;

RenderPolicy是渲染策略扩展点;

Render模块提供了RenderDataCompute表达式计算扩展点,通过RenderPolicy对每个标签进行渲染。

# Template模板

模板是Docx格式的Word文档,你可以使用Microsoft office、WPS Office、Pages等任何你喜欢的软件制作模板,也可以使用Apache POI代码来生成模板。

所有的标签都是以{{开头,以}}结尾,标签可以出现在任何位置,包括页眉,页脚,表格内部,文本框等,表格布局可以设计出很多优秀专业的文档,推荐使用表格布局。

poi-tl模板遵循**“所见即所得”**的设计,模板和标签的样式会被完全保留。

# data-model数据

数据类似于哈希或者字典,可以是Map结构(key是标签名称):

Map<String, Object> data = new HashMap<>();
data.put("name", "Sayi");
data.put("start_time", "2019-08-04");
1
2
3

可以是对象(属性名是标签名称)

public class Data {
  private String name;
  private String startTime;
  private Author author;
}
1
2
3
4
5

数据也可以是树结构,每级之间用点来分隔开。比如

{{author.name}} 标签对应的数据是author对象的name属性值。
1

Word模板不是由简单的文本表示,所以在渲染图片、表格等元素时提供了数据模型,它们都实现了接口RenderData,比如图片数据模型PictureRenderData包含图片路径、宽、高三个属性。

# output输出

以流的方式进行输出:

template.write(OutputStream stream);
1

可以写到任意输出流中,比如文件流:

template.write(new FileOutputStream("output.docx"));
1

比如网络流:

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);
1
2
3
4
5
6
7
8
9
10

最后不要忘记关闭这些流。

# 语法结构

所有的语法结构都是以{{开始,以}}结束。

{{ 标记类型 }}
1
标记类型 描述
template 普通文本
@template 图片
#template 表格
*template 列表
+template Word文档合并

# 文本

{{template}}
1

数据模型:

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

代码示例:

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

除了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());
1
2
3

# 名词解释

Word模板引擎功能 描述
文本 将标签渲染为文本
图片 将标签渲染为图片
表格 将标签渲染为表格
列表 将标签渲染为列表
图表 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染
If Condition判断 根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环 根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行 循环复制渲染表格的某一行
Loop表格列 循环复制渲染表格的某一列
Loop有序列表 支持有序列表的循环,同时支持多级列表
Highlight代码高亮 word中代码块高亮展示,支持26种语言和上百种着色样式
Markdown 将Markdown渲染为word文档
Word批注 完整的批注功能,创建批注、修改批注等
Word附件 Word中插入附件
SDT内容控件 内容控件内标签支持
Textbox文本框 文本框内标签支持
图片替换 将原有图片替换成另一张图片
书签、锚点、超链接 支持设置书签,文档内锚点和超链接功能
Expression Language 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
样式 模板即样式,同时代码也可以设置样式
模板嵌套 模板包含子模板,子模板再包含子模板
合并 Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件) 插件化设计,在文档任何位置执行函数

注意:只能操作.docx格式的word,不能操作.doc格式的word. 只能操作word中的表格, 不能操作Excel中的表格。

# 核心API

XWPFTemplate,核心API只需要一行代码。

XWPFTemplate template = XWPFTemplate.compile("~/file.docx").render(datas);
1

# 版本说明

在使用poi-tl时, 需要注意版本之间的冲突问题。

参考官方版本说明 (opens new window)

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

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

# 三、poi-tl开发

# Maven依赖

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

NOTE: poi-tl 1.12.x requires POI version 5.2.2+.

# 快速开始

从一个超级简单的例子开始:
把`{{title}}`替换成"Poi-tl 模板引擎"。
1. 新建文档template.docx,包含文本`{{title}}`
2. TDO模式:Template + data-model = output
1
2
3
4

代码示例:

//核心API采用了极简设计,只需要一行代码
XWPFTemplate.compile("template.docx").render(new HashMap<String, Object>(){{
        put("title", "Poi-tl 模板引擎");
}}).writeToFile("out_template.docx");
1
2
3
4

# 对象属性赋值

map方式(最简单实用),数据是Map结构,KEY是标签名称,VALUE是对应的值。

单值

Map<String, Object> content = new HashMap<>();
content.put("name", "Sapi");
1
2

也可以是对象,属性名是标签名称。

public class User {
	private Long id;
	private String name;
}
1
2
3
4
Map<String, Object> content = new HashMap<>();
content.put("user", new User(1, "法外狂徒张三"));
1
2

doc文档中获取数据语法结构

{{user.name}}
1

# 四、poi-tl扩展

poi-tl 更多插件 (opens new window)

# 表格行循环

需要在Configure对象中绑定需要循环的list对象。

public class TestTableServiceImpl {

    public static final String ASSET_CHANGE_FILE_PATH = "/doc/word_table.docx";

    public XWPFTemplate exportDoc() throws IOException {
        //1 获取模板文件流
        File resourceFile = new ClassPathResource(ASSET_CHANGE_FILE_PATH).getFile();

        //2 构建word数据
        Map<String, Object> content = new HashMap<>();
//        content.put("assets", fillAssets());
        content.put("assets", getTableDatas());

        //3 创建行循环策略
        LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(true);
        // 告诉模板引擎,要在tagName做行循环,绑定行循环策略
        Configure config = Configure.builder().bind("assets", policy).build();

        // 编译渲染
        return XWPFTemplate.compile(resourceFile, config).render(content);
    }
    
    private List<TableData> getTableDatas() {
        List<TableData> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            TableData table = new TableData();
            table.setIndex(i + 1);
            table.setCode("TMP" + i);
            table.setName("测试数据" + i);
            table.setRemark("备注" + i);
            list.add(table);
        }
        return list;
    }
}
    
    
@Data
public class TableData {
    private Integer index;
    private String code;
    private String name;
    private String remark;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

创建表格模板

导出填充后的效果

# SpringEL表达式 (opens new window)

Spring Expression Language 是一个强大的表达式语言,支持在运行时查询和操作对象图,可作为独立组件使用,也可作为poi-tl模板上, 用于模板填充时参数的引用。

单独使用时需要引入相应的依赖:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-expression</artifactId>
  <version>5.3.18</version>
</dependency>
1
2
3
4
5

关于SpringEL的写法可以参见Spring官方文档 (opens new window),下面给出一些典型的示例

{{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日'))}}  使用静态类方法
1
2
3
4
5
6
7
8
9

# 插件poi-tl-ext

插件描述:在poi-tl的基础上扩展渲染HTML,目前实现了富文本编辑器可实现的大部分效果

插件源码地址:https://github.com/draco1023/poi-tl-ext

# 五、poi-tl示例

Spring Boot项目集成poi-tl示例。

# 1、普通文本示例

# Maven依赖

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

NOTE: poi-tl 1.12.x requires POI version 5.2.2+.

# DOC模板绘制

  1. 绘制将要导出的DOC文档,数据值用SpringEL表达式替换。

  1. 存放在项目的resources/doc目录,文件名为myTextDoc.docx

# Controller

@GetMapping("/exportDoc/{id}")
public void exportDoc(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        String fileName = "申请表_" + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + ".docx";
        XWPFTemplate document = myService.exportDoc(id);
        response.reset();
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-disposition",
                "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        OutputStream os = response.getOutputStream();
        document.write(os);
        os.close();
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Service

public static final String ALLOCATE_FILE_PATH = "/doc/myTextDoc.docx";

public XWPFTemplate generateWordXWPFTemplate(ProcessAllocate data, ProcessAllocateOpinionVO opinion) throws IOException {
        Map<String, Object> content = new HashMap<>();
        content.put("data", data);
        content.put("opinion", opinion);

        // 日期处理
        Date requestDate = data.getRequestDate();
        content.put("year", DateUtil.year(requestDate));
        content.put("month", DateUtil.month(requestDate) + 1);
        content.put("day", DateUtil.dayOfMonth(requestDate));
        content.put("requestDate", DateUtil.format(data.getRequestDate(), DatePattern.NORM_DATE_PATTERN));

        return XWPFTemplate.compile(new ClassPathResource(ALLOCATE_FILE_PATH).getFile()).render(content);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2、表格示例

# Maven依赖

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

NOTE: poi-tl 1.12.x requires POI version 5.2.2+.

# DOC模板绘制

  1. 绘制将要导出的DOC文档,数据值用SpringEL表达式替换。

  1. 存放在项目的resources/doc目录,文件名为myTableDoc.docx

# Controller

@GetMapping("/exportDoc/{id}")
public void exportDoc(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        String fileName = "变更表_" + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + ".docx";
        XWPFTemplate document = myService.exportDoc(id);
        response.reset();
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-disposition",
                "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        OutputStream os = response.getOutputStream();
        document.write(os);
        os.close();
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Service

public static final String ALLOCATE_FILE_PATH = "/doc/myTableDoc.docx";

public XWPFTemplate exportDoc(Long id) throws IOException {
    //1 获取模板文件流
    File resourceFile = new ClassPathResource(ASSET_CHANGE_FILE_PATH).getFile();

    //2 word数据
    ProcessChangeData data = getInfo(id);
    Map<String, Object> content = new HashMap<>();
    content.put("change", data.getChange());
    // 填充表格数据
    content.put("assets", fillAssets(data.getAssets()));
    Date requestTime = data.getChange().getRequestTime();
    content.put("year", DateUtil.year(requestTime));
    content.put("month", DateUtil.month(requestTime) + 1);
    content.put("day", DateUtil.dayOfMonth(requestTime));

    //3 表格指定插件
    LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(true);
    Configure config = Configure.builder().bind("assets", policy).build();

    return XWPFTemplate.compile(resourceFile, config).render(content);
}

/**
* 转换填充表格数据
*/
private List<Map<String, Object>> fillAssets(List<ProcessChangeAnnex> assets) {
    List<Map<String, Object>> list = new ArrayList<>();
    for (int i = 0; i < assets.size(); i++) {
        ProcessChangeAnnex annex = assets.get(i);
        Map<String, Object> table = new HashMap<>();
        table.put("index", i + 1);
        table.put("assetCode", annex.getAssetCode());
        table.put("assetName", annex.getAssetName());
        table.put("categoryName", annex.getCategoryName());
        // ...
        list.add(table);
    }
    return list;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 3、图片示例

# DOC模板绘制

  1. 绘制将要导出的DOC文档,数据值用SpringEL表达式替换。

  1. 存放在项目的resources/doc目录,文件名为myImgDoc.docx

# Service**

public static final String ALLOCATE_FILE_PATH = "/doc/myImgDoc.docx";

public XWPFTemplate exportDoc(Long id) throws IOException {
    //1 获取模板文件流
    File resourceFile = new ClassPathResource(ASSET_CHANGE_FILE_PATH).getFile();

    //2 word数据
    ProcessChangeData data = getInfo(id);
    Map<String, Object> content = new HashMap<>();
    content.put("change", data.getChange());
    
    // 二维码图片
    try {
        BufferedImage bufferImage = QrCodeUtils.defaultBufferedImage(data.getAid().toString());
        PictureRenderData pictureRenderData = Pictures.ofBufferedImage(bufferImage, PictureType.PNG).size(100, 100).create();
        content.put("qrImg", pictureRenderData);
    } catch (Exception e) {
        log.error("生成二维码发生异常", e);
    }
    return XWPFTemplate.compile(resourceFile, config).render(content);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21