简单的表格型Word生成

803 阅读6分钟

1.写在前面

(原文在语雀里写的,导到掘金样式有些缺失~~~)

项目里经常会出现需要Word表格导出的情况,笔者遇到最多的形式是表格的形式,类似这样:

对于这种表格形式的Word导出,我们常用的工具是POI

POIApache开源项目下面的一个,它主要为了解决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}}

为了更好的了解这种方式,这里我们可以用WPSWord保存为.xml格式:

我们打开可以看到对应的文件内容

本质上,这种通过模版引擎导出Word的工作,实际上就是对xml长文本中的占位符使用数据进行替换的过程。(这里需要注意.doc格式的文件不全等于xml文件,它本质上是个压缩包,里面的xml文件是其中的主要部分,POI解压并读取这部分,替换对应的内容后再重新打包)

这种方案不仅应用在Word模版替换中,早期的前后端不分离的web框架中,常见的JSPFreemakerThymeleaf都是这种预先构建模版然后数据替换的过程,只不过替换对象从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());
        }
    }

这里的工具类主要做了这些事:

  1. 设置响应头
  2. 转化我们传入的对象的格式为Map类型
  3. 引擎渲染
  4. 流处理

这里的转换的工具类主要是将任意的对象转化为以字段名为key,值为valueMap对应的代码如下:

	/**
     * 将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.附录

语雀:www.yuque.com/zhoujianze/…

git:github.com/ThreeBody19…

gitee:gitee.com/inspiration…

poi-tl:deepoove.com/poi-tl/1.7.…