• Springboot结合Freemaker导出模板doc和docx文件


    一、背景

    日常搬砖中,来了一个需求需要导出一个word报告,文件类型有doc类型,有docx类型。并且要严格按照需求的文档模板来导出,字体样式行间距等等都需要和模板一样,如此就想到了模板结合Freemaker语法导出我们需要的文档报告。

    二、结果

    2.1 先看一下word模板,这里给出的是调整后的word模板,模板包含了:单条赋值循环赋值表格赋值,基本满足所有的文档需求了,如果有其他的赋值方式,按照上面三条改造一下即可。
    在这里插入图片描述
    2.2 对模板进行改造,采用Freemaker语法进行变量字段的声明。上下图对比可见,日期和标题一是单条赋值,标题二是循环赋值,标题三是表格循环赋值。
    在这里插入图片描述
    2.3 以上操作均是使用 word工具打开直接手工编辑生成。

    三、实现

    3.1 项目版本依赖

    本项目是Springboot + Freemaker 实现,以下给出本项目的Springboot版本和Freemaker依赖版本。学习的同学可以结合最新版本使用。这里给出 阿里maven依赖仓库,大家可以去查最新的依赖版本。

    // Springboot版本
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.4.2</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    
    // Freemaker版本
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.30</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3.2 导出doc模板

    3.2.1 打开修改后的模板文档,另存为 Word XML 文档 类型文件,这里先将文件名修改成test_template,当然在项目里修改也行,如下图:
    在这里插入图片描述
    3.2.2 在Springboot项目中 resources 下新建 templates 文件夹,将上述的 test_template.xml文件放入templates 文件夹中并修改文件后缀名为 ftl(freemarker的文件名是以.ftl后缀的),如下图:
    在这里插入图片描述
    3.2.3 这里有点需要注意,就是模板文件 ${text} 域 可能出现格式错误,这样在生成模板文件的时候就会报错,所以我们事先打开文件查看一下,发现如下错误,将花括号里的内容删除,然后填充上变量字段。如下图:
    修改前
    在这里插入图片描述
    修改后
    在这里插入图片描述
    3.2.4 Freemaker语法这里贴几个,有别的需要百度查一下Freemaker语法即可。在IDEA里编辑ftl文件时候,千万别格式化,不然导出的样式会让你有意想不到的结果。最后由于完整样例导致页面太卡,无奈只能部分关键样例,为了大家看着方便我给样例格式化了一下,但是大家在做的时候千万不要格式化啊。

    // 直接赋值
    ${compareLayer}
     
    // 集合遍历
    <#list filterResults as filterResult>    
      ${filterResult}  
    </#list>
     
    // 判断集合是否为空
    <#if (filterResults?? && filterResults?size > 0) >
    </#if>
    
    // ftl部分样例
    <w:r>
        <w:rPr>
            <w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
                      w:eastAsia="微软雅黑" w:cs="Times New Roman"/>
            <w:sz w:val="20"/>
            <w:szCs w:val="20"/>
            <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
        </w:rPr>
        <w:t>${reportDate}</w:t>
    </w:r>
    <w:r>
    <w:rPr>
        <w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
                  w:cs="Times New Roman"/>
        <w:b/>
        <w:bCs/>
        <w:sz w:val="24"/>
        <w:szCs w:val="24"/>
        <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
    </w:rPr>
    <w:t>标题二</w:t>
    </w:r>
    <w:r>
    <w:rPr>
        <w:rFonts w:hint="default" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
                  w:cs="Times New Roman"/>
        <w:b/>
        <w:bCs/>
        <w:sz w:val="24"/>
        <w:szCs w:val="24"/>
        <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
    </w:rPr>
    <w:t>]</w:t>
    </w:r>
    </w:p><#list text2s as text>
    <w:p>
    <w:pPr>
    <w:keepNext w:val="0"/>
    <w:keepLines w:val="0"/>
    <w:pageBreakBefore w:val="0"/>
    <w:widowControl w:val="0"/>
    <w:numPr>
        <w:ilvl w:val="0"/>
        <w:numId w:val="0"/>
    </w:numPr>
    <w:kinsoku/>
    <w:wordWrap/>
    <w:overflowPunct/>
    <w:topLinePunct w:val="0"/>
    <w:autoSpaceDE/>
    <w:autoSpaceDN/>
    <w:bidi w:val="0"/>
    <w:adjustRightInd/>
    <w:snapToGrid/>
    <w:spacing w:line="360" w:lineRule="auto"/>
    <w:jc w:val="both"/>
    <w:rPr>
        <w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
                  w:cs="Times New Roman"/>
        <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
    </w:rPr>
    </w:pPr>
    <w:r>
    <w:rPr>
        <w:rFonts w:hint="eastAsia" w:ascii="Times New Roman" w:hAnsi="Times New Roman"
                  w:cs="Times New Roman"/>
        <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
    </w:rPr>
    <w:t>${text}</w:t>
    </w:r></w:p></#list>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    3.2.5 还有需要注意的,maven在编译打包的时候,可能不会将 ftl模板文件 打包到 target 中,这里就需要在pom文件中添加配置,如下图:
    在这里插入图片描述

    3.2.4 生成doc模板工具类,这里工具方法有,直接以流的形式将doc文件输到前端,将doc文件以InputStream流返回,也有生成docx的方法,这里为下面生成docx做准备。

    package util.wordtemplate;
    
    
    import freemarker.template.Configuration;
    import freemarker.template.Template;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.ResourceUtils;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.*;
    import java.net.URLEncoder;
    import java.nio.charset.StandardCharsets;
    import java.util.Enumeration;
    import java.util.UUID;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;
    import java.util.zip.ZipOutputStream;
    
    
    @Slf4j
    public class WordTemplateUtil {
        private Configuration configuration;
        //模板文件的位置
        private static String tempPath;
    
        /**
         * 构造函数
         */
        public WordTemplateUtil() {
            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
            configuration.setDefaultEncoding("UTF-8");
            configuration.setClassForTemplateLoading(this.getClass(), "/templates");
            if(tempPath == null || tempPath.length()==0){
                try {
                    //装载模板文件目录
                    tempPath = ResourceUtils.getURL("classpath:").getPath() + "templates/";
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    
        /**
         * 获取模板
         * @param name
         * @return
         * @throws Exception
         */
        public Template getTemplate(String name) throws Exception {
            return configuration.getTemplate(name);
        }
    
        /**
         * 获取word byte
         * @param data
         * @param templateName
         * @return
         * @throws IOException
         */
        public InputStream getInputStreamWordDoc(Object data, String templateName) {
            return getFreemarkerInputStream(data, templateName);
        }
    
        /**
         * 获取word byte
         * @param data  填充数据
         * @param templateName  模板名称
         * @param origTemplateName  原始模板名称
         * @return
         */
        public InputStream getInputStreamWordDocx(Object data, String templateName, String origTemplateName) {
            File outFile = null;
            OutputStream outputStream = null;
            InputStream inputStream = null;
            ZipOutputStream zipout = null;
            try {
                // 临时文件路径
                String tempFilePathName = tempPath + UUID.randomUUID().toString().replaceAll("-", "") + ".docx";
                outFile = new File(tempFilePathName);
                outputStream = new FileOutputStream(outFile);
                // 内容模板
                ByteArrayInputStream xmlTemplateInput = getFreemarkerInputStream(data, templateName);
                //最初设计的模板
                String origDocxFilePathName = tempPath + origTemplateName;
                File origDocxFile = new File(origDocxFilePathName);
                if (!origDocxFile.exists()) {
                    origDocxFile.createNewFile();
                }
                ZipFile zipFile = new ZipFile(origDocxFile);
                Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
                zipout = new ZipOutputStream(outputStream);
                // 开始覆盖文档
                int len = -1;
                byte[] buffer = new byte[2 * 1024];
                while (zipEntrys.hasMoreElements()) {
                    ZipEntry next = zipEntrys.nextElement();
                    InputStream is = zipFile.getInputStream(next);
                    if (!next.toString().contains("media")) {
                        zipout.putNextEntry(new ZipEntry(next.getName()));
                        if ("word/document.xml".equals(next.getName())) {
                            if (xmlTemplateInput != null) {
                                while ((len = xmlTemplateInput.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                xmlTemplateInput.close();
                            }
                        } else {
                            while ((len = is.read(buffer)) != -1) {
                                zipout.write(buffer, 0, len);
                            }
                            is.close();
                        }
                    }
                }
                inputStream = new FileInputStream(outFile);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }finally {
                if(zipout != null){
                    try {
                        zipout.close();
                    } catch (IOException e) {
                        log.error(e.getMessage(), e);
                    }
                }
                if(outputStream != null){
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        log.error(e.getMessage(), e);
                    }
                }
                if (outFile != null) outFile.delete();
            }
            return inputStream;
        }
    
        /**
         * 获取模板字符串输入流
         * @param data   参数
         * @param templateName  模板名称
         * @return
         */
        public ByteArrayInputStream getFreemarkerInputStream(Object data, String templateName) {
            ByteArrayInputStream inputStream = null;
            try {
                //获取模板
                Template template = getTemplate(templateName);
                StringWriter swriter = new StringWriter();
                //生成文件
                template.process(data, swriter);
                //这里一定要设置utf-8编码 否则导出的word中中文会是乱码
                inputStream = new ByteArrayInputStream(swriter.toString().getBytes(StandardCharsets.UTF_8));
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            return inputStream;
        }
    
        /**
         * 导出word文档到客户端
         * @param response
         * @param fileName
         * @param tplName
         * @param data
         * @throws Exception
         */
        public void exportDoc(HttpServletResponse response, String fileName, String tplName, Object data) throws Exception {
            response.reset();
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/msword");
            fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment; filename*=utf-8''" + fileName);
            // 把本地文件发送给客户端
            Writer out = response.getWriter();
            Template template = getTemplate(tplName);
            template.process(data, out);
            out.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180

    3.2.5 测试一下导出doc文件方法如下:

    // Controller 方法
    /**
     * @Description   导出模板报告
     */
    @ApiOperation(value = "导出模板报告接口", notes = "")
    @PostMapping("/export")
    public void toExportReport(HttpServletResponse response, @RequestBody DataRequest dataRequest) {
        try {
            testService.exportReport(response, dataRequest);
        } catch (BusinessException e) {
            log.error(e.getMessage(), e);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
    
    // Service 方法
    @Override
    public void exportReport(HttpServletResponse response, DataRequest dataRequest) {
        try {
            WordTemplateUtil templateUtil = new WordTemplateUtil();
            templateUtil.exportDoc(response, "报告导出.doc", "test_template.ftl", dataRequest);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
    // 使用postman调用接口 模拟参数
    {
        "reportDate":"20XX年XX月XX日",
        "text1":"内容一",
        "text2s":[
            "内容二",
            "内容三",
            "内容四"
        ],
        "text3s":[
            {
                "num":"1",
                "text3":"内容五",
                "text4":"内容六",
                "text5":"内容七",
                "text6":"内容八",
                "text7":"内容九",
                "text8":"内容十"
            },
            {
                "num":"2",
                "text3":"内容五1",
                "text4":"内容六2",
                "text5":"内容七3",
                "text6":"内容八4",
                "text7":"内容九5",
                "text8":"内容十6"
            }
        ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    3.3 导出docx模板

    3.3.1 以上述模板为例,解压 docx 文件,在 /word 文件夹下找到 document.xml ,使用 freemaker 语法将该 xml 文件修改成模板,在需要赋值的地方采用 freemaker语法 替换,采用上述doc赋值方式即可。如下图:
    在这里插入图片描述
    3.3.2 将解压出来的 xmldocx原文件 放入templates 文件夹下,并重命名test_template,xml 文件内容按照上述doc模板进行修改即可,这里不再赘述。如下图:
    在这里插入图片描述
    3.3.3 使用上面工具类进行赋值导出,这里采用流的形式返回,大家可以根据自己具体业务进行转换成自己需要的返回结果即可。我这原业务其实是需要将流转换成MultipartFile类型,然后调用上传接口将文件上传到文件服务器上,然后再通过feign调用转换接口,将docx文件转换成pdf文件,返回前端pdf下载信息,前端直接下载就行了,这里主要给大家提供学习参考,我就简化了流程,如果大家需要我这流程,大家留言,我会尽快将代码粘出来给大家参考。 方法如下:

    public InputStream exportReport(DataRequest dataRequest) {
            InputStream inputStream = null;
            Date date = new Date();
            try {
                WordTemplateUtil templateUtil = new WordTemplateUtil();
                InputStream inputStream = templateUtil.getInputStreamWordDocx(dataRequest, "test_template.xml", "test_template.docx");
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            return inputStream;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.3.4 docx模板避坑指南
    3.3.4.1 运行项目后调用接口就是找不到xml和docx文件位置,进入target 目录也没有看到,这是需要确认maven打包是否包含了xml文件和docx文件,如果没有请在 pom 如下位置添加配置即可。

    <resources>
         <resource>
              <directory>src/main/resources</directory>
              <filtering>true</filtering>
              <includes>
                  <include>bootstrap.yml</include>
                  <include>**/*.xml
                  **/*.ftl</include>
                  <include>**/*.sql
                  **/*.docx</include>
              </includes>
          </resource>
    </resources>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.3.4.2 调用接口会在 ZipFile zipFile = new ZipFile(origDocxFile); 这块报错,这块坑了我两小时才找到原因,一直以为我代码问题,后来发现因为maven在打包的时候会把docx文件进行压缩,所以损坏了文件,如何解决,就是编译的时候不让动docx,配置如下:

    <plugin>
        <artifactId>maven-resources-plugin</artifactId>
        <configuration>
            <encoding>UTF-8</encoding>
            <nonFilteredFileExtensions>
                <nonFilteredFileExtension>docx</nonFilteredFileExtension>
            </nonFilteredFileExtensions>
        </configuration>
    </plugin>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    kubeadm 部署方式修改kube-proxy为 ipvs模式
    【工具篇】Unity翻书效果插件Book-Page Curl Pro教程
    内网离线安装elasticsearch、kibana
    uni-app h5、app模式下集成turn.js 翻书动画
    如何自学(黑客)网络安全技术————(详细分析学习思路)方法
    一本通1080;余数相同问题
    Ceph常用命令总结
    小啊呜产品读书笔记001:《邱岳的产品手记-02》开篇词&01&02&03讲
    当鼠标移动到table的时候行列都有样式
    C++的一些应用
  • 原文地址:https://blog.csdn.net/buertianci/article/details/126069856