赞
踩
日常搬砖中,来了一个需求需要导出一个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</include>
<include>**/*.ftl</include>
<include>**/*.sql</include>
<include>**/*.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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。