思路:利用Freemarker模板生成docx格式的word文档
具体操作步骤:
1、
首先在pom.xml中添加freemarker依赖
<dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> <scope>compile</scope> </dependency>
需要过滤掉不需要编码的文件:过滤后缀为.zip的所有文件,不对其进行统一编码
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <!--<version>3.1.0</version>--> <configuration> <encoding>UTF-8</encoding> <nonFilteredFileExtensions> <nonFilteredFileExtension>zip</nonFilteredFileExtension> </nonFilteredFileExtensions> </configuration> </plugin>
2、
创建一个docx的word模板,图中英文为需要填充的内容(可随意)
3、
将word文件后缀名docx改为zip,并解压到当前目录下
4、
将之前改好的zip格式的文件和word目录下的document.xml文件拷贝到项目的templates目录下
5、
nacos配置路径和文件名,和上图中的一致
6、
修改document.xml文件内的参数,把meetingTitle改为${metingTitle},【注意:不要格式化】
7、
在<w:tr>前面加上<#list meetingList as meeting>和对应的</w:tr>后面添加</#list>,用于循环填充
<#list meetingList as meeting> <w:tr> <w:tblPrEx> <w:tblBorders> <w:top w:val="single" w:color="auto" w:sz="4" w:space="0"/> <w:left w:val="single" w:color="auto" w:sz="4" w:space="0"/> <w:bottom w:val="single" w:color="auto" w:sz="4" w:space="0"/> <w:right w:val="single" w:color="auto" w:sz="4" w:space="0"/> <w:insideH w:val="single" w:color="auto" w:sz="4" w:space="0"/> <w:insideV w:val="single" w:color="auto" w:sz="4" w:space="0"/> </w:tblBorders> <w:tblCellMar> <w:top w:w="0" w:type="dxa"/> <w:left w:w="108" w:type="dxa"/> <w:bottom w:w="0" w:type="dxa"/> <w:right w:w="108" w:type="dxa"/> </w:tblCellMar> </w:tblPrEx> <w:tc> <w:tcPr> <w:tcW w:w="771" w:type="pct"/> </w:tcPr> <w:p> <w:pPr> <w:spacing w:after="0" w:line="220" w:lineRule="atLeast"/> <w:jc w:val="left"/> <w:rPr> <w:rFonts w:hint="default" w:ascii="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/> <w:sz w:val="32"/> <w:szCs w:val="32"/> <w:lang w:val="en-US" w:eastAsia="zh-CN"/> </w:rPr> </w:pPr> </w:p> </w:tc> <w:tc> <w:tcPr> <w:tcW w:w="818" w:type="pct"/> <w:vAlign w:val="center"/> </w:tcPr> <w:p> <w:pPr> <w:spacing w:after="0" w:line="220" w:lineRule="atLeast"/> <w:jc w:val="left"/> <w:rPr> <w:rFonts w:hint="default" w:ascii="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/> <w:sz w:val="32"/> <w:szCs w:val="32"/> <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="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/> <w:sz w:val="32"/> <w:szCs w:val="32"/> <w:lang w:val="en-US" w:eastAsia="zh-CN"/> </w:rPr> <w:t>${meeting.meetingTime}</w:t> </w:r> </w:p> </w:tc> <w:tc> <...省略...> </w:tr> </#list>
8、
合并单元格在第一次循环时添加<w:vmerge w:val=“restart”/>,其他循环加上<w:vmerge w:val=“continue”/>
<w:tcPr> <w:tcW w:w="771" w:type="pct"/> <w:vMerge w:val="restart"/> <w:vAlign w:val="center"/> </w:tcPr> <w:tcPr> <w:tcW w:w="725" w:type="pct"/> <w:vMerge w:val="continue"/> <w:tcBorders/> </w:tcPr>
9、
JAVA代码如下:
@Service public class Test extends ServiceImpl<TestMapper, Test> implements TestService { @Resource private TestMapper testMapper; @Value("${template.xmlParentPath}") private String xmlParentPath; @Value("${template.xmlTemplateName}") private String xmlTemplateName; @Value("${template.zipFileUrl}") private String zipFileUrl; @Override public void download(String id, HttpServletRequest request, HttpServletResponse response) { List<Map<String, Object>> mapList = testMapper.getData(id); Map<String, Object> map = testMapper.getMettingData(id); if (CollUtil.isEmpty(mapList) || map == null || map.size() == 0) { throw new GenericException(purchaseCodeEnum.DATA_IS_NULL.getCode(), purchaseCodeEnum.DATA_IS_NULL.getMsg()); } map.put("mettingList",mapList); try { String fileName = fileService.encodeChineseDownloadFileName(request, "测试word.docx"); WordTool.downWordDocxOrDoc(map, null, null, xmlParentPath, false, xmlTemplateName, zipFileUrl, false, fileName, request,response); } catch (Exception e) { Log.error(e.getMessage()); throw new GenericException(purchaseCodeEnum.DOWNLOAD_FILE_ERROR.getCode(), purchaseCodeEnum.DOWNLOAD_FILE_ERROR.getMsg()); } } }
/** * description:对文件流输出下载的中文文件名进行编码 屏蔽各种浏览器版本的差异性 * @param request http请求 * @param fileName 文件名称 * @return java.lang.String 新的zip名称 * @author: lx * @date: 2022/2/22 22:57 */ public String encodeChineseDownloadFileName(HttpServletRequest request, String fileName) throws Exception { String newFileName = null; String agent = request.getHeader("USER-AGENT"); if (null != agent) { if (-1 != agent.indexOf("Firefox")) {//Firefox newFileName = "=?UTF-8?B?" + (new String(org.apache.commons.codec.binary.Base64.encodeBase64(fileName.getBytes( "UTF-8")))) + "?="; } else if (-1 != agent.indexOf("Chrome")) {//Chrome newFileName = new String(fileName.getBytes(), "ISO8859-1"); } else {//IE7+ newFileName = java.net.URLEncoder.encode(fileName, "UTF-8"); newFileName = newFileName.replace("+", "%20"); } } else { newFileName = fileName; } return newFileName; }
public class WordTool { private static final Logger LOGGER = LoggerFactory.getLogger(WordTool.class); /** * 参数错误码 */ private static final Integer PARAM_ERROR_CODE = 500001; /** * 参数错误描述 */ private static final String PARAM_ERROR_DESC = "参数有误!"; /** * 生成word错误码 */ private static final Integer WORD_CREATE_ERROR_CODE = 500002; /** * word生成失败描述 */ private static final String WORD_CREATE_ERROR_DESC = "word生成失败"; /** * word转html错误码 */ private static final Integer WORD_TO_HTML_ERROR_CODE = 500003; /** * word转html描述 */ private static final String WORD_TO_HTML_ERROR_DESC = " word转html失败"; /** * 默认的编码为utf-8 */ private static final String DEFAULT_ENCODING = "utf-8"; /** * 生成及下载word文件 * description: 使用document.xml(由word模版解压而来)来创建docx或者doc后缀的word文件的方法,此方法可以生成docx及doc * 注意:这里document.xml不要进行格式化,否则会导致word如果有做预览的时候格式错乱 * @param dataModel word数据模型,可以是map * @param versionCode 版本号,可为空,默认为:2.3.23版本 * @param encoding 编码,可为空,默认为:utf-8 * @param xmlParentPath document.xml文件的父路径, 相对路径或者绝对路径,相对路径时指:相对classpath的路径, * 可为空,当表示绝对父路径时必填,相对父路径时可不填,不填默认加载配置文件中的: * spring.freemarker.template-loader-path=classpath:/templates/ 的配置 * @param xmlParentPathIsAP document.xml文件的父路径是否是绝对路径,true-是,false-否 * @param xmlTemplateName document.xml文件的名称,需以.xml结尾 * @param zipFileUrl zip文件的路径(将模版docx或者doc文件后缀修改为.zip即可),需以.zip结尾, * 相对路径或者绝对路径,相对路径时指:相对classpath的路径 * @param zipFileUrlIsAp zip文件是否是绝对路径,true-是,false-否 * @param fileName 生成文件的名称,需以.docx或者.doc结尾 * @return void 无 * @author: lx * @date: 2022/2/22 22:52 */ public static synchronized void downWordDocxOrDoc(Object dataModel, String versionCode, String encoding, String xmlParentPath, boolean xmlParentPathIsAP, String xmlTemplateName, String zipFileUrl, boolean zipFileUrlIsAp, String fileName, HttpServletRequest request, HttpServletResponse response) throws GenericException,IOException { Version version = null; if (StringUtils.isBlank(versionCode)) { //默认为2.3.23 version = Configuration.VERSION_2_3_23; } else { version = new Version(versionCode); } if (StringUtils.isBlank(encoding)) { encoding = DEFAULT_ENCODING; } if (dataModel == null) { LOGGER.error("[xml生成word]数据为空"); throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC); } if (StringUtils.isBlank(xmlParentPath) || StringUtils.isBlank(xmlTemplateName) || StringUtils.isBlank(fileName) || StringUtils.isBlank(zipFileUrl)) { LOGGER.error("[xml生成word]xml文件的名字(xmlTemplateName)、" + "zip文件的路径(zipFileUrl)、输出文件名称(fileName)存在空的参数"); throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC); } if (xmlParentPathIsAP && StringUtils.isBlank(xmlParentPath)) { LOGGER.error("[xml生成word]xml文件的父路径(xmlParentPath)不能为空"); throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC); } //如果xml模版名字不是以.xml结尾的 if (!xmlTemplateName.endsWith(".xml")) { LOGGER.error("[xml生成word]xml模版的名字(xmlTemplateName)名称有误,不是.xml后缀"); throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC); } if (!zipFileUrl.endsWith(".zip")) { LOGGER.error("[xml生成word]zip文件的路径(zipFileUrl)有误,不是.zip后缀"); throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC); } OutputStream outputStream = response.getOutputStream(); //设置要下载的文件的名称 response.setHeader("Content-disposition", "attachment;fileName=" + fileName); //设置MIME类型 response.setContentType("application/msword;charset=UTF-8"); ByteArrayInputStream in = null; ZipOutputStream zipOut = null; // FileOutputStream outputStream = null; InputStream inputStream = null; ZipInputStream zipInputStream = null; try { //获取模板 Template template = getConfiguration(version, encoding, xmlParentPath, xmlParentPathIsAP).getTemplate(xmlTemplateName); StringWriter swriter = new StringWriter(); //生成文件 template.process(dataModel, swriter); //这里一定要设置utf-8编码 否则导出的word中中文会是乱码 in = new ByteArrayInputStream(swriter.toString().getBytes(encoding)); //生成docx文件的地址 // outputStream = new FileOutputStream(file); //重新打包的zip输出流(新的zip文件) zipOut = new ZipOutputStream(outputStream); int len = -1; byte[] buffer = new byte[1024]; //如果是绝对路径 if (zipFileUrlIsAp) { //zip文件(其实就是docx文件,改个后缀而已) File docxFile = new File(zipFileUrl); ZipFile zipFile = new ZipFile(docxFile); Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries(); //开始覆盖文档------------------ while (zipEntrys.hasMoreElements()) { ZipEntry next = zipEntrys.nextElement(); InputStream is = zipFile.getInputStream(next); if (next.toString().indexOf("media") < 0) { zipOut.putNextEntry(new ZipEntry(next.getName())); //如果是word/document.xml由我们输入 if ("word/document.xml".equals(next.getName())) { if (in != null) { while ((len = in.read(buffer)) != -1) { zipOut.write(buffer, 0, len); } in.close(); in = null; } } else { while ((len = is.read(buffer)) != -1) { zipOut.write(buffer, 0, len); } is.close(); } } } } else { //如果是相对路径,用流读取zip文件 ClassPathResource resource = new ClassPathResource(zipFileUrl); inputStream = resource.getInputStream(); zipInputStream = new ZipInputStream(inputStream); ZipEntry zipEntry = null; while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (zipEntry.toString().indexOf("media") < 0) { zipOut.putNextEntry(new ZipEntry(zipEntry.getName())); //如果是word/document.xml由我们输入 if ("word/document.xml".equals(zipEntry.getName())) { if (in != null) { while ((len = in.read(buffer)) != -1) { zipOut.write(buffer, 0, len); } in.close(); in = null; } } else { while ((len = zipInputStream.read(buffer)) != -1) { zipOut.write(buffer, 0, len); } } } } zipInputStream.close(); inputStream.close(); } LOGGER.info("[xml生成word]生成word文件成功"); } catch (Exception e) { LOGGER.error("[xml生成word]生成word出错", e); throw new GenericException(WORD_CREATE_ERROR_CODE, WORD_CREATE_ERROR_DESC); } finally { try { if (zipOut != null) { zipOut.flush(); zipOut.close(); } if (outputStream != null) { outputStream.flush(); outputStream.close(); } if (in != null) { in.close(); } } catch (Exception e) { LOGGER.error("", e); throw new GenericException(WORD_CREATE_ERROR_CODE, WORD_CREATE_ERROR_DESC); } } } }