poi-tl
poi-tl
java根据word模板导出word文档
是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库
NOTE: 1.12.x 要求 POI 版本在 5.2.2+.
poi-tl和poi-tl历史版本
1.12.0 Documentation,Apache POI5.2.2+,JDK1.8+
其中 1.7.x 版本下 没有 处理图片的 Pictures类
1. 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文本框 | 文本框内标签支持 |
| 图片替换 | 将原有图片替换成另一张图片 |
| 书签、锚点、超链接 | 支持设置书签,文档内锚点和超链接功能 |
| Expression Language | 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL… |
| 样式 | 模板即样式,同时代码也可以设置样式 |
| 模板嵌套 | 模板包含子模板,子模板再包含子模板 |
| 合并 | Word合并Merge,也可以在指定位置进行合并 |
| 用户自定义函数(插件) | 插件化设计,在文档任何位置执行函数 |
2. 使用
2.1.Maven添加依赖
需要poi
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.0</version>
</dependency>
2.2.入门示例
需要docx 格式的word
新建Word文档template.docx
添加文本 文档中添加 {{title}}
public static void main(String[] args) throws IOException {
XWPFTemplate.compile("D:\\template.docx").render(new HashMap<String, Object>(){{
put("title", "测试测试");
}}).writeToFile("D:\\out_template.docx");
}
输出文件
Map<String, Object> map = new HashMap<String,Object>();
map.put("score", "28");
map.put("title", "测试任务");
File file = new File("D:/文件输入.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);
// 输出文件
FileOutputStream out = new FileOutputStream(new File("D:\\文件输出.docx"));
template.write(out);
out.flush();
out.close();
template.close();
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
网络流输出
Map<String, Object> map = new HashMap<String,Object>();
map.put("score", "28");
map.put("title", "测试任务");
File file = new File("D:/文件输入.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);
// 网络流输出
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition","attachment;filename=\""+"out_template.docx"+"\"");
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
// 这步不能忘记,否则,文件流无法正常关闭,导致模板文件无法修改删除
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
针对 需要进度条的场景
try {
//存放要填充的数据
Map<String, Object> datas = Maps.newHashMap();
map.put("score", "28");
map.put("title", "测试任务");
File fileByUrl = BasUtil.getFileByUrl(filePath);
XWPFTemplate compile = XWPFTemplate.compile(fileByUrl.getAbsolutePath(), configure);
compile.render(datas);
// 为了获取文件流的大小 ,主要是设置 请求头中的 Content-Length
ByteArrayOutputStream bos = new ByteArrayOutputStream();
compile.write(bos);
byte[] fileBytes = bos.toByteArray();
//输出为文件,指定输出文件名
// compile.writeToFile(destFilePath+"out_test01.docx");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + Encodes.urlEncode(fileName));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileBytes.length)); // 这样前端可以进行进度计算
// 写入文件内容到输出流
OutputStream out = response.getOutputStream();
out.write(fileBytes);
out.flush();
PoitlIOUtils.closeQuietlyMulti(compile, bos, out);
} catch (Exception e) {
e.printStackTrace();
}
针对网络文件
File fileByUrl = BasUtil.getFileByUrl(filePath);
XWPFTemplate compile = XWPFTemplate.compile(fileByUrl.getAbsolutePath(), configure);
compile.render(datas);
通过网络url返回File对象
public static File getFileByUrl(String fileUrl){
try {
// 创建 URL 对象
URL url = new URL(fileUrl);
// 打开网络连接
InputStream inputStream = url.openStream();
// 将文件内容写入临时文件
File tempFile = File.createTempFile("tempfile", ".docx");
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
inputStream.close();
outputStream.close();
return tempFile;
} catch (IOException e) {
e.printStackTrace();
}
throw new CustomException("下载失败,请重试!");
}
3. 标签
- 在POI-TL中没有复杂的控制结构和变量赋值,只有标签;
- 标签前后由两个大括号组成
- 标签的内容为:符号+标签名称(符号代表对应的标签类型)
符号与标签类型关系如下:
| 符号 | 标签类型 |
|---|---|
| (无符号,默认) | 文本 |
| @ | 图片 |
| # | 表格 |
| * | 列表 |
3.1 文本
String:文本TextRenderData:有样式的文本HyperlinkTextRenderData:超链接和锚点文本Object:调用 toString() 方法转化为文本
文本换行使用
\n字符。
推荐使用工厂 Texts 构建文本模型。
put("name", "Sayi");
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());
TextRenderData的结构体
{
"text": "Sayi",
"style": {
"strike": false, // 删除线
"bold": true,
"italic": false, //斜体
"color": "00FF00",
"underLine": false, // 下划线
"fontFamily": "微软雅黑",
"fontSize": 12,
"highlightColor": "green", // 背景高亮色
"vertAlign": "superscript", //上标或者下标
"characterSpacing" : 20 //间距
}
3.2 图片
图片标签以@开始:
{{@var}}
数据模型:
String:图片url或者本地路径,默认使用图片自身尺寸ByteArrayPictureRenderDataFilePictureRenderDataUrlPictureRenderData
推荐使用工厂 Pictures 构建图片模型。
// 指定图片路径
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图片,我们可以利用Java生成图表插入到word文档中
put("buffered", Pictures.ofBufferedImage(bufferImage, PictureType.PNG)
.size(100, 100).create());
FilePictureRenderData的结构体
{
"pictureType" : "PNG", //图片类型
"path": "logo.png", //图片路径
"pictureStyle": { // 尺寸 ,单位是像素
"width": 100,
"height": 100
},
"altMeta": "图片不存在"
}
3.3 表格
表格标签以#开始:
{{#var}}
数据模型:
TableRenderData
推荐使用工厂 Tables 、 Rows 和 Cells 构建表格模型。
1. Example 1. 基础表格示例
// 一个2行2列的表格
put("table0", Tables.of(new String[][] {
new String[] { "00", "01" },
new String[] { "10", "11" }
}).border(BorderStyle.DEFAULT).create());
| 00 | 01 |
|---|---|
| 10 | 11 |
2. Example 2. 表格样式示例
// 第0行居中且背景为蓝色的表格
RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF")
.bgColor("4472C4").center().create();
RowRenderData row1 = Rows.create("李四", "博士");
put("table1", Tables.create(row0, row1));

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

TableRenderData表格模型在单元格内可以展示文本和图片,同时也可以指定表格样式、行样式和单元格样式,而且在N行N列渲染完成后可以应用单元格合并规则 MergeCellRule ,从而实现更复杂的表格。
{
"rows": [ //行数据
{
"cells": [ // 单元格数据
{
"paragraphs": [ // 单元格内段落
{
"contents": [
{
[TextRenderData] // 单元格内文本
},
{
[PictureRenderData] //单元格内图片
}
],
"paragraphStyle": null // 单元格内段落文本的样式:对齐
}
],
"cellStyle": { // 单元格样式:垂直对齐方式,背景色
"backgroundColor": "00000",
"vertAlign": "CENTER"
}
}
],
"rowStyle": { // 行样式:行高(单位cm)
"height": 2.0f
}
}
],
"tableStyle": { //表格样式:表格对齐、边框样式
"width": 14.63f, // 表格宽度(单位cm),表格的最大宽度 = 页面宽度 - 页边距宽度 * 2,页面宽度为A4(20.99 * 29.6,页边距为3.18 * 2.54)的文档最大表格宽度14.63cm。
"colWidths": null
},
"mergeRule": { // 单元格合并规则,比如第0行第0列至第1行第2列单元格合并
"mapping": {
"0-0": "1-2"
}
}
}
4. 案例
// 插入表格
// 用行循环插件
LoopRowTableRenderPolicy renderPolicy = new LoopRowTableRenderPolicy();
ConfigureBuilder builder = Configure.builder();
Configure configure = builder.bind("antiBurstingTitle", renderPolicy).build();
datas.put("antiBurstingTitle", BasData.getAntiBurstingList());
// 模拟数据
public static List<AntiBursting2VO> getAntiBurstingList() {
List<AntiBursting2VO> list = Lists.newArrayList();
list.add(new AntiBursting2VO(1,"合肥",46,28,116,591));
list.add(new AntiBursting2VO(2, "蚌埠", 2, 54, 79, null));
list.add(new AntiBursting2VO(3, "淮北", 47, 64, null, null));
list.add(new AntiBursting2VO(4, "宣城", 46, 42, null, null));
list.add(new AntiBursting2VO(5, "马鞍山", 8, 1, 41, 64));
list.add(new AntiBursting2VO(6, "六安", 1, null, 37, 55));
list.add(new AntiBursting2VO(7, "滁州", 5, 4, 35, 46));
list.add(new AntiBursting2VO(8, "芜湖", 1, 29, 47, null));
list.add(new AntiBursting2VO(9, "亳州", null, 36, null, null));
list.add(new AntiBursting2VO(10, "阜阳", 3, 1, 23, 109));
list.add(new AntiBursting2VO(11, "黄山", 1, 2, 19, 20));
list.add(new AntiBursting2VO(12, "安庆", 2, 17, 54, null));
list.add(new AntiBursting2VO(13, "淮南", 14, 77, null, null));
list.add(new AntiBursting2VO(14, "宿州", 2, 1, null, 63));
list.add(new AntiBursting2VO(15, "铜陵", 1, null, 22, null));
list.add(new AntiBursting2VO(16, "池州", 2, 3, 19, null));
return list;
}
5. 表格行循环
https://blog.csdn.net/ximaiyao1984/article/details/126502073
https://blog.csdn.net/ximaiyao1984/article/details/126502073
- 循环行
观测指标 {{rows}} | 日期 |
|---|---|
| [title] |
- 循环列
{{column}}观测指标 | [month] |
|---|---|
| 结论 |
3.4 图表
https://blog.csdn.net/yelangkingwuzuhu/article/details/131786614
需要先插入图表,占位,并且图表类型要对应上,比如 柱状图+折线图,不能选择柱状图
同时修改右击图表选择 --> 查看可选文字 --> 填写
{{变量字段}}

package com.yelang.test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.Charts;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.data.SeriesRenderData;
public class TestMain2 {
public static void main(String[] args) throws IOException {
Map<String, Object> map = new HashMap<String,Object>();
// 1. 普通文本
map.put("score", "28");
map.put("title", "测试任务");
map.put("type", "上半年综合测评");
map.put("status", "已完成");
map.put("time", "2023-07-18");
// 2. 图片
map.put("locpicture", new PictureRenderData(400, 300, "D:/image.jpg"));
map.put("urlImg", Pictures.ofUrl("https://p1.itc.cn/images01/20230418/5d13ab4a86c04a8dac668bf4129e1f0c.png", PictureType.PNG).size(400, 300).create());
// 3. 柱状图
ChartMultiSeriesRenderData sbqk = Charts
.ofMultiSeries("十六市区县情况", new String[] { "济南","青岛","烟台","威海"})
.addSeries("上报情况", new Double[] { 15.0,20.6,42.6,90.1})
.addSeries("查出情况", new Double[] { 12.0,15.3,28.6,80.1})
.create();
map.put("sbqk", sbqk);
// 4. 柱状图
ChartMultiSeriesRenderData sjzlpm = Charts
.ofMultiSeries("医院综合排名", new String[] { "山东大学齐鲁医院","山东省泰山医院","山东省第二人民医院","山东省第三医院"})
.addSeries("数据质量排名", new Double[] { 70.5,40.6,22.7,85.4})
.addSeries("价格质量排名", new Double[] { 80.5,75.6,72.7,85.4})
.create();
map.put("sjzlpm", sjzlpm);
// 5. 柱状图、折线图共存
List<SeriesRenderData> seriesRenderData = new ArrayList<SeriesRenderData>(3);
SeriesRenderData series1 = new SeriesRenderData("GDP", new Double[] {70.5,40.6,22.7,85.4,700.0,40.8});
series1.setComboType(SeriesRenderData.ComboType.BAR);
seriesRenderData.add(series1);
SeriesRenderData series2 = new SeriesRenderData("人口", new Double[] {80.5,50.6,62.7,45.4,200.0,140.8});
series2.setComboType(SeriesRenderData.ComboType.BAR);
seriesRenderData.add(series2);
SeriesRenderData series3 = new SeriesRenderData("指数", new Double[] {0.6,0.6,0.7,0.4,0.7,0.8});
series3.setComboType(SeriesRenderData.ComboType.LINE);
seriesRenderData.add(series3);
ChartMultiSeriesRenderData hntb = Charts
.ofMultiSeries("某省社会排名", new String[] { "城市1","城市2","城市3","城市4","城市5","城市6"})
.create();
hntb.setSeriesDatas(seriesRenderData);
map.put("hntb", hntb);
File file = new File("D:/文件输入.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);
FileOutputStream out = new FileOutputStream(new File("D:\\文件输出.docx"));
template.write(out);
out.flush();
out.close();
template.close();
System.out.println("完成");
}
}
案例
参考:https://www.jb51.net/program/299547zc7.htm

/**
* 根据网络生成word
**/
public static void generateByNetWork() {
String ip = "192.168.x.x";
// 我这里是用文件服务器返回的网络文件流,你也可以直接放置一个文件的url
String pictureUrl = "http://" + ip + ":8080/api/file/download?id=b9e0ac0336a84fc0b4ebb4cdf2c805e0.png";
HashMap<String, Object> finalMap = new HashMap<>();
finalMap.put("姓名", "张三");
finalMap.put("身份证号", "511232XXXXXXX");
finalMap.put("手机号", "1328292xxxx");
finalMap.put("员工签字", "张三");
finalMap.put("日期", "2023-05-16");
// 从网络流读取图片,置入word模板,等待编译
if (Validator.isNotEmpty(pictureUrl)) {
PictureRenderData picture = Pictures.ofUrl(pictureUrl).size(40, 30).create();
finalMap.put("员工签字图片", picture);
}
ArrayList<Object> workList = CollUtil.newArrayList();
finalMap.put("工作经历列表", workList);
for (int i = 0; i < 3; i++) {
// 模拟从mysql查询列表数据
HashMap<String, Object> workItem = new HashMap<>();
workItem.put("序号", i + 1);
workItem.put("公司名", "四川有限公司" + i);
workItem.put("工作时长", i + 10 + "小时");
workItem.put("salary", "800" + i);
workItem.put("联系", "项目经理" + i);
workItem.put("岗位", "java工程师" + i);
workList.add(workItem);
}
// 注意:此处用的是 <区块对> key是字符串,value则放置一个集合,类似于模板引擎的foreach标签
ArrayList<Object> stateList = CollUtil.newArrayList();
finalMap.put("申明项列表", stateList);
// 模拟从mysql查询数据,改造为word模板所需的数据结构
List<String> stateListFromMySQL = Arrays.asList("本人所递交的所有办理人才引进材料及填写的情况均属实;"
, "我已认真阅读以上内容并确认;"
, "若在申请期间信息变更不做变更。若违反,本人愿意承担由此产生的后果。");
for (int i = 0; i < stateListFromMySQL.size(); i++) {
HashMap<String, Object> stateItem = new HashMap<>();
// 此处在 每行文字后,都添加\n 的换行符。注意:此换行仅仅只是换行,并不等同于enter,不会触发类似于 <在word中enter换行,自动触发编排序号累加> 的动作
stateItem.put("item", stateListFromMySQL.get(i) + "\n");
stateList.add(stateItem);
}
// 从网络url 下载word模板到指定文件夹
File wordTemplate = HttpUtil.downloadFileFromUrl("http://" + ip + ":8080/file/download" + "?id=talentsTemplate.docx", "D://upload" + File.separator);
// 此处使用了poi-tl的<表格行循环插件>,此处一定要进行参数bind,方便word模板参数替换
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure build = Configure.builder().bind(policy, "工作经历列表").build();
// 进行编译
XWPFTemplate render = XWPFTemplate.compile(wordTemplate.getAbsolutePath(), build).render(finalMap);
File word = new File("D://upload" + File.separator + IdUtil.getSnowflake().nextId() + ".docx");
try {
// 关键步骤,
render.writeToFile(word.getAbsolutePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
// 下面的方法可以根据业务调整,我这里是将参数替换后的word上传到项目对应的文件服务器,拿到的url进行存入数据库
String json = HttpUtil.createPost("http://" + ip + ":8080/file/upload")
.header("Content-Type", ContentType.MULTIPART.getValue())
.form("file", word)
.execute()
.body();
// hutool工具类,删除 word。因为在这个流程中,word只是一个中间产物,因为我上边已经把该word 上传到我的文件服务器,并且文件服务器 会返回给我它对应的url
FileUtil.del(word);
List<String> list = JSONUtil.parseArray(json).toList(String.class);
}