前言

项目有需求,通过特定模板导出 .doc 格式的 Word 文档。因为之前有过使用 apache-poi 导出 excel 的经验,所以在一开始选择了 apache-poi 作为实现依赖,但后来发现生成 word 调用及实现过于复杂,遂放弃。查阅资料后,试着使用 easy-poi 实现,但又发现可定制程度太小,无法动态生成表格和图片,遂放弃。

就在山穷水尽之时,我想到了 freemarker。众所周知,Word 的本质就是一个 xml 文件,所以我们使用 freemarker 来动态生成 Word,应该是可行的。

实现过程

总共分为两部分,一部分是文档模板的准备,一部分是代码的准备。

模板文档准备过程

1. 数据位置替换

(注意:如果要代码插入图片,那么这里一定要插入图片)

java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_Word


模板替换后:

其中表格长度会随着list内容动态生成。

java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_FreeMarker_02

2. 另存为 xml 文件

将刚刚改完的模板另存为 xml 文件。

3. 编辑 xml 文件

  • 编辑分开的替换符:
    打开 xml 文件,将其中的代码格式化后,我们找出刚才替换符的位置,会发现可能有替换符和值分开的情况:
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_FreeMarker_03

  • 这样的替换符会失效,我们将中间的内容删除,让他们在一起:
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_Word_04


  • 选定动态生成范围,添加 list 标签
    在这里,一般 <w:tr> 标签包裹的是一行,<w:tc> 标签包裹的是一个单元格,所以找到表格下 <w:tr> 标签,在外面包裹 <#list> 标签:
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_Word_05

  • 其中,specialityList 是代码里 List 的变量名称,speciality 是模板里的变量名称。
  • 图片替换(头疼)
    如果需要动态生成图片,就相对麻烦一些。如果没有生成图片需求,可以跳过此部分。
    以目前的经验看,有两种不同的 xml 模板,一种是直接在正文内容中插入了 base64 的编码,另一种是通过 id 链接的方式插入图片。
  • 如果是第一种方式,替换会简单些,直接将 base64 的编码换成占位变量即可。
  • 第二种就比较恶心,需要更改三个地方。
  • 首先,在文章末尾找到 base64标签,并添加同级标签和包裹标签,并在中间添加替换符、< pkg:name >值变为自增值。
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_FreeMarker_06

  • 其次,找到rId和图片的桥梁元素< Relationship >(一般在文件头部),将相应元素替换为对应自增值。
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_FreeMarker_07

  • 最后,找到文章位置插入标签 < v:imagedata >(可能是别的,建议根据rid全文查找到),改为自增值和包裹标签。
  • java FreeMark在word中 生成动态行列表格 freemarker word 动态表格_FreeMarker_08

至此,模板准备工作终于结束了。

代码与项目结构
项目结构
  • 建议在resource中路径下建立项目建构

代码这块,比准备模板要简单多啦。

  • 首先是生成工具类,注意图片要转 base64 哦
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;

import java.io.*;
import java.util.Map;

/**
 * @author amber
 */
public class WordDocxUtil {

    /**
     * 将图片内容转换成Base64编码的字符串
     *
     * @param imageFile 图片文件的全路径名称
     * @return 转换成Base64编码的图片内容字符串
     */
    public static String getImageBase64String(String imageFile) {
        if (StringUtils.isEmpty(imageFile)) {
            return "";
        }
        File file = new File(imageFile);
        if (!file.exists()) {
            return "";
        }
        InputStream is = null;
        byte[] data = null;
        try {
            is = new FileInputStream(file);
            data = new byte[is.available()];
            is.read(data);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(data);
    }

    public static String getImageBase64String(InputStream inputStream){
        byte[] data = null;
        try {
            data = new byte[inputStream.available()];
            inputStream.read(data);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(data);
    }
    /**
     * 使用FreeMarker自动生成Word文档
     *
     * @param dataMap  生成Word文档所需要的数据
     * @param fileName 生成Word文档的全路径名称
     */
    public static void generateWord(Map<String, Object> dataMap, String fileName, String templatePath, String template) throws Exception {
        // 设置FreeMarker的版本和编码格式
        Configuration configuration = new Configuration(new Version("2.3.28"));
        configuration.setDefaultEncoding("UTF-8");

        // 设置FreeMarker生成Word文档所需要的模板的路径
        configuration.setDirectoryForTemplateLoading(new File(templatePath));
        // 设置FreeMarker生成Word文档所需要的模板
        Template t = configuration.getTemplate(template, "UTF-8");
        // 创建一个Word文档的输出流
        Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(fileName)), "UTF-8"));
        //FreeMarker使用Word模板和数据生成Word文档
        t.process(dataMap, out);
        out.flush();
        out.close();
    }

}

然后是如何应用

Map<String, Object> params = new HashMap<>();
params.put("details", setDocDetailsData(detailsList, workMode));
WordDocxUtil.generateWord(params, "/temp/" + fileName, "src/main/resources/word/", "TemplateV1.xml");

大功告成!