基于Freemarker模板引擎生成多个Word文件(doc格式),并压缩成Zip格式导出
一、前言
之前做项目遇到一个导出word并压缩成zip的功能需求,当时翻了不少百度文章,没找到系统、详尽的实操教学,现在项目做完了,我就在这做一个简单的总结吧。
二、环境
SpringBoot + Vue + Linux
<!--依赖-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
三、后端实现过程
1、Freemaker模板准备
第一步:原型-创建WORD文件,画好样式,参数采用注入的写法,例${xxxx}
将word文件另存为 word xml文档或者是word 2003xml文档,建议存为2003版,这样输出的格式不会出现兼容问题。
第二步:使用notePad++查看并修改文件内容,使其符合模板要求。使用其他编辑器亦可,看个人习惯。
刚打开时内容很混乱,建议使用Pretty print(XML only - with line breaks)插件格式化,插件请自行安装,安装步骤自行百度。
格式化之后可以将无关紧要的系统自动生成的作者信息等删除,重点是修改上一步画的模板内容,将参数恢复模板样式。示例:
如果表格是动态加载的,需要在对应的动态加载起始及末尾位置加入list标签,用来循环插入数据。
模板画好后,保存,然后将文件格式改为Freemarker可识别的’.ftl’即可。
将模板放入templates文件夹,以便程序调用
至此,模板制作、准备工作完成
2、工具类
需要两个工具类,一是WordUtils(用于生成doc格式的WORD文档),二是ZipUtil(用于导出zip格式的压缩包)
WordUtils
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.Version;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* @author running
* @description
* @date 2020/9/22
*/
public class WordUtils {
private static Configuration configuration = null;
static {
configuration = new Configuration(new Version("2.3.30"));
configuration.setDefaultEncoding("utf-8");
//以下配置只能在本地使用,linux环境下无法获取地址,注掉
// try {
// ResourceLoader resourceLoader = new DefaultResourceLoader();
//指定模板目录在类路径:WEB-INF/classes
// Resource resource = resourceLoader.getResource("/");
// File file = resource.getFile();
//设置要解析的模板所在的目录,并加载模板文件
// configuration.setDirectoryForTemplateLoading(file);
///设置包装器,并将对象包装为数据模型
configuration.setObjectWrapper(new DefaultObjectWrapper(new Version("2.3.30")));
// } catch (IOException e) {
// e.printStackTrace();
// }
}
private WordUtils() {
throw new AssertionError();
}
/**
* 导出单个word
*
* @param map 数据
* @param title 文件名
* @param ftlFile 模板文件
* @param response 响应
*/
public static void exportWord(Map map, String title, String ftlFile, HttpServletResponse response) {
File file = null;
InputStream fin = null;
ServletOutputStream out = null;
try {
//Template freemarkerTemplate = configuration.getTemplate(ftlFile, "UTF-8");
ClassPathResource classPathResource = new ClassPathResource(ftlFile);
InputStream inputStream = classPathResource.getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream);
Template freemarkerTemplate = new Template(ftlFile, reader, configuration);
// 调用工具类的createDoc方法生成Word文档
String fileName = title + ".doc";
file = createDoc(map, freemarkerTemplate, fileName);
fin = new FileInputStream(file);
response.setCharacterEncoding("utf-8");
response.setContentType("application/msword");
// 设置浏览器以下载的方式处理该文件名
response.setHeader("Content-Disposition", "attachment;filename="
.concat(String.valueOf(URLEncoder.encode(fileName, "UTF-8"))));
out = response.getOutputStream();
// 缓冲区
byte[] buffer = new byte[1024];
int bytesToRead = -1;
// 通过循环将读入的Word文件的内容输出到浏览器中
while ((bytesToRead = fin.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fin != null) {
fin.close();
}
if (out != null) {
out.close();
}
// 删除临时文件
if (file != null) {
file.delete();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
/**
* 压缩包方式导出多个word
* 由于一次请求浏览器只能响应一次,想导出多个必须打包,亲测for循环导出只能导一个
* 如果想做到分别单独下载,那就得用插件啦,这里不提供插件的做法
* 思路:生成临时目录-在临时目录生成word-将临时目录打zip包-zip文件下载-删除临时目录和zip包,
* 回收系统资源
*
* @param mapList
* @param titleList
* @param ftlFile
*/
public static void exportWordBatch(List<Map<String, Object>> mapList, List<String> titleList, String ftlFile,
HttpServletResponse response, HttpServletRequest request) {
File file = null;
File zipfile = null;
File directory = null;
InputStream fin = null;
ServletOutputStream out = null;
response.setCharacterEncoding("utf-8");
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachment;filename=" + System.currentTimeMillis() + ".zip");
try {
//以此方式,在Linux环境下也可获取到模板文件
ClassPathResource classPathResource = new ClassPathResource(ftlFile);
InputStream inputStream = classPathResource.getInputStream();
InputStreamReader reader = new InputStreamReader(inputStream);
Template freemarkerTemplate = new Template(ftlFile, reader, configuration);
out = response.getOutputStream();
//根据当前时间创建临时目录
String path = request.getRealPath("/resources/word/" + System.currentTimeMillis());
directory = new File(path);
directory.mkdirs();
for (int i = 0; i < mapList.size(); i++) {
Map<String, Object> map = mapList.get(i);
String title = titleList.get(i);
// 调用工具类的createDoc方法在临时目录下生成Word文档
file = createDoc(map, freemarkerTemplate, directory.getPath() + "/" + title + ".doc");
}
//压缩目录
ZipUtil.createZip(path, path + "zip.zip");
//根据路径获取刚生成的zip包文件
zipfile = new File(path + "zip.zip");
fin = new FileInputStream(zipfile);
// 缓冲区
byte[] buffer = new byte[1024];
int bytesToRead = -1;
// 通过循环将读入的Word文件的内容输出到浏览器中
while ((bytesToRead = fin.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fin != null) {
fin.close();
}
if (out != null) {
out.close();
}
if (zipfile != null) {
zipfile.delete();
}
if (directory != null) {
//递归删除目录及目录下文件
ZipUtil.deleteFile(directory);
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
//生成word文档方法
private static File createDoc(Map<?, ?> dataMap, Template template, String filename) {
File f = new File(filename);
Template t = template;
Writer w = null;
FileOutputStream fos = null;
try {
// 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
fos = new FileOutputStream(f);
w = new OutputStreamWriter(fos, UTF_8);
//不要偷懒写成下面酱紫: 否则无法关闭fos流,打zip包时存取被拒抛异常
//w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");
t.process(dataMap, w);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
} finally {
try {
fos.close();
w.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return f;
}
}
ZipUtil
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @author running
* @description
* @date 2020/9/22
*/
public class ZipUtil {
public static void zip(String inputFileName, String zipFileName)
throws Exception {
zip(zipFileName, new File(inputFileName));
}
private static void zip(String zipFileName, File inputFile)
throws Exception {
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(
zipFileName));
zip(out, inputFile, "");
System.out.println("zip done");
out.close();
}
private static void zip(ZipOutputStream out, File f, String base)
throws Exception {
if (f.isDirectory()) {
File[] fl = f.listFiles();
out.putNextEntry(new ZipEntry(base + "/"));
base = base.length() == 0 ? "" : base + "/";
for (int i = 0; i < fl.length; i++) {
zip(out, fl[i], base + fl[i].getName());
}
} else {
out.putNextEntry(new ZipEntry(base));
FileInputStream in = new FileInputStream(f);
int b;
//System.out.println(base);
while ((b = in.read()) != -1) {
out.write(b);
}
in.close();
}
}
/**
* 创建ZIP文件
*
* @param sourcePath 文件或文件夹路径
* @param zipPath 生成的zip文件存在路径(包括文件名)
*/
public static void createZip(String sourcePath, String zipPath) {
FileOutputStream fos = null;
ZipOutputStream zos = null;
try {
fos = new FileOutputStream(zipPath);
zos = new ZipOutputStream(fos);
writeZip(new File(sourcePath), "", zos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (zos != null) {
zos.close();
//压缩成功后,删除打包前的文件
deleteFile(new File(sourcePath));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void writeZip(File file, String parentPath,
ZipOutputStream zos) {
if (file.exists()) {
// 处理文件夹
if (file.isDirectory()) {
parentPath += file.getName() + File.separator;
File[] files = file.listFiles();
for (File f : files) {
writeZip(f, parentPath, zos);
}
} else {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
ZipEntry ze = new ZipEntry(parentPath + file.getName());
zos.putNextEntry(ze);
byte[] content = new byte[1024];
int len;
while ((len = fis.read(content)) != -1) {
zos.write(content, 0, len);
zos.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public static void copyResource(List<String> oldResPath, String newResPath) {
for (int m = 0; m < oldResPath.size(); m++) {
try {
// 如果文件夹不存在 则建立新文件夹
(new File(newResPath)).mkdirs();
File a = new File(oldResPath.get(m));
// 如果已经是具体文件,读取
if (a.isFile()) {
FileInputStream input = new FileInputStream(a);
FileOutputStream output = new FileOutputStream(newResPath + "/" + (a.getName()).toString());
byte[] b = new byte[1024 * 4];
int len;
while ((len = input.read(b)) != -1) {
output.write(b, 0, len);
}
output.flush();
output.close();
input.close();
// 如果文件夹下还存在文件,遍历,直到得到具体的文件
} else {
String[] file = a.list();
File temp = null;
for (int i = 0; i < file.length; i++) {
if (oldResPath.get(m).endsWith(File.separator)) {
temp = new File(oldResPath.get(m) + file[i]);
} else {
temp = new File(oldResPath.get(m) + File.separator + file[i]);
}
if (temp.isFile()) {
FileInputStream input = new FileInputStream(temp);
FileOutputStream output = new FileOutputStream(newResPath + "/" + (temp.getName()).toString());
byte[] b = new byte[1024 * 4];
int len;
while ((len = input.read(b)) != -1) {
output.write(b, 0, len);
}
output.flush();
output.close();
input.close();
}
if (temp.isDirectory()) {
List<String> oldChildPath = new ArrayList<String>();
oldChildPath.add(oldResPath.get(m) + "/" + file[i]);
newResPath = newResPath + "/" + file[i];
// 如果是子文件夹 递归循环
copyResource(oldChildPath, newResPath);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 删除文件夹
*
* @param file
*/
public static void deleteFile(File file) {
// 判断文件是否存在
if (file.exists()) {
// 判断是否是文件
if (file.isFile()) {
file.delete();
} else if (file.isDirectory()) {
// 否则如果它是一个目录
// 声明目录下所有的文件 files[];
File files[] = file.listFiles();
// 遍历目录下所有的文件
for (int i = 0; i < files.length; i++) {
// 把每个文件 用这个方法进行迭代
deleteFile(files[i]);
}
}
file.delete();
}
}
/**
* 时间格式化
*
* @return
*/
public static String dateToString() {
Date d = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String time = formatter.format(d);
return time;
}
}
3、准备测试数据
@PostMapping("downZip")
public void downZip(HttpServletResponse response, HttpServletRequest request){
//数据集
List<Map<String,Object>> list = new ArrayList<>();
Map<String,Object> map = new HashMap<>();
//环节数据
List<Map<String,String>> linkList = new ArrayList<>();
Map<String,String> linkMapOne = new HashMap<>();
Map<String,String> linkMapTwo = new HashMap<>();
//记录数据
List<Map<String,String>> records = new ArrayList<>();
Map<String,String> recordOne = new HashMap<>();
Map<String,String> recordTwo = new HashMap<>();
//标题
map.put("testName","测试数据啊");
map.put("startDay","2020年10月21日");
map.put("endDay","2020年10月28日");
//内容描述
map.put("testDescription","这是一个测试数据");
//环节情况
linkMapOne.put("linkName","环节1");
linkMapTwo.put("linkName","环节2");
linkMapOne.put("operator","张三");
linkMapTwo.put("operator","李四");
linkMapOne.put("status","未审批");
linkMapTwo.put("status","已审批");
linkList.add(linkMapOne);
linkList.add(linkMapTwo);
map.put("linkList",linkList);
//记录
recordOne.put("history","历史性进展");
recordTwo.put("history","史诗级发展");
recordOne.put("stepNo","第一步");
recordTwo.put("stepNo","第二步");
records.add(recordOne);
records.add(recordTwo);
map.put("demoList",records);
list.add(map);
//文件名
List<String> fileList = new ArrayList<>();
String fileName = "测试";
fileList.add(fileName);
//设置模板
String ftlName = "/templates/测试.ftl";
WordUtils.exportWordBatch(list,fileList,ftlName,response,request);
}
4、效果图
四、前端调用过程
export const exportTestPort = params => {
return service({
method: 'POST',
url: '/test/downZip',
data: params,
headers: {
// 定义请求头类型为流的类型
'Content-Type': 'application/json;application/octet-stream'
},
responseType: 'blob' // 定义文件返回的数据的格式
})
}
exportTestPort (params).then(res => {
if (!res) {
return
}
// 创建文件临时存储地址
const url = window.URL.createObjectURL(
new Blob([res], { type: 'application/zip' })
)
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
try {
window.navigator.msSaveOrOpenBlob(res, '测试.zip')
} catch (e) {
this.$message.error('下载测试附件失败')
}
} else {
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.download = '测试.zip'
document.body.appendChild(link)
link.click()
URL.revokeObjectURL(link.href)
document.body.removeChild(link)
}
})
五、结语
个人感觉,导出文件最需要注意的是模板的制作,往往一个小的偏差就可能导致模板导出失败,不过好处是IDEA会有报错的日志,通过日志很容易修改。至于工具类,都是现成的,直接用即可。
今天的记录就到这里。good luck!
————————————————
版权声明:本文为CSDN博主「奔跑-ing」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44396925/article/details/109197223