1.写在前面
(原文在语雀里写的,导到掘金样式有些缺失~~~)项目里经常会出现需要Word表格导出的情况,笔者遇到最多的形式是表格的形式,类似这样:
对于这种表格形式的Word导出,我们常用的工具是POI。
POI是Apache开源项目下面的一个,它主要为了解决java开发者对Office格式的文件进行读写的问题。但是它的底层接口过于复杂,学习成本比较大,使用起来不是很方便,如果处理开头提到的问题我们一般不会直接使用POI去从0开始构建Word,比较好的方案是使用模版+数据。
模版+数据的方案是指预先构建好需要导出Word模版,在需要填充内容的地方使用指定的符号进行占位,最后通过模版引擎把占位符替换为数据,从而完成Word的渲染。
这种模版引擎有很多选择,常见的Freemarker、Mustache等等模版引擎都可以实现。它们的语法是这样的:
A.Freemarker
#变量
${variableName}
#循环
<#list tiems as item>
${item.name}
</#list>
#条件
<#if condition>
</#if>
B.Mustache
#变量
{{variableName}}
#循环
{{#items}}
{{name}}
{{/items}}
#条件
{{#condition}}
<!-- 条件为真时的内容 -->
{{/condition}}
{{^condition}}
<!-- 条件为假时的内容 -->
{{/condition}}
为了更好的了解这种方式,这里我们可以用WPS将Word保存为.xml格式:
我们打开可以看到对应的文件内容
本质上,这种通过模版引擎导出Word的工作,实际上就是对xml长文本中的占位符使用数据进行替换的过程。(这里需要注意.doc格式的文件不全等于xml文件,它本质上是个压缩包,里面的xml文件是其中的主要部分,POI解压并读取这部分,替换对应的内容后再重新打包)
这种方案不仅应用在Word模版替换中,早期的前后端不分离的web框架中,常见的JSP、Freemaker和Thymeleaf都是这种预先构建模版然后数据替换的过程,只不过替换对象从xml改为了html。
为了后面编码方便,我这里使用Mustache模版引擎来处理这种问题。
:::info PS:熟悉Vue的读者应该对Mustache不陌生**,大家如果对这种语法有兴趣,可以到gitHub上看一下Mustache**作者的项目(https://github.com/janl/mustache.js?tab=readme-ov-file),Mustache的意思实际上就是胡子,就像语法中的左右的括号'{{}}'
对,你没看错,他就是下图中的作者
:::
2.实现思路
整体的工作需要完成以下几个步骤:1. 模版构建
2. 数据查询
3. 获取导出模版地址
4. 模版引擎渲染
5. 流导出
具体的时序图如下:
3.演示环境及准备
3.1开发环境
-
java版本: Oracle OpenJDK version 17.0.2
-
开发工具:IntelliJ IDEA 2023.1.5(Ultimate Edition)
-
开发环境:mac15.1.1
-
API测试工具:Apifox 2.3.19
3.2依赖
我们使用的依赖包为**poi-tl**,所装依赖如下:<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.6.0</version>
</dependency>
PS:如果想要详细了解这个依赖的其他功能可以去官网查看
3.3导出模版构建
这里我们需要先准备好导出的模版,预先用占位符对需要填充的地方进行处理,这里是填充好的效果::::info **PS:**这里有几点需要注意的地方:
1.对于特殊格式(比如说图片、表格等等)的占位符是不同,比如上图右上角的个人照片字段(profilePicture),它的占位符是**{{@var}}**的格式,不同的写法具体请参考官网
2.实际生成的文本格式与你录入的占位符的格式有关,比如图中的教育背景我是用了黑体蓝色,后面展示效果的时候也会是对应的格式
3.文件的后缀使用**.docx而不要使用doc,因为我们后面的工具类使用的是.docx**对应的包
4.模版文件在服务器上的存储名称最好使用英文命名,而不要使用中文
:::
4.代码实现
4.1整体Controller思路
在负责导出Controller里我们做这几件事:1. 获取简历数据
2. 获取模版地址(服务器的绝对路径或者相对路径)
3. 调用工具类
具体代码如下:
/**
* 导出简历
* @param id 用户ID
* @param response 响应
* @return 导出结果
*/
@GetMapping("/export/{id}")
public ResponseResult<String> exportResume(@Validated @PathVariable @NotBlank(message = "用户ID不能为空") String id, HttpServletResponse response) {
//获取数据并转化为导出需要的VO
ResumeExport resumeExport = resumeService.getResumeExportById(id);
if (resumeExport == null) {
return new ResponseResult<String>().setHttpResultEnum(HttpResultEnum.PARAM_IS_ERROR);
}
//准备模版文件
String filePath = ymlUtil.getFileUploadPathByModuleName(FileTypeEnum.RESUME.getModuleName()) + File.separator+FILE_NAME;
//导出
WordTemplateUtil.generateWordByBean(filePath,"个人简历",response, resumeExport);
return new ResponseResult<String>().setData("导出成功");
}
我们首先构建了一个导出对象ResumeExport,它和我们需要导出的字段一一对应,需要注意的是,特殊格式的字段(比如说图片、图表等等)需要使用制定的对象封装,这里我们以图片字段举例子,导出对象ResumeExport中加入了图片处理对象PictureRenderData,这个是官方依赖提供的封装图片的对象,文档如下:
整体的导出对象大致如下:
@Data
public class ResumeExport {
//此处省略其他关于简历的对象字段
/**
* 头像地址
*/
private PictureRenderData profilePictureUrl;
/**
* 默认图片宽度
*/
private static final int DEFAULT_PHOTO_WIDTH = 100;
/**
* 默认图片高度
*/
private static final int DEFAULT_PHOTO_HEIGHT = 160;
public ResumeExport(Resume resume, String profilePictureUrl) {
BeanUtils.copyProperties(resume, this);
this.profilePictureUrl= new PictureRenderData(DEFAULT_PHOTO_WIDTH, DEFAULT_PHOTO_HEIGHT,new File(profilePictureUrl));
}
}
这里需要注意的是文件的路径filePath应该使用文件的服务器路径,不要使用网络路径,否则后面工具类读取文件的时候会报错。
4.2导出工具类
这里我们对导出的代码封装在了工具类中,代码如下: /**
* 通过bean生成word
* @param filePath 文件路径
* @param fileName 文件名
* @param response 响应
* @param obj 数据
*/
public static void generateWordByBean(String filePath, String fileName, HttpServletResponse response, Object obj){
//设置响应头
setResponse(response,fileName);
//将对象转化为Map
Map<String,Object> map=translate2Map(obj);
//输出文件
try {
XWPFTemplate template = XWPFTemplate.compile(filePath).render(map);
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
PoitlIOUtils.closeQuietlyMulti(bos,out);
template.close();
} catch (IOException e) {
log.error("IO异常{}",e.getMessage());
}
}
这里的工具类主要做了这些事:
- 设置响应头
- 转化我们传入的对象的格式为Map类型
- 引擎渲染
- 流处理
这里的转换的工具类主要是将任意的对象转化为以字段名为key,值为value的Map对应的代码如下:
/**
* 将VO转化为以字段名称作为key,值为value的Map
* @param obj 对象
* @return Map
*/
private static Map<String,Object> translate2Map(Object obj){
//转化为Map
HashMap<String,Object> map=new HashMap<>();
for(Field field:obj.getClass().getDeclaredFields()){
try {
ReflectionUtils.makeAccessible(field);
map.put(field.getName(),field.get(obj));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
我们使用Apifox调用以上接口,结果如下:
5.总结
对于简单的Word的表格生成,使用Poi-tl这个依赖包还是很好用的,如果对于精细化的需求,比如说高并发或者超大体量的Word表格类型,笔者并未做过测试,这方面请谨慎使用。6.附录
gitee:gitee.com/inspiration…
poi-tl:deepoove.com/poi-tl/1.7.…