思路:利用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);
}
}
}
}