基于Freemarker模板引擎生成多个Word文件(doc格式),并压缩成Zip格式导出

基于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!

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值