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>

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>

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

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"
        }
    ]
}

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

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</include>
              <include>**/*.ftl</include>
              <include>**/*.sql</include>
              <include>**/*.docx</include>
          </includes>
      </resource>
</resources>

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>
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Boot + Freemarker 会自动跳到 login 页的原因可能是因为你的应用程序中使用了 Spring Security,而 Spring Security 会默认开启登录验证功能。 如果你想要禁用 Spring Security 的登录验证功能,可以在 application.properties 或 application.yml 文件中添加以下配置: ``` spring.security.enabled=false ``` 如果你想要保留 Spring Security 的登录验证功能,可以在你的应用程序中添加一个自定义的登录页面,并将 Spring Security 的登录页面指向你的自定义登录页面。你可以按照以下步骤进行操作: 1. 创建一个自定义的登录页面。比如,你可以将登录页面的文件名设置为 "custom-login.html",并将其放置在 "/templates/" 目录下。 ``` <!DOCTYPE html> <html> <head> <title>Login Page</title> </head> <body> <h2>Login Page</h2> <form action="/login" method="POST"> <label for="username">Username:</label> <input type="text" id="username" name="username"><br><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br><br> <input type="submit" value="Submit"> </form> </body> </html> ``` 2. 创建一个 Spring Security 的配置类,并在配置类中将登录页面指向你的自定义登录页面。 ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/custom-login.html") .permitAll(); } } ``` 这样,当用户访问需要登录验证的页面时,Spring Security 就会自动跳到你的自定义登录页面。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值