日常搬砖中,来了一个需求需要导出一个word报告,文件类型有doc类型,有docx类型。并且要严格按照需求的文档模板来导出,字体样式行间距等等都需要和模板一样,如此就想到了模板结合Freemaker语法导出我们需要的文档报告。
2.1 先看一下word模板,这里给出的是调整后的word模板,模板包含了:单条赋值,循环赋值,表格赋值,基本满足所有的文档需求了,如果有其他的赋值方式,按照上面三条改造一下即可。
2.2 对模板进行改造,采用Freemaker语法进行变量字段的声明。上下图对比可见,日期和标题一是单条赋值,标题二是循环赋值,标题三是表格循环赋值。
2.3 以上操作均是使用 word工具打开直接手工编辑生成。
本项目是Springboot + Freemaker 实现,以下给出本项目的Springboot版本和Freemaker依赖版本。学习的同学可以结合最新版本使用。这里给出 阿里maven依赖仓库,大家可以去查最新的依赖版本。
// Springboot版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
// Freemaker版本
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
3.2.1 打开修改后的模板文档,另存为 Word XML 文档 类型文件,这里先将文件名修改成test_template,当然在项目里修改也行,如下图:
3.2.2 在Springboot项目中 resources 下新建 templates 文件夹,将上述的 test_template.xml文件放入templates 文件夹中并修改文件后缀名为 ftl(freemarker的文件名是以.ftl后缀的),如下图:
3.2.3 这里有点需要注意,就是模板文件 ${text} 域 可能出现格式错误,这样在生成模板文件的时候就会报错,所以我们事先打开文件查看一下,发现如下错误,将花括号里的内容删除,然后填充上变量字段。如下图:
修改前:
修改后:
3.2.4 Freemaker语法这里贴几个,有别的需要百度查一下Freemaker语法即可。在IDEA里编辑ftl文件时候,千万别格式化,不然导出的样式会让你有意想不到的结果。最后由于完整样例导致页面太卡,无奈只能部分关键样例,为了大家看着方便我给样例格式化了一下,但是大家在做的时候千万不要格式化啊。
// 直接赋值
${compareLayer}
// 集合遍历
<#list filterResults as filterResult>
${filterResult}
</#list>
// 判断集合是否为空
<#if (filterResults?? && filterResults?size > 0) >
</#if>
// ftl部分样例
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
w:eastAsia="微软雅黑" w:cs="Times New Roman"/>
<w:sz w:val="20"/>
<w:szCs w:val="20"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${reportDate}</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
w:cs="Times New Roman"/>
<w:b/>
<w:bCs/>
<w:sz w:val="24"/>
<w:szCs w:val="24"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>标题二</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:hint="default" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
w:cs="Times New Roman"/>
<w:b/>
<w:bCs/>
<w:sz w:val="24"/>
<w:szCs w:val="24"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>]</w:t>
</w:r>
</w:p><#list text2s as text>
<w:p>
<w:pPr>
<w:keepNext w:val="0"/>
<w:keepLines w:val="0"/>
<w:pageBreakBefore w:val="0"/>
<w:widowControl w:val="0"/>
<w:numPr>
<w:ilvl w:val="0"/>
<w:numId w:val="0"/>
</w:numPr>
<w:kinsoku/>
<w:wordWrap/>
<w:overflowPunct/>
<w:topLinePunct w:val="0"/>
<w:autoSpaceDE/>
<w:autoSpaceDN/>
<w:bidi w:val="0"/>
<w:adjustRightInd/>
<w:snapToGrid/>
<w:spacing w:line="360" w:lineRule="auto"/>
<w:jc w:val="both"/>
<w:rPr>
<w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
w:cs="Times New Roman"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
w:cs="Times New Roman"/>
<w:lang w:val="en-US" w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${text}</w:t>
</w:r></w:p></#list>
3.2.5 还有需要注意的,maven在编译打包的时候,可能不会将 ftl模板文件 打包到 target 中,这里就需要在pom文件中添加配置,如下图:
3.2.4 生成doc模板工具类,这里工具方法有,直接以流的形式将doc文件输到前端,将doc文件以InputStream流返回,也有生成docx的方法,这里为下面生成docx做准备。
package util.wordtemplate;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ResourceUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
@Slf4j
public class WordTemplateUtil {
private Configuration configuration;
//模板文件的位置
private static String tempPath;
/**
* 构造函数
*/
public WordTemplateUtil() {
configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
configuration.setDefaultEncoding("UTF-8");
configuration.setClassForTemplateLoading(this.getClass(), "/templates");
if(tempPath == null || tempPath.length()==0){
try {
//装载模板文件目录
tempPath = ResourceUtils.getURL("classpath:").getPath() + "templates/";
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
/**
* 获取模板
* @param name
* @return
* @throws Exception
*/
public Template getTemplate(String name) throws Exception {
return configuration.getTemplate(name);
}
/**
* 获取word byte
* @param data
* @param templateName
* @return
* @throws IOException
*/
public InputStream getInputStreamWordDoc(Object data, String templateName) {
return getFreemarkerInputStream(data, templateName);
}
/**
* 获取word byte
* @param data 填充数据
* @param templateName 模板名称
* @param origTemplateName 原始模板名称
* @return
*/
public InputStream getInputStreamWordDocx(Object data, String templateName, String origTemplateName) {
File outFile = null;
OutputStream outputStream = null;
InputStream inputStream = null;
ZipOutputStream zipout = null;
try {
// 临时文件路径
String tempFilePathName = tempPath + UUID.randomUUID().toString().replaceAll("-", "") + ".docx";
outFile = new File(tempFilePathName);
outputStream = new FileOutputStream(outFile);
// 内容模板
ByteArrayInputStream xmlTemplateInput = getFreemarkerInputStream(data, templateName);
//最初设计的模板
String origDocxFilePathName = tempPath + origTemplateName;
File origDocxFile = new File(origDocxFilePathName);
if (!origDocxFile.exists()) {
origDocxFile.createNewFile();
}
ZipFile zipFile = new ZipFile(origDocxFile);
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
zipout = new ZipOutputStream(outputStream);
// 开始覆盖文档
int len = -1;
byte[] buffer = new byte[2 * 1024];
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (!next.toString().contains("media")) {
zipout.putNextEntry(new ZipEntry(next.getName()));
if ("word/document.xml".equals(next.getName())) {
if (xmlTemplateInput != null) {
while ((len = xmlTemplateInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
xmlTemplateInput.close();
}
} else {
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
inputStream = new FileInputStream(outFile);
} catch (Exception e) {
log.error(e.getMessage(), e);
}finally {
if(zipout != null){
try {
zipout.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
if(outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
if (outFile != null) outFile.delete();
}
return inputStream;
}
/**
* 获取模板字符串输入流
* @param data 参数
* @param templateName 模板名称
* @return
*/
public ByteArrayInputStream getFreemarkerInputStream(Object data, String templateName) {
ByteArrayInputStream inputStream = null;
try {
//获取模板
Template template = getTemplate(templateName);
StringWriter swriter = new StringWriter();
//生成文件
template.process(data, swriter);
//这里一定要设置utf-8编码 否则导出的word中中文会是乱码
inputStream = new ByteArrayInputStream(swriter.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return inputStream;
}
/**
* 导出word文档到客户端
* @param response
* @param fileName
* @param tplName
* @param data
* @throws Exception
*/
public void exportDoc(HttpServletResponse response, String fileName, String tplName, Object data) throws Exception {
response.reset();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/msword");
fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=utf-8''" + fileName);
// 把本地文件发送给客户端
Writer out = response.getWriter();
Template template = getTemplate(tplName);
template.process(data, out);
out.close();
}
}
3.2.5 测试一下导出doc文件方法如下:
// Controller 方法
/**
* @Description 导出模板报告
*/
@ApiOperation(value = "导出模板报告接口", notes = "")
@PostMapping("/export")
public void toExportReport(HttpServletResponse response, @RequestBody DataRequest dataRequest) {
try {
testService.exportReport(response, dataRequest);
} catch (BusinessException e) {
log.error(e.getMessage(), e);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
// Service 方法
@Override
public void exportReport(HttpServletResponse response, DataRequest dataRequest) {
try {
WordTemplateUtil templateUtil = new WordTemplateUtil();
templateUtil.exportDoc(response, "报告导出.doc", "test_template.ftl", dataRequest);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
// 使用postman调用接口 模拟参数
{
"reportDate":"20XX年XX月XX日",
"text1":"内容一",
"text2s":[
"内容二",
"内容三",
"内容四"
],
"text3s":[
{
"num":"1",
"text3":"内容五",
"text4":"内容六",
"text5":"内容七",
"text6":"内容八",
"text7":"内容九",
"text8":"内容十"
},
{
"num":"2",
"text3":"内容五1",
"text4":"内容六2",
"text5":"内容七3",
"text6":"内容八4",
"text7":"内容九5",
"text8":"内容十6"
}
]
}
3.3.1 以上述模板为例,解压 docx 文件,在 /word 文件夹下找到 document.xml ,使用 freemaker 语法将该 xml 文件修改成模板,在需要赋值的地方采用 freemaker语法 替换,采用上述doc赋值方式即可。如下图:
3.3.2 将解压出来的 xml 和 docx原文件 放入templates 文件夹下,并重命名test_template,xml 文件内容按照上述doc模板进行修改即可,这里不再赘述。如下图:
3.3.3 使用上面工具类进行赋值导出,这里采用流的形式返回,大家可以根据自己具体业务进行转换成自己需要的返回结果即可。我这原业务其实是需要将流转换成MultipartFile类型,然后调用上传接口将文件上传到文件服务器上,然后再通过feign调用转换接口,将docx文件转换成pdf文件,返回前端pdf下载信息,前端直接下载就行了,这里主要给大家提供学习参考,我就简化了流程,如果大家需要我这流程,大家留言,我会尽快将代码粘出来给大家参考。 方法如下:
public InputStream exportReport(DataRequest dataRequest) {
InputStream inputStream = null;
Date date = new Date();
try {
WordTemplateUtil templateUtil = new WordTemplateUtil();
InputStream inputStream = templateUtil.getInputStreamWordDocx(dataRequest, "test_template.xml", "test_template.docx");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return inputStream;
}
3.3.4 docx模板避坑指南
3.3.4.1 运行项目后调用接口就是找不到xml和docx文件位置,进入target 目录也没有看到,这是需要确认maven打包是否包含了xml文件和docx文件,如果没有请在 pom 如下位置添加配置即可。
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>bootstrap.yml</include>
<include>**/*.xml
**/ *.ftl</include>
<include>**/*.sql
**/ *.docx</include>
</includes>
</resource>
</resources>
3.3.4.2 调用接口会在 ZipFile zipFile = new ZipFile(origDocxFile); 这块报错,这块坑了我两小时才找到原因,一直以为我代码问题,后来发现因为maven在打包的时候会把docx文件进行压缩,所以损坏了文件,如何解决,就是编译的时候不让动docx,配置如下:
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>docx</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>