Java操作Word文档
poi-tl介绍
官方文档:https://deepoove.com/poi-tl/
poi-tl(poi template language)是Word模板引擎,使用模板和数据创建很棒的Word文档。
在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。
方案 | 移植性 | 功能性 | 易用性 |
---|---|---|---|
Poi-tl | Java跨平台 | Word模板引擎,基于Apache POI,提供更友好的API | 低代码,准备文档模板和数据即可 |
Apache POI | Java跨平台 | Apache项目,封装了常见的文档操作,也可以操作底层XML结构 | 文档不全,这里有一个教程:Apache POI Word快速入门 |
Freemarker | XML跨平台 | 仅支持文本,很大的局限性 | 不推荐,XML结构的代码几乎无法维护 |
OpenOffice | 部署OpenOffice,移植性较差 | - | 需要了解OpenOffice的API |
HTML浏览器导出 | 依赖浏览器的实现,移植性较差 | HTML不能很好的兼容Word的格式,样式糟糕 | - |
Jacob、winlib | Windows平台 | - | 复杂,完全不推荐使用 |
poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中,并且拥有着让人喜悦的特性。
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,也可以在指定位置进行合并 |
用户自定义函数(插件) | 插件化设计,在文档任何位置执行函数 |
快速上手
Maven
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
准备一个模板文件,占位符使用双大括号占位
你好,我是{{name}},今年{{age}}岁
然后将模板放在 resources 目录下,编写代码
@Test
void test1() {
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 18);
// 加载本地模板文件
InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);
try {
// 写出到文件
template.writeAndClose(new FileOutputStream("output.docx"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
效果展示
加载远程模板文件
在实际业务场景中,模板可能会有很多,并且不会保存在本地,这时就需要加载远程模板来进行处理
下面是示例代码
@Test
void test2() {
try {
// 加载远程模板
String templateUrl =
"https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/%E6%BC%94%E7%A4%BA%E6%A8%A1%E6%9D%BF1.docx";
URL url = new URL(templateUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream inputStream = conn.getInputStream();
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 18);
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);
// 写出到文件
template.writeAndClose(new FileOutputStream("output2.docx"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
编写接口返回处理后的文件
下面我们来实现编写一个接口,前端访问时携带参数,后端完成编译后返回文件给前端下载
@Api(tags = "模板管理")
@RestController
@RequestMapping("/word")
public class WordController {
@GetMapping("getWord")
public void getWord(String name, Integer age, HttpServletResponse response) {
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", name);
data.put("age", age);
// 加载本地模板文件
InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);
// 设置响应头,指定文件类型和内容长度
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=output.docx");
try {
// 返回网络流
ServletOutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
// 关闭流
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
前端代码编写
定义接口地址,并且请求中声明 responseType
import { request } from '@umijs/max';
// 下载报告
export async function getWordFun(age, name) {
return request(`/word/getWord?age=${age}&name=${name}`, {
method: 'get',
responseType: 'blob', // 使用blob下载
});
}
然后响应拦截器中判断 responseType
requestErrorConfig.ts
/**
* @name 错误处理
* pro 自带的错误处理, 可以在这里做自己的改动
* @doc https://umijs.org/docs/max/request#配置
*/
export const errorConfig: RequestConfig = {
// 响应拦截器
responseInterceptors: [
(response) => {
// 拦截响应数据,进行个性化处理
const res = response as unknown as ResponseStructure;
// 判断流数据
if (res.request.responseType === 'blob') {
return response;
}
// 判断状态码
if (!sucCodes.includes(res.data?.code)) {
return Promise.reject(res.data);
}
return response;
},
],
};
编写页面代码
import React from 'react';
import { ProForm, ProFormDigit, ProFormText } from '@ant-design/pro-components';
import { getWordFun } from '@/services/ant-design-pro/reportApi';
const Report = () => {
const onFinish = async (values) => {
let res = await getWordFun(values.age, values.name);
// 接收流文件数据并下载
const blob = new Blob([res], {
type: res.type,
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'test.docx';
link.click();
};
return (
<>
<ProForm title="新建表单" onFinish={onFinish}>
<ProFormText name="name" label="名称" placeholder="请输入名称" />
<ProFormDigit type={'number'} name="age" label="年龄" placeholder="请输入年龄" />
</ProForm>
</>
);
};
export default Report;
下载的文件内容
图片
图片标签以@开始:{{@var}}
@Test
void test3() {
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 18);
data.put("img", Pictures.ofUrl("http://deepoove.com/images/icecream.png")
.size(100, 100).create());
// 加载本地模板文件
InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);
try {
// 写出到文件
template.writeAndClose(new FileOutputStream("output.docx"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
表格
表格标签以#开始:{{#var}}
// 插入表格
@Test
void test4() {
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 18);
data.put(
"img", Pictures.ofUrl("http://deepoove.com/images/icecream.png").size(100, 100).create());
// 第0行居中且背景为蓝色的表格
RowRenderData row0 =
Rows.of("学历", "时间").textColor("FFFFFF").bgColor("4472C4").center().create();
RowRenderData row1 = Rows.create("本科", "2015~2019");
RowRenderData row2 = Rows.create("研究生", "2019~2021");
data.put("eduList", Tables.create(row0, row1, row2));
// 加载本地模板文件
InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);
try {
// 写出到文件
template.writeAndClose(new FileOutputStream("output.docx"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
表格行循环
我们希望根据一个集合的内容来决定表格的行数,这是就用到表格行循环
货物明细需要展示所有货物,
{{goods}}
是个标准的标签,将{{goods}}
置于循环行的上一行,循环行设置要循环的标签和内容,注意此时的标签应该使用[]
,以此来区别poi-tl的默认标签语法。
示例代码
// 循环行表格
@Test
void test5() {
Good good = new Good();
good.setName("小米14");
good.setPrice("4599");
good.setColor("黑色");
good.setTime("2024-05-23");
Good good2 = new Good();
good2.setName("苹果15");
good2.setPrice("7599");
good2.setColor("黑色");
good2.setTime("2024-05-23");
Good good3 = new Good();
good3.setName("华为Meta60");
good3.setPrice("7999");
good3.setColor("白色");
good3.setTime("2024-05-23");
ArrayList<Good> goods = new ArrayList<>();
goods.add(good);
goods.add(good2);
goods.add(good3);
// 定义模板对应的数据
HashMap<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 18);
data.put(
"img", Pictures.ofUrl("http://deepoove.com/images/icecream.png").size(100, 100).create());
// 第0行居中且背景为蓝色的表格
RowRenderData row0 =
Rows.of("学历", "时间").textColor("FFFFFF").bgColor("4472C4").center().create();
RowRenderData row1 = Rows.create("本科", "2015~2019");
RowRenderData row2 = Rows.create("研究生", "2019~2021");
data.put("eduList", Tables.create(row0, row1, row2));
// 添加采购列表数据
data.put("goods", goods);
// 加载本地模板文件
InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");
// 定义行循环插件
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
// 绑定插件
Configure config = Configure.builder().bind("goods", policy).build();
// 渲染模板
XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);
try {
// 写出到文件
template.writeAndClose(new FileOutputStream("output.docx"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Data
public class Good {
private String name;
private String price;
private String color;
private String time;
}