Java FreeMarker导出word

一、前言

在写项目的时候遇到一个导出报表的需求,由此来记录分享一波(.)。

本文主要使用FreeMarker模板导出如下的word:
在这里插入图片描述

二、什么是 FreeMarker?

FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。

详情可查看官网介绍:中文:http://freemarker.foofun.cn/ ,英文:https://freemarker.apache.org/

三、准备工作

1、整理文档,如下所示:

在这里插入图片描述

因为表格内的数据为集合,所以我们只需要留一条就行,然后再循环就OK了。

如果那个文字介绍也是根据后台查询的话,最好就给几个字(不然的话再生成xml的时候这些文字会分开)。如下:
在这里插入图片描述

2、生成XML文档

直接将上面的文档另存为xml格式的文件就行,然后打开这个xml文件,将内容格式化一下,我比较喜欢用这个网站:https://www.sojson.com/xml.html。 将内容copy到这个网站进行格式化,然后再copy到xml文件中就好了。
在这里插入图片描述

3、使用参数替换xml内容

(1)、文字替换,格式:${param!},其中“!”表示可以为空
在这里插入图片描述

(2)、集合替换,格式:<#list userList as user></#list>

找到数据行所在的<w:tr></w:tr>标签,将其放在#list标签内。注:不是标题行
在这里插入图片描述

(3)、if条件格式:
在这里插入图片描述

4、xml内容替换参数后,将其后缀改为flt,并放到resouces目录下

在这里插入图片描述

四、导出方法

@PostMapping("/export_word")
public void exportWord(HttpServletResponse response) {
    Map<String, Object> dataMap = Maps.newConcurrentMap();
    dataMap.put("title", "这是标题这是标题");
    // 一、用户数据
    List<SysUser> userList = Lists.newArrayList();
    userList.add(new SysUser("张三", "15211111111", "0", 25, "[email protected]"));
    userList.add(new SysUser("李四", "15222222222", "1", 20, "[email protected]"));
    userList.add(new SysUser("王五", "15233333333", "2", 23, "[email protected]"));
    dataMap.put("userList", userList);
    // 二、岗位数据
    dataMap.put("postInfo", "这是岗位介绍这是岗位介绍这是岗位介绍这是岗位介绍这是岗位介绍这是岗位介绍这是岗位介绍这是岗位介绍");
    List<SysPost> postList = Lists.newArrayList();
    postList.add(new SysPost(1L, "ceo", "董事长", "0"));
    postList.add(new SysPost(2L, "manager", "经理", "0"));
    postList.add(new SysPost(3L, "user", "普通员工", "1"));
    dataMap.put("postList", postList);

    String ftl = "ftl/ExportWord.ftl";
    WordUtils.getInstance().exportDocFile(ftl, "Word导出测试", dataMap, response);
}

五、导出工具类

package com.rain.common.utils.poi;

import freemarker.template.Configuration;
import freemarker.template.Template;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import sun.misc.BASE64Encoder;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

/**
 * word导出工具类
 * @Author RainCity
 * @Date 2024-01-22 11:00:00
 */
public class WordUtils {
    private static final Logger log = LoggerFactory.getLogger(WordUtils.class);
    private static WordUtils service = null;

    public WordUtils() {
        super();
    }

    public static WordUtils getInstance() {
        if(service == null) {
            service = new WordUtils();
        }
        return service;
    }

    /**
     * 导出至word
     * @param ftl ftl模板地址
     * @param fileName 文件名
     * @param dataMap 导出数据
     * @param response response
     */
    public void exportDocFile(String ftl, String fileName, Map<String, Object> dataMap, HttpServletResponse response){
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
        configuration.setDefaultEncoding("UTF-8");

        OutputStreamWriter oWriter = null;
        Writer out = null;
        try {
            ClassPathResource resource = new ClassPathResource(ftl);
            File file = File.createTempFile("StatementTemp",".ftl");
            FileUtils.copyURLToFile(resource.getURL(), file);
            String templateFile = file.getAbsolutePath();
            templateFile = pathReplace(templateFile);
            if(null == templateFile){
                log.error("导出失败,模板不存在");
                return;
            }
            String ftlPath = templateFile.substring(0, templateFile.lastIndexOf("/"));
            log.info("模板路径:{}", ftlPath);
            // FTL文件所存在的位置--绝对路径
            configuration.setDirectoryForTemplateLoading(new File(ftlPath));
            //configuration.setClassForTemplateLoading(this.getClass(), ftlPath);
            String ftlFile = templateFile.substring(templateFile.lastIndexOf("/")+1);
            log.info("模板临时文件名称:{}", ftlFile);
            // 模板文件名
            Template template = configuration.getTemplate(ftlFile);

            response.reset();
            response.setContentType("application/octet-stream; charset=utf-8");
            String filename = fileName + ".doc";
            response.setHeader("Content-Disposition", "attachment; filename="+ urlEncode(filename));
            oWriter = new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8);
            out = new BufferedWriter(oWriter);
            template.process(dataMap, out);
        } catch (Exception e) {
            log.error("导出word失败:", e);
        } finally {
            try {
                if(null != out){
                    out.close();
                }
                if(null != oWriter){
                    oWriter.close();
                }
            } catch (IOException e) {
                log.error("Writer or OutputStreamWriter close failed:{}", e.getMessage());
            }
        }
    }

    /**
     * 创建doc文件
     * @param ftl src/main/resources/ftl/xxx.ftl
     * @param dataMap 导出数据
     * @param exportPath  eg: /tmp/test/test123.doc
     * @param loadType  设置路径加载方式。1-绝对路径,2-项目相对路径
     * @return {@link File}
     * @ver v1.0.0
     */
    public File createDocFile(String ftl, Map<String, Object> dataMap, String exportPath, int loadType) {
        File file = new File(ftl);
        String templateFile = file.getAbsolutePath();
        Template t = null;
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
        configuration.setDefaultEncoding("UTF-8");
        try {
            templateFile = pathReplace(templateFile);
            if(null == templateFile){
                log.error("模板不存在!");
                return null;
            }
            String ftlPath = templateFile.substring(0, templateFile.lastIndexOf("/"));
            if(loadType == 1) {
                // FTL文件所存在的位置
                configuration.setDirectoryForTemplateLoading(new File(ftlPath));
            }else {
                //以类加载的方式查找模版文件路径
                configuration.setClassForTemplateLoading(this.getClass(), ftlPath);
            }

            String ftlFile = templateFile.substring(templateFile.lastIndexOf("/")+1);
            // 模板文件名
            t = configuration.getTemplate(ftlFile);

            File outFile = new File(exportPath);
            Writer out =  new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(outFile.toPath())));
            t.process(dataMap, out);
        } catch (Exception e) {
            log.error("导出word文档出错:{}", e.getMessage());
        }
        return null;
    }

    /**
     *  把路径的替换成/
     * @param path 路径
     * @return {@link String}
     * @ver v1.0.0
     */
    private static String pathReplace(String path) {
        while(path != null && path.contains("")) {
            path = path.replace("", "/");
        }
        return path;
    }

    /**
     * 图片转base64
     * @param imgStr 图片 http 地址
     * @return {@link String}
     * @ver v1.0.0
     */
    public static String getImgStrToBase64(String imgStr) {
        if(StringUtils.isEmpty(imgStr)){
            return "";
        }
        log.info("imgStr:===>{}",imgStr);
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        byte[] buffer = null;
        try {
            //判断网络链接图片文件/本地目录图片文件
            if (imgStr.startsWith("http://") || imgStr.startsWith("https://")) {
                // 创建URL
                URL url = new URL(imgStr);
                // 创建链接
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                inputStream = conn.getInputStream();
                outputStream = new ByteArrayOutputStream();
                // 将内容读取内存中
                buffer = new byte[1024];
                int len = -1;
                while ((len = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, len);
                }
                buffer = outputStream.toByteArray();
            } else {
                inputStream = Files.newInputStream(Paths.get(imgStr));
                int count = 0;
                while (count == 0) {
                    count = inputStream.available();
                }
                buffer = new byte[count];
                inputStream.read(buffer);
            }
            // 对字节数组Base64编码
            return new BASE64Encoder().encode(buffer);
        }catch (Exception e) {
            log.error("图片转换失败:{}", e.getMessage());
            return "";
        } finally {
            if (inputStream != null) {
                try {
                    // 关闭inputStream流
                    inputStream.close();
                } catch (IOException e) {
                    log.error("InputStream 关闭失败:{}", e.getMessage());
                }
            }
            if (outputStream != null) {
                try {
                    // 关闭outputStream流
                    outputStream.close();
                } catch (IOException e) {
                    log.error("ByteArrayOutputStream 关闭失败:{}", e.getMessage());
                }
            }
        }
    }


    /**
     * URL 编码, Encode默认为UTF-8.
     */
    public static String urlEncode(String part) {
        try {
            return URLEncoder.encode(part, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error("编码失败:{}", e.getMessage());
            return "";
        }
    }
}

六、工具类中需要用到的freemarker依赖

<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

好啦,到此就可以完成导出word啦(.)(.)(.)