为了导出docx格式看了等多文档,最后做个总结依赖包用到dom4j和freemarker,最为方便。
<!-- https://mvnrepository.com/artifact/freemarker/freemarker -->
<dependency>
<groupId>freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
0.主要目的:将这样一个页面导出为word文档为doc格式,包含一些文本和循环遍历出来的echarts图表。
1.新建一个word文档(docx格式或doc),生成模板内容,例如下面这种。
整体思路
-保存后,复制出来一份,
-修改后缀名为zip。
-解压到一个文件夹中。
-打开文件夹看到如下目录
-获取word里的document.xml文档以及_rels文件夹下的document.xml.rels文档
-把内容填充到document.xml里,以及图片配置信息填充至document.xml.rels文档里
-在输入docx文档的时候把填充过内容的的 document.xml、document.xml.rels用流的方式写入zip(详见下面代码)。
-把图片写入zip文件下word/media文件夹中
-输出docx文档(因为word文档本身就是ZIP格式实现的)
2. 目录结构如下:主要文件由上一步拷贝过来的
- document.xml里存放主要数据
- media存放图片信息
- _rels里存放配置信息
document.xml中存放图片的模板主要内容
3.document.xml修改模板内容加上freemarker遍历map集合,填入数据
4.document.xml.rels修改模板引用内容
注意:这里图片配置信息是根据 rId来获取的。docx模板总的${mdl.rId}就是rId的具体值。
为了避免重复,我的图片rId从12开始(在我没有修改之前,里面最大的rId是rId12)。
5.header1.xml页眉 (可不要)
6.[Content_Types].xml文件模板
7.前端页面关键就是请求,根据自己需求构建json字符串格式,map数据传到后台
//导出word docx
function download_reportNew() {
//此处遍历页面得数据,放到json中,可根据自己需要省略-----------------start
console.log("new wordx");
var title= '${reportData.title}';
var reportUnit= '${reportData.reportUnit!}';
var reportTypeDate= '${reportData.reportTypeDate!}';
var json;
var jsonHead = {"title": title, "reportUnit": reportUnit,"reportTypeDate":reportTypeDate};
//此处用到了freemarker的模板遍历数据
<#list reportData.reportModels as model>
var modelTitle_${model ? index}= '${model.title!}';
var modelDataSource_${model ? index}= '${model.dataSource!}';
var modelShowContent_${model ? index}= '${model.showContent!}';
var model_pic_${model ? index} = null ;
var pic_${model ? index} = null ;
if (model_${model ? index}.option != null) {
pic_${model ? index}=model_${model ? index}.chart.getConnectedDataURL();
model_pic_${model ? index}=pic_${model ? index}.substr(22,pic_${model ? index}.length);
}
var jsonBody = {"model_${model ? index}":{
"modelTitle": modelTitle_${model ? index},
"modelDataSource": modelDataSource_${model ? index},
"modelShowContent":modelShowContent_${model ? index},
"model_pic":model_pic_${model ? index}
}};
//最后的json对象
json=$.extend(true,jsonHead,jsonBody);
</#list>
//此处遍历页面得数据,放到json中,可根据自己需要省略-----------------end
$.ajax({
//第一次请求生成doc临时文件
url: '${base}/report/reportView/reportExportNew.do',
method: 'POST',
contentType: 'application/json;charset=utf-8',
data: JSON.stringify(json),
success: function (data) {
if (data.status == 0) {
//第二次请求读取文件写入response输出流,实现下载。
window.location.href = '${base}/report/reportView/reportExportLast.do'+ "?filepath=" + data.retinfo ;
} else {
alert("下载word失败!");
}
},
error: function (data) {
alert('文件下载失败' + data);
}
})
}
8.第一次请求生成doc临时文件:word导出为doc格式的后台controller类
/**
* @param
* @Description 报表导出
* @Date 2019/11/20 11:53
* @Param map 填入模板的数据
* @Author
*/
@PostMapping("/reportExportNew")
public void reportExportNew(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String,
Object> map) throws IOException {
WebResult res = new WebResult();
try {
String lastFilePath = reportViewService.createWordDocx(map);
String lastPath = lastFilePath.replace(SEPARATOR,"~");
String text=filastPath;
//生成的文件名放到response中返回(此处根据自己需要可直接返回text) ---------start
PrintWriter out = null;
try {
response.setContentType("application/json;charset=UTF-8");
out = response.getWriter();
out.write(text);
} catch (IOException var9) {
LOGGER.error(var9.getMessage(), var9);
} finally {
if (out != null) {
out.print("");
out.close();
}
}
//生成的文件名放到response中返回(此处根据自己需要可直接返回text) ---------end
} catch (Exception e) {
e.printStackTrace();
}
}
9.后台构建map填入模板需要的数据createWordDocx(map)方法
/**
* 创建docx 返回临时路径
* @param map
* @return
* @throws IOException
*/
public String createWordDocx(Map<String, Object> map) throws IOException{
/**
* @param dataMap 参数数据
* @param docxTemplateFile docx模主板名称
* @param xmlDocument docx中document.xml模板文件 用来存在word文档的主要数据信息
* @param xmlDocumentXmlRels docx中document.xml.rels 模板文件 用来存在word文档的主要数据配置 包括图片的指向
* @param xmlContentTypes docx中 [Content_Types].xml 模板文件 用来配置 docx文档中所插入图片的类型 如 png、jpeg、jpg等
* @param xmlHeader docx中 header1.xml 模板文件 用来配置docx文档的页眉文件
* @param templatePath 模板存放路径 如 /templates/
* @param outputFileTempPath 所生成的docx文件的临时路径文件夹 如果 temp/20180914051811/
* @param outputFileName 所生成的docx文件名称 如 xxx.docx 或 xxx.doc
* */
String timeStr = LocalDateUtils.getCurrentTime_yyyyMMddHHmmssSSS();
String docxTemplateFile = "docxTemplates.docx";
String xmlDocument = "document.xml";
String xmlDocumentXmlRels = "document.xml.rels";
String xmlContentTypes = "[Content_Types].xml";
//可以用来修改页眉的一些信息
String xmlHeader = "header1.xml";
String templatePath = SEPARATOR + "template" + SEPARATOR;
String outputFileTempPath = templatePath+"temp" + SEPARATOR + timeStr + SEPARATOR;
String outputFileName = timeStr + "."+SUFFIX_DOCX;
String classPath=ReportViewServiceImpl.class.getResource("/").getPath().toString();
LOGGER.info("classPath:{}",classPath);
LOGGER.info("templatePath:{}",templatePath);
LOGGER.info("outputFileTempPath:{}",outputFileTempPath);
LOGGER.info("outputFileName:{}",outputFileName);
//填充整体数据
Map<String, Object> dataMap = new HashMap<>(16);
//模块内容列表
List<Map<String, Object>> modelList = new ArrayList<>(16);
//单个模块
Map<String, Object> model;
// 页眉
dataMap.put("ymdhis", LocalDateUtils.getCurrentTime_yyyyMMddHHmmss());
// 图片类型
List<String> modelTypes = new ArrayList<>();
modelTypes.add("png");
dataMap.put("mdlTypes", modelTypes);
//取空白图片Base64码
String url = classPath+"template/blank.png";
String blankEncode = getImageStr(url);
// 文档标题
dataMap.put("title", map.get("title"));
dataMap.put("reportUnit", map.get("reportUnit"));
dataMap.put("reportTypeDate", map.get("reportTypeDate"));
//模块数量
int modelNum = map.size() - dataMap.size()+2;
for (int i = 0; i < modelNum; i++) {
model = (Map<String, Object>) map.get("model_" + i);
if (model.get("model_pic") == null) {
//64位编码格式改成path和name
model.put("model_pic", blankEncode);
}
//每个文件路径
String fileName = "pic"+i+".png";
String filePath = classPath+outputFileTempPath+fileName;
model.put("path",filePath);
model.put("name",fileName);
modelList.add(model);
}
//批量生成文件
for (Map<String, Object> mod:modelList) {
baseToFile(mod);
}
dataMap.put("modelList", modelList);
try {
String lastFilePath = WordUtils.createDocx(dataMap, docxTemplateFile, xmlDocument, xmlDocumentXmlRels, xmlContentTypes,
xmlHeader, templatePath, outputFileTempPath, outputFileName);
return lastFilePath;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
10.wordUtiles类的createDocx
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* @Description docx、doc文档生成工具类 (改变后缀名即可)
* 在使用制作模板的过程中如果模板中有图片那就保留图片,注意[Content_Types].xml和document.xml.rels文档
* 如果模板中没有图片 则不需要设置[Content_Types].xml和document.xml.rels
* 由于word模板的个性化 所以 每次做模板都要重新覆盖原来的模板
* @Author
*/
public class WordUtils {
private final static String SEPARATOR = File.separator;
/**
* @param dataMap 参数数据
* @param docxTemplateFile docx模主板名称
* @param xmlDocument docx中document.xml模板文件 用来存在word文档的主要数据信息
* @param xmlDocumentXmlRels docx中document.xml.rels 模板文件 用来存在word文档的主要数据配置 包括图片的指向
* @param xmlContentTypes docx中 [Content_Types].xml 模板文件 用来配置 docx文档中所插入图片的类型 如 png、jpeg、jpg等
* @param xmlHeader docx中 header1.xml 模板文件 用来配置docx文档的页眉文件
* @param templatePath 模板存放路径 如 /templates/
* @param outputFileTempPath 所生成的docx文件的临时路径文件夹 如果 temp/20180914051811/
* @param outputFileName 所生成的docx文件名称 如 xxx.docx 或 xxx.doc
*/
public static String createDocx(Map dataMap, String docxTemplateFile, String xmlDocument, String xmlDocumentXmlRels,
String xmlContentTypes, String xmlHeader, String templatePath,
String outputFileTempPath, String outputFileName) throws Exception {
URL basePath = WordUtils.class.getClassLoader().getResource("");
String realTemplatePath = basePath.getPath() + templatePath;
//临时文件产出的路径
String outputPath = basePath.getPath() + outputFileTempPath;
String lastFilePath = outputFileTempPath+outputFileName;
List<String> delFileList = new ArrayList<>();
try {
//获取 document.xml.rels 输入流
String xmlDocumentXmlRelsComment = FreeMarkUtils.getFreemarkerContent(dataMap, xmlDocumentXmlRels, templatePath);
ByteArrayInputStream documentXmlRelsInput = new ByteArrayInputStream(xmlDocumentXmlRelsComment.getBytes());
//获取 header1.xml 输入流
ByteArrayInputStream headerInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlHeader, templatePath);
//获取 [Content_Types].xml 输入流
ByteArrayInputStream contentTypesInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlContentTypes, templatePath);
//读取 document.xml.rels 文件 并获取rId 与 图片的关系 (如果没有图片 此文件不用编辑直接读取就行了)
Document document = DocumentHelper.parseText(xmlDocumentXmlRelsComment);
// 获取根节点
Element rootElt = document.getRootElement();
// 获取根节点下的子节点head
Iterator iter = rootElt.elementIterator();
List<Map<String, String>> picList = (List<Map<String, String>>) dataMap.get("modelList");
// 遍历Relationships节点
while (iter.hasNext()) {
Element recordEle = (Element) iter.next();
String id = recordEle.attribute("Id").getData().toString();
String target = recordEle.attribute("Target").getData().toString();
if (target.indexOf("media") == 0) {
for (Map<String, String> picMap : picList) {
if (target.endsWith(picMap.get("name"))) {
picMap.put("rId", id);
}
}
}
}
//覆盖原来的picList;
dataMap.put("modelList", picList);
//获取 document.xml 输入流
ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlDocument, templatePath);
File docxFile = new File(realTemplatePath + SEPARATOR + docxTemplateFile);
if (!docxFile.exists()) {
docxFile.createNewFile();
}
ZipFile zipFile = new ZipFile(docxFile);
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
File tempPath = new File(outputPath);
//如果输出目标文件夹不存在,则创建
if (!tempPath.exists()) {
tempPath.mkdirs();
}
ZipOutputStream zipout = new ZipOutputStream(new FileOutputStream(outputPath + outputFileName));
//覆盖文档
int len = -1;
byte[] buffer = new byte[1024];
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
// 把输入流的文件传到输出流中 如果是word/document.xml由我们输入
zipout.putNextEntry(new ZipEntry(next.getName()));
//写入图片配置类型
if ("[Content_Types].xml".equals(next.getName())) {
if (contentTypesInput != null) {
while ((len = contentTypesInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
contentTypesInput.close();
}
} else if (next.getName().indexOf("document.xml.rels") > 0) {
//写入填充数据后的主数据配置信息
if (documentXmlRelsInput != null) {
while ((len = documentXmlRelsInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
documentXmlRelsInput.close();
}
} else if ("word/document.xml".equals(next.getName())) {
//写入填充数据后的主数据信息
if (documentInput != null) {
while ((len = documentInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
documentInput.close();
}
} else if ("word/header1.xml".equals(next.getName())) {
//写入填充数据后的页眉信息
if (headerInput != null) {
while ((len = headerInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
headerInput.close();
}
} else {
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
//覆盖文档
//写入新图片
len = -1;
if (picList != null && !picList.isEmpty()) {
for (Map<String, String> pic : picList) {
ZipEntry next = new ZipEntry("word" + SEPARATOR + "media" + SEPARATOR + pic.get("name"));
zipout.putNextEntry(new ZipEntry(next.toString()));
InputStream in = new FileInputStream(pic.get("path"));
while ((len = in.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
in.close();
}
}
zipout.close();
return lastFilePath;
} catch (Exception e) {
e.printStackTrace();
throw new Exception("生成docx文件失败!");
}
}
/**
* 递归删除文件夹
*
* @param dir
*/
public static void delFiles(String dir) {
try {
File file = new File(dir);
if(!file.exists()){
return;
}
if(file.isFile() || file.list()==null) {
file.delete();
System.out.println("删除了"+file.getName());
}else {
File[] files = file.listFiles();
for(File a:files) {
a.delete();
}
file.delete();
System.out.println("删除了"+file.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
11.第二次请求读取文件写入response输出流,实现下载。:将文件输出到response中,浏览器实现下载
/**
* @param
* @Description 报表导出
* @Date 2019/11/13 11:53
* @Author
*/
@RequestMapping("/reportExportLast")
public void reportExportLast(HttpServletRequest request, HttpServletResponse response) throws IOException {
String templateName = request.getParameter("filepath");
if (templateName.isEmpty()) {
templateName = "report" + System.currentTimeMillis();
}
String classPath=ReportViewAction.class.getResource("/").getPath().toString();
String fileName = templateName.replace("~",SEPARATOR);
String filePath = classPath+fileName;
String name = "";
//文件后缀名
String fileExt = fileName.substring(fileName.lastIndexOf(".")+1);
File file = new File(filePath);
try (InputStream inputStream = new FileInputStream(file);
ServletOutputStream out = response.getOutputStream()) {
if (SUFFIX_DOCX.equals(fileExt)){
name = new String("大数据报告.docx".getBytes("UTF-8"),"UTF-8");
response.setContentType("application/msword;charset=UTF-8");
}else if (SUFFIX_PDF.equals(fileExt)){
name = new String("大数据报告.pdf".getBytes("UTF-8"),"UTF-8");
response.setContentType("application/pdf;charset=UTF-8");
}
name = URLEncoder.encode(name,"UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + name);
byte[] buffer = new byte[1024];
int bytesToRead;
while ((bytesToRead = inputStream.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//删除临时文件
String outputPath = filePath.substring(0,filePath.lastIndexOf(SEPARATOR)+1);
WordUtils.delFiles(outputPath);
}
}
具体代码
https://gitee.com/zc0709/JavaUtilsProject