用freemarker导出复杂word模板

需求

需要将一个报表的数据导出成一个word,报表中有固定项,还有需要动态生成n个的表单。简单点举个例子差不多就像是下面这样。
在这里插入图片描述
最开始想用easy-poi导出至word模板来实现,发现他只能实现固定的项目,像上图中,有N个周期的成绩就要显示n个成绩块的这个就无法实现。
百度了一圈……找啊找不到实现的方式,最后突然想到以前做的导出word是用freemarker实现的,于是稍微研究了一下,就搞定了。

具体实现

一、创建一个word文件

新建一个word文件,然后把想要的样式什么的都调整好,字体、文字大小、行间距等等。
在这里插入图片描述

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

F12 -> 选择.xml文件 -> 保存
在这里插入图片描述
在这里插入图片描述

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

这应该会吧……

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

在这里插入图片描述
可以格式化一下,idea支持ftl格式的格式化,格式化之后看起来好看多了

五、编辑.ftl文件

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

在这里插入图片描述
进入body看一下
在这里插入图片描述
<w:p>这个标签可以看做是html的p标签,一个自然段的内容会在一个<w:p>标签里面。
在这里插入图片描述
在这里插入图片描述
<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取值语法:${变量名}
在这里插入图片描述
成绩单下面的具体的成绩的表格以及周期,是一个集合遍历出来的,咱们也要在ftl文件中将遍历的逻辑写出来
freemarker遍历标签,在需要遍历的区域的最外层,套一个<#list>标签
<#list 数组 as 每个对象别名>
有点像for循环给循环的对象起一个别名
for(Grade grade in grades)
在这里插入图片描述
需要取遍历对象的某个属性的值,就要这么写
${别名.属性名}
周期值:
${grade.period}
下面语文、数学、英语上都去替换一下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
至此已经可以满足基本的要求了。

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;
    }
}

运行结果
在这里插入图片描述

4.拓展一些需求

1.对数据进行判空

如果传入的数据是个null,就会报错
在这里插入图片描述
还很好心的给了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,‘暂无成绩’)}

咱们就用它提示的来写
在这里插入图片描述
导出一下看看
在这里插入图片描述
还真可以哎……

再用if判断写一个
在这里插入图片描述
在这里插入图片描述
嗯……也没问题

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

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

在这里插入图片描述
在这里插入图片描述
搞定!

在Web应用中,有时需要按照固定的模板将数据导出Word,如流程审批单,在流程处理完成后将处理过程按照流程单的要求导出,有时程序中需要实现生成标准Word文档,要求能够打印,并且保持页面样式不变,常见的方案有POI、iText、JACOB、JSP几种方式,POI读取Word文档比较适合、对于生成文档样式比较难控制,iText操作Excel还可以,对Word的操作功能有限,JACOB操作Word实现复杂,并且无法将服务部署到Linux平台,要求安装office,对于实现固定格式的报表实现困难,对于JSP直接输出方式样式控制难。 Word从2003开始支持XML格式,用XML+Freemarder还做就很简单了,大致的思路是先用office2003或者2007编辑好 word的样式,然后另存为xml,将xml翻译为FreeMarker模板,最后用java来解析FreeMarker模板并输出Doc。经测试这样方式生成的word文档完全符合office标准,样式、内容控制非常便利,打印也不会变形,生成的文档和office中编辑文档完全一样。具体实现过程如下: 1、 首先用office【版本要2003以上,以下的不支持xml格式】编辑文档的样式,将需要动态填充的内容使用Freemarker标签替换:Word文档样式如下: 2、 将Word文档另存为XML格式,将后缀名“xml”修改为“ftl” 3、 使用Freemarker填充内容,代码如下: [java] view plaincopyprint? 1. package com.test.freemarker.report; 2. 3. 4. 5. import java.io.BufferedWriter; 6. 7. import java.io.File; 8. 9. import java.io.FileOutputStream; 10. 11. import java.io.IOException; 12. 13. import java.io.OutputStreamWriter; 14. 15. import java.io.Writer; 16. 17. import java.util.HashMap; 18. 19. import java.util.Map; 20. 21. 22. 23. import freemarker.template.Configuration; 24. 25. import freemarker.template.Template; 26. 27. import freemarker.template.TemplateException; 28. 29. 30. 31. public class DocumentHandler { 32. 33. private Configuration configuration = null; 34. 35. 36. 37. public DocumentHandler() { 38. 39. configuration = new Configuration(); 40. 41. configuration.setDefaultEncoding("utf-8"); 42. 43. } 44. 45. 46. 47. public void createDoc() { 48. 49. // 要填入模本的数据文件 50. 51. Map dataMap = new HashMap(); 52. 53. getData(dataMap); 54. 55. // 设置模本装置方法和路径,FreeMarker支持多种模板装载方法。可以重servlet,classpath,数据库装载, 56. 57. // 这里我们的模板是放在com.havenliu.document.template包下面 58. 59.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值