需求

需要将一个报表的数据导出成一个word,报表中有固定项,还有需要动态生成n个的表单。简单点举个例子差不多就像是下面这样。

使用Freemarker填充模板导出复杂Excel freemarker导出word_java


最开始想用easy-poi导出至word模板来实现,发现他只能实现固定的项目,像上图中,有N个周期的成绩就要显示n个成绩块的这个就无法实现。

百度了一圈……找啊找不到实现的方式,最后突然想到以前做的导出word是用freemarker实现的,于是稍微研究了一下,就搞定了。

具体实现

一、创建一个word文件

新建一个word文件,然后把想要的样式什么的都调整好,字体、文字大小、行间距等等。

使用Freemarker填充模板导出复杂Excel freemarker导出word_java

二、将word文件另存为成.xml文件

F12 -> 选择.xml文件 -> 保存

使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_03


使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_04

三、直接将.xml文件的后缀改成.ftl

这应该会吧……

四、把.ftl文件放到java项目中去

使用Freemarker填充模板导出复杂Excel freemarker导出word_List_05


可以格式化一下,idea支持ftl格式的格式化,格式化之后看起来好看多了

五、编辑.ftl文件

1.咱们先大概的分析一下整个文件的构成

使用Freemarker填充模板导出复杂Excel freemarker导出word_List_06


进入body看一下

使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_07


<w:p>这个标签可以看做是html的p标签,一个自然段的内容会在一个<w:p>标签里面。

使用Freemarker填充模板导出复杂Excel freemarker导出word_word_08


使用Freemarker填充模板导出复杂Excel freemarker导出word_java_09


<w:tbl>这个就相当于html的table标签,

<w:tr>相当于html的tr标签

<w:tc>这个应该是单元格标签,代表了一个单元格。

<w:p>标签中的实际显示的内容是由<w:r>标签中的<w:t>标签的内容决定的

<w:rpr>这个标签里面的东西是用来改当前这个p标签里面内容的样式的。

2.将占位的值改成freemarker语法的参数

假设咱们的数据格式是这样的:

public class Entity {
        private String userName;
        private List<Grade> grades;

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public List<Grade> getGrades() {
            return grades;
        }

        public void setGrades(List<Grade> grades) {
            this.grades = grades;
        }

        public Entity(String userName, List<Grade> grades) {
            this.userName = userName;
            this.grades = grades;
        }

        public Entity() {
        }

    }

    public class Grade {
        private String period;
        private Double chinese;
        private Double math;
        private Double english;

        public String getPeriod() {
            return period;
        }

        public void setPeriod(String period) {
            this.period = period;
        }

        public Double getChinese() {
            return chinese;
        }

        public void setChinese(Double chinese) {
            this.chinese = chinese;
        }

        public Double getMath() {
            return math;
        }

        public void setMath(Double math) {
            this.math = math;
        }

        public Double getEnglish() {
            return english;
        }

        public void setEnglish(Double english) {
            this.english = english;
        }

        public Grade(String period, Double chinese, Double math, Double english) {
            this.period = period;
            this.chinese = chinese;
            this.math = math;
            this.english = english;
        }
    }

注意:传入模板中的数据,一定要是一个Map,如果有数组,数组也需要转成一个List才行。

freemarker取值语法:${变量名}

使用Freemarker填充模板导出复杂Excel freemarker导出word_java_10


成绩单下面的具体的成绩的表格以及周期,是一个集合遍历出来的,咱们也要在ftl文件中将遍历的逻辑写出来

freemarker遍历标签,在需要遍历的区域的最外层,套一个<#list>标签

<#list 数组 as 每个对象别名>

有点像for循环给循环的对象起一个别名

for(Grade grade in grades)

使用Freemarker填充模板导出复杂Excel freemarker导出word_word_11


需要取遍历对象的某个属性的值,就要这么写

${别名.属性名}

周期值:

${grade.period}

下面语文、数学、英语上都去替换一下

使用Freemarker填充模板导出复杂Excel freemarker导出word_List_12


使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_13


使用Freemarker填充模板导出复杂Excel freemarker导出word_List_14


至此已经可以满足基本的要求了。

3.编写接口和写入word逻辑

controller

@GetMapping("test")
    public void test(HttpServletResponse response, HttpServletRequest request) throws IllegalAccessException, IOException {
        Entity entity = new Entity();
        entity.setUserName("张三");
        List<Grade> grades = new ArrayList<>();
        grades.add(new Grade("202301", 100D, 100D, 100D));
        grades.add(new Grade("202302", 100D, 100D, 100D));
        grades.add(new Grade("202303", 100D, 100D, 100D));
        entity.setGrades(grades);

        String wordName = "test.ftl";
        String fileName = "result.docx";
        String name = "result";

        Map<String, Object> map = ObjectToMap(entity);
        WordUtil.exportMillCertificateWord(request, response, map, wordName, fileName, name);

    }

WordUtil

package com.ruoyi.system;

import freemarker.template.Configuration;
import freemarker.template.Template;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Map;

/**
 * @author Frank
 * @date 2023/9/11
 */
public class WordUtil {
    //配置信息,代码本身写的还是很可读的,就不过多注解了
    private static Configuration configuration = null;
    // 这里注意的是利用WordUtils的类加载器动态获得模板文件的位置

    //private static final String templateFolder = wordUtils.class.getClassLoader().getResource("../../../../templates").getPath();
    private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();

    static {
        configuration = new Configuration();
        configuration.setDefaultEncoding("utf-8");
        try {
            System.out.println(templateFolder);
            configuration.setDirectoryForTemplateLoading(new File(templateFolder));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private WordUtil() {
        throw new AssertionError();
    }

    /**
     * 导出excel
     *
     * @param request  请求对象
     * @param response 响应对象
     * @param map      word文档中参数
     * @param wordName 为模板的名字  例如xxx.ftl
     * @param fileName 是word 文件的名字 格式为:"xxxx.doc"
     * @param name     是临时的文件夹米名称 string类型 可随意定义
     * @throws IOException
     */
    public static void exportMillCertificateWord(HttpServletRequest request, HttpServletResponse response, Map map, String wordName, String fileName, String name) throws IOException {
        Template freemarkerTemplate = configuration.getTemplate(wordName);
        File file = null;
        InputStream fin = null;
        ServletOutputStream out = null;
        try {
            // 调用工具类的createDoc方法生成Word文档
            file = createDoc(map, freemarkerTemplate, name);
            fin = new FileInputStream(file);
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/x-download");
            fileName = new String(fileName.getBytes(), "ISO-8859-1");
            response.setHeader("Content-Disposition", "attachment;filename=".concat(fileName));
            out = response.getOutputStream();
            byte[] buffer = new byte[512];// 缓冲区
            int bytesToRead = -1;
            // 通过循环将读入的Word文件的内容输出到浏览器中
            while ((bytesToRead = fin.read(buffer)) != -1) {
                out.write(buffer, 0, bytesToRead);
            }
        } finally {
            if (fin != null) fin.close();
            if (out != null) out.close();
            if (file != null) file.delete();// 删除临时文件
        }
    }

    private static File createDoc(Map<?, ?> dataMap, Template template, String name) {
        File f = new File(name);
        Template t = template;
        try {
            // 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
            Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");
            t.process(dataMap, w);
            w.close();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
        return f;
    }
}

运行结果

使用Freemarker填充模板导出复杂Excel freemarker导出word_java_15

4.拓展一些需求

1.对数据进行判空

如果传入的数据是个null,就会报错

使用Freemarker填充模板导出复杂Excel freemarker导出word_xml_16


还很好心的给了tip

If the failing expression is known to legally refer to something that’s sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??

翻译一下:

如果已知失败表达式合法地引用了某些有时为空或缺失的东西,则指定一个默认值,如myOptionalVar!当存在时使用<#if myOptionalVar??>,当缺少</#if>时使用<#else>。(这些只覆盖表达式的最后一步;要覆盖整个表达式,请使用括号:(myOptionalVar.foo)!myDefault (myOptionalVar.foo) ? ?

那咱就按照他的建议去改一下

先用(myOptionalVar.foo)!myDefault
【属性名】!【默认值】
原理是在用到值的地方去判空,如果为空的话,就取后面的默认值。
假设咱们这边是对grade.math进行判空,那就这么写
${grade.math!‘暂无成绩’}
查了一下官网文档,还有以下几种写法。
${grade.math?default(‘暂无成绩’)}
${grade.math???string(grade.math,‘暂无成绩’)}

咱们就用它提示的来写

使用Freemarker填充模板导出复杂Excel freemarker导出word_word_17


导出一下看看

使用Freemarker填充模板导出复杂Excel freemarker导出word_word_18


还真可以哎……再用if判断写一个

使用Freemarker填充模板导出复杂Excel freemarker导出word_List_19


使用Freemarker填充模板导出复杂Excel freemarker导出word_word_18


嗯……也没问题

2.对低于60分的成绩进行标红,高于90分的标绿

实现原理:在<w:rPr>这个标签中的<w:color>给他赋值,用if判断决定赋什么值

使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_21


使用Freemarker填充模板导出复杂Excel freemarker导出word_springboot_22


搞定!