Freemarker按模板导出Word

Freemarker按模板导出Word

彼方 980 2022-05-24

前言

公司要求按照客户提供的四个word文档来导出文件,要求样式不变,将内容填充到文档中,并且文档中包含图片。

需求分解

需要先根据Word文档来制作模板,然后使用freemarker提供的替换符来替换文档中需要填充字体的地方,如果有图片,需要获取图片并转成Base64编码再替换文档中对应的属性值即可。

开发步骤

模板制作

打开Word文件,在需要填充字体的地方使用${}符号进行占位,内部填充属性名,如果有图片填充需要,需要先找一张图片进行占位(图片大小和位置要设置好)
Snipaste_2022-05-24_14-31-36

转换文档类型

右键Word文档,另存为xml结尾的文件,将文件中图片对应的Base64编码删除,便于稍后替换。

创建FTL文件

在项目中创建ftl结尾的文件,将xml文件中的代码复制到ftl文件中,按ctrl+alt+l键进行格式化。

修改FTL文件

格式化之后的ftl文件需要从上到下进行检查,对替换符位置错误的手动进行修改,将刚刚删除Base64编码的地方用对应的属性值替换(注意上面的图片格式也要同步替换,否则图片无法显示)
Snipaste_2022-05-24_14-37-19

Java相关

Java代码中只需要查找文档信息对应的对象,需要转Base64的转一下,需要转成字符串集合的也转一下,然后传到下面的工具类里即可。

相关依赖

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.29</version>
        </dependency>

相关工具类

public class FtlFileUtils {

    /**
     * 生成flt模板文件(FTL工具类)
     *
     * @param templateName 模板名称
     * @param model 占位符替换的对象
     * @param file 目标文件
     * @throws IOException 文件读取异常
     * @throws TemplateException 模板异常
     */
    public static void createFtlFile(String templateName, Object model, File file)
            throws IOException, TemplateException {
        Writer out = null;
        try {
            Configuration configuration = new Configuration(Configuration.getVersion());
            configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
            configuration.setOutputFormat(XMLOutputFormat.INSTANCE);
            configuration.setClassForTemplateLoading(FtlFileUtils.class, DownloadConstants.TEMPLATE_RESOURCE_PATH);
            Template template = configuration.getTemplate(templateName, StandardCharsets.UTF_8.name());
            out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8),
                    FileUtils.DEAFULT_STREAM_READ_COUNT * 10);
            template.process(JSONObject.parseObject(JSON.toJSONString(model)), out);
        } catch (IOException | TemplateException e) {
            log.error("生成模板文件异常,templateName:{}, model:{}, file:{}", templateName, model.toString(), file.getAbsolutePath());
            throw e;
        } finally {
            StreamUtils.close(out);
        }
    }
}
public class HttpDownloadUtils {
    /**
     * 文件下载工具类
     *
     */
    private static final String HEADER_RANGE = "Range";
    private static final String HEADER_CONTENT_RANGE = "Content-Range";
    private static final String HEADER_CONTENT_RANGE_TEMPLATE = "bytes %s-%s/%s";
    private HttpDownloadUtils() {
    }

    /**
     * 下载文件
     *
     * @param response http输出流
     * @param request http输入流
     * @param businessPath 业务路径
     * @param filePath 文件路径
     * @param fileName 真实文件名
     * @throws IOException 文件不存在抛出异常
     */
    public static void downloadFile(HttpServletResponse response, HttpServletRequest request, String businessPath,
                            String filePath, String fileName) throws IOException {
        File pathFile = new File(businessPath + FileUtils.DIR_SPERATOR + filePath);
        if (!pathFile.exists()) {
            ResultUtils.writeJavaScript(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response,
                    ResponseMessageEnums.DOWNLOAD_1301);
            return;
        }
        try {
            HttpHeaderUtils.setFileDownloadHeader(request, response, fileName);
            // 如果是video标签发起的请求就不会为null
            String rangeString = request.getHeader(HEADER_RANGE);
            if (EmptyUtils.isNotBlank(rangeString)) {
                int fileLength = Long.valueOf(pathFile.length()).intValue();
                int startIndex;
                int length = 1024 * 1024;
                String[] ranges = rangeString.split("=")[1].split("-");
                startIndex= Integer.parseInt(ranges[0]);
                int endIndex = startIndex + length - 1;
                if (ranges.length > 1 && EmptyUtils.isNotBlank(ranges[1])) {
                    // 有设置结束位
                    endIndex = Integer.parseInt(ranges[1]);
                }
                if (endIndex <= startIndex ) {
                    // 往前拖动
                    endIndex = startIndex + length - 1;
                }
                if (endIndex > fileLength - 1) {
                    // 超过文件最大长度
                    endIndex = fileLength - 1;
                }
                int contentLength = endIndex - startIndex;
                // 视频文件大小 Content-Length [文件的总大小] - [客户端请求的下载的文件块的开始字节]
                response.setContentLength(contentLength);
                // 拖动进度条时的断点 bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
                response.setHeader(HEADER_CONTENT_RANGE, String.format(HEADER_CONTENT_RANGE_TEMPLATE, startIndex, endIndex, fileLength));
                byte[] buffer = new byte[1024];
                FileInputStream fis = null;
                BufferedInputStream bis = null;
                OutputStream os = null;
                try {
                    os = response.getOutputStream();
                    fis = new FileInputStream(pathFile);
                    bis = new BufferedInputStream(fis);
                    bis.skip(startIndex);
                    if (endIndex < fileLength - 1) {
                        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                        int n, readLength = 0;
                        while (readLength <= contentLength - 1024) {
                            n = bis.read(buffer);
                            readLength += n;
                            os.write(buffer, 0, n);
                        }
                        if (readLength <= contentLength) {
                            n = bis.read(buffer, 0, (contentLength - readLength));
                            os.write(buffer, 0, n);
                        }
                    } else {
                        for (int i = bis.read(buffer); i != -1; i = bis.read(buffer)) {
                            os.write(buffer, 0, i);
                        }
                    }
                } catch (IOException e) {
                    if (!(e instanceof ClientAbortException)) {
                        log.error("获取附件信息出错,filePath:{},fileName:{}", filePath, fileName, e);
                        ResultUtils.writeJavaScript(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response,
                                ResponseMessageEnums.DOWNLOAD_1302);
                    }
                } finally {
                    StreamUtils.close(bis);
                    StreamUtils.close(fis);
                    StreamUtils.close(os);
                }
            } else {
                InputStream inputStream = new FileInputStream(pathFile);
                response.setHeader("Content-Length", String.valueOf(inputStream.available()));
                FileUtils.downloadFile(response.getOutputStream(), inputStream);
            }
        } catch (Exception e) {
            if (!(e instanceof ClientAbortException)) {
            log.error("获取附件信息出错,filePath:{},fileName:{}", filePath, fileName, e);
            ResultUtils.writeJavaScript(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response,
                    ResponseMessageEnums.DOWNLOAD_1302);
            }
        }
    }

    /**
     * 下载文件
     *
     * @param response http输出流
     * @param request http输入流
     * @param file 文件
     * @param fileName 真实文件名
     * @throws IOException 文件不存在抛出异常
     */
    public static void downloadFile(HttpServletResponse response, HttpServletRequest request, File file,
                                    String fileName) throws IOException {
        if (!file.exists()) {
            ResultUtils.writeJavaScript(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response,
                    ResponseMessageEnums.DOWNLOAD_1301);
            return;
        }
        try {
            HttpHeaderUtils.setFileDownloadHeader(request, response, fileName);
            FileUtils.downloadFile(response.getOutputStream(), file);
        } catch (Exception e) {
            log.error("获取附件信息出错,filePath:{},fileName:{}", file.getAbsolutePath(), fileName, e);
            ResultUtils.writeJavaScript(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response,
                    ResponseMessageEnums.DOWNLOAD_1302);
        }
    }
}

Freemarker相关教程

非空判断

最好对所有的填充值进行非空判断,示例:
原值:${name}
非空判断值:${name?if_exists}
循环之前对集合判空,示例:

 <#if shouldInstallList??> // 循环前要对集合判空
   <#list 0..(shouldInstallList?size-1)!0 as i> // 此类型为按集合长度循环
     <w:tr w:rsidR="00C86F38" w14:paraId="312081F1" w14:textId="77777777" w:rsidTr="009A69FA">
       <w:trPr>
         <w:trHeight w:val="352"/>
       </w:trPr>
       <w:tc>
         <w:p w14:paraId="616F7EC9" w14:textId="77777777" w:rsidR="00C86F38"
              w:rsidRDefault="001D7EA2">
           <w:proofErr w:type="spellStart"/>
             <w:t>${shouldInstallList[i]!''}</w:t>
           <w:proofErr w:type="spellEnd"/>
         </w:p>
       </w:tc>
       <w:tc>
         <w:tcPr>
           <w:tcW w:w="5436" w:type="dxa"/>
           <w:tcBorders>
             <w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>
             <w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>
             <w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>
             <w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>
           </w:tcBorders>
         </w:tcPr>
         <#if confirmShouldInstallList??>
           <w:p w14:paraId="0D5FD950" w14:textId="77777777" w:rsidR="00C86F38"
                w:rsidRDefault="007E34A2">
             <w:pPr>
               <w:rPr>
                 <w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:hint="eastAsia"/>
                 <w:szCs w:val="21"/>
               </w:rPr>
             </w:pPr>
             <w:r>
             </w:r>
             <w:proofErr w:type="spellStart"/>
             <w:r w:rsidRPr="002645FE">
               <w:rPr>
                 <w:rFonts w:ascii="宋体" w:hAnsi="宋体"/>
                 <w:szCs w:val="21"/>
               </w:rPr>
               <w:t>${confirmShouldInstallList[i]!''}</w:t>
             </w:r>
             <w:proofErr w:type="spellEnd"/>
           </w:p>
           </#if>
       </w:tc>
     </w:tr>
     </#list>
   </#if>

默认填充

你还可以为空时默认填充值,示例:
${name!'名字'}name为空时,默认填充名字两字

两种循环

freemarker中常用的两种循环:

  • 根据集合长度进行循环
  • 根据集合进行循环

示例1:
根据集合长度进行循环,适用于List<Stirng>类型集合,可以获取指定索引的字符串。
代码:

<#list 0..(shouldInstallList?size-1)!0 as i>
<#-- 内部使用此类型进行填充-->
  <w:t>${shouldInstallList[i]!''}</w:t>
</#list>

示例2:
根据集合进行循环,适用于List<Object>类型集合,可以获取集合中指定属性的值。
代码:

<#list ticketEvaluation as evaluation>
  	<#-- 内部使用此类型进行填充-->
  	<w:t>${evaluation.content?if_exists}</w:t>
</#list>

获取集合长度

<#if (fields?size>0) >

</#if>

循环段落

需要循环段落,找到段落对应的 <w:p> 标签,在标签外进行循环

循环表格

需要循环表格的某一行(包含外框线),找到表格某一行的<w:r >标签,在标签外进行循环

判断定值

Freemarker中可以根据属性值展示不通的数据
示例1:如果当前索引为i的值为1,则文档中对应位置打√

<#if confirmRunningSafetyMeasuresList[i] = '1'>
        <w:t>√</w:t>
</#if>

示例2:根据风险等级判断应该显示的字体

<#if riskLevel??>
  <#if riskLevel = 1>
    <w:t>本次作业风险等级:低风险 </w:t>
    </#if>
  <#if riskLevel = 2>
    <w:t>本次作业风险等级:一般风险</w:t>
    </#if>
  <#if riskLevel = 3>
    <w:t>本次作业风险等级:较大风险</w:t>
    </#if>
  <#if riskLevel = 4>
    <w:t>本次作业风险等级:重大风险</w:t>
    </#if>
 </#if>

获取索引

<#list ticketEvaluation as evaluation>
  	<#-- 内部使用此类型进行填充-->
  	<w:t>${evaluation_index}</w:t>
</#list>

占位符

&#160;

说明

  • 导出前先确定模板替换值得样式需要和文档中字体的样式一样,否则后面转成xml文件后修改比较麻烦,最好另存为之前确认模板没有问题再转格式。
  • 非空判断一定要做!!!