freemarker导出word文档

 本文记录一下利用freemarker模板工具导出word文档的方法及开发过程中遇到的问题。导出后的效果是下图这样的。

1.制作模板

1.1生成word文档

首先把需要导出的内容及格式生成一个word文档,文档中需要传入的参数用${参数名}的方式占位。字体、下划线及段落格式都需要设置好。

 1.2将word文档另存为xml文件

 1.3xml文件修改后缀名为.ftl

ftl文件放到resource目录下,用idea打开之后,ctrl+alt+l格式化一下。打开之后,文件是这样的:

注意:关于.ftl模板文件的存放路径,我会在下面详细说。

1.4模板文件常用语法

模板文件在制作word文档阶段,尽量把字体、字号等样式类的东西设置好,在ftl文件里修改比较麻烦。ftl文件的整体结构,从上而下一般是:文档说明部分、样式部分、内容部分。下面针对内容部分常用的几种修改语法做一下简单介绍。我们通常一行的内容会存在于一组<w:p></w:p>标签里面,到下一组wp标签时会换行。

1.4.1 list标签

list标签用于循环展示内容,本案例中,试卷的题目是需要循环的,因此用到list标签。

写法:<#list exportPaperList! as exam></#list>

注意:as左边为业务代码传入的参数,名称必须一致,as右边为list内单个实体的名称,可以自行定义。

示例:

1.4.2 if标签

if标签用于判断传入的参数是否存在,判断成立则if标签内的内容正常展示,不成立的不展示。

写法:<#if exam.optionA??></#if>

本案例中,用户可以选择导出的试卷带答案或者不带答案,可以通过If标签判断有无传参来实现。如果不是用if标签判断,直接把空字符展示的话,为空的wp标签也会展示一个空行,导出的文档不美观。

示例:

1.4.3 文本判空

传入的参数为空时会报错,因此对可能为空的文本要给予默认值。实际使用中,该语法大部分都是配合if标签一块使用,可根据实际情况灵活使用。本案例中,单选、判断等题型没有评语,只有简答题阅卷时要写评语,因此需要对评语文本进行判空,为空是基于默认值-空字符串。

写法:${(exam.stem) ? default ('')}

示例:

2.业务代码

2.1需要的依赖

		<!-- freemarker -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-freemarker</artifactId>
		</dependency>

2.2业务代码

    /**
     * 导出已答试卷
     * @param recordId		试卷Id
     * @param request
     * @param response
     * @throws IOException
     */
    @GetMapping("/exportWord")
    public void getWord(@RequestParam("recordId") String recordId, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (StringUtils.isEmpty(recordId)){
            throw new BusinessException("缺少必要参数!");
        }
        List<MmsTestPaperRecordDetail> recordDetailList = mmsTestPaperRecordService.getRecordDetail(recordId);
        List<ExportPaperVo> exportPaperList = new ArrayList<>();
        for (MmsTestPaperRecordDetail mmsTestPaperRecordDetail : recordDetailList) {
            ExportPaperVo exportPaperVo = new ExportPaperVo();
            //1. 【单选题】请问,1+1等于几。(难易程度:易,分值:1分)
            exportPaperVo.setStem(mmsTestPaperRecordDetail.getSeqNumber() + "." + "【" + mmsTestPaperRecordDetail.getTopicType() + "】" + mmsTestPaperRecordDetail.getContent() + "(难易程度:" + mmsTestPaperRecordDetail.getDifficulty() + ", 分值:" + mmsTestPaperRecordDetail.getGrade() + "分)");
            //单选及多选填充四个选项
            if (TopicTypeEnum.SINGLE_CHOICE.equals(mmsTestPaperRecordDetail.getTopicType()) || TopicTypeEnum.MULTIPLE_CHOICE.equals(mmsTestPaperRecordDetail.getTopicType())){
                if (StringUtils.isNotNull(mmsTestPaperRecordDetail.getOptionsA())){
                    exportPaperVo.setOptionA("A." + mmsTestPaperRecordDetail.getOptionsA());
                }
                if (StringUtils.isNotNull(mmsTestPaperRecordDetail.getOptionsB())){
                    exportPaperVo.setOptionB("B." + mmsTestPaperRecordDetail.getOptionsB());
                }
                if (StringUtils.isNotNull(mmsTestPaperRecordDetail.getOptionsC())){
                    exportPaperVo.setOptionC("C." + mmsTestPaperRecordDetail.getOptionsC());
                }
                if (StringUtils.isNotNull(mmsTestPaperRecordDetail.getOptionsD())){
                    exportPaperVo.setOptionD("D." + mmsTestPaperRecordDetail.getOptionsD());
                }

            }
            //判断仅填充前两个选项
            if(TopicTypeEnum.TRUE_OR_FALSE_QUESTION.equals(mmsTestPaperRecordDetail.getTopicType())){
                exportPaperVo.setOptionA("正确。");
                exportPaperVo.setOptionB("错误。");
            }

            //正确答案:xxx,您的答案:xxx,您的得分:xxx,评语:。
            exportPaperVo.setAnswer("正确答案:" + mmsTestPaperRecordDetail.getTrueAnswer() + ",您的答案:" + mmsTestPaperRecordDetail.getMyAnswer() + ",您的得分:" + mmsTestPaperRecordDetail.getMyScore());

            //只有简答题需要评语,没有评语的不展示
            if (TopicTypeEnum.SHORT_ANSWER_QUESTION.equals(mmsTestPaperRecordDetail.getTopicType())
                    && StringUtils.isNotNull(mmsTestPaperRecordDetail.getComment())){
                exportPaperVo.setAnswer(exportPaperVo.getAnswer() + ",评语:" + mmsTestPaperRecordDetail.getComment() + "。");
            }else {
                exportPaperVo.setAnswer(exportPaperVo.getAnswer() + "。");
            }

            exportPaperList.add(exportPaperVo);
        }

        HashMap<String, Object> map = new HashMap<>();
        map.put("exportPaperList",exportPaperList);

        //查询考试名称,成绩,阅卷人,阅卷时间信息
//        MmsTestPaperRecord mmsTestPaperRecord = mmsTestPaperRecordMapper.getTestPaperRecordDetailById(recordId);
        //考试记录主表增加了考试名称字段,不再需要联表查询
        MmsTestPaperRecord mmsTestPaperRecord = mmsTestPaperRecordService.getById(recordId);
        //考试名称
        map.put("title", mmsTestPaperRecord.getExamName());
        //成绩
        map.put("score", mmsTestPaperRecord.getTotalScore());
        //阅卷人
        map.put("markName", mmsTestPaperRecord.getMarkName());
        //阅卷时间
        if (ObjectUtils.isNotEmpty(mmsTestPaperRecord.getMarkTime())){
            map.put("markTime", DateUtils.formatDateYMD(mmsTestPaperRecord.getMarkTime()));
        }


        //模板名称
        String wordTemplateName = "exam.ftl";
        //导出文件名称
        String fileName = mmsTestPaperRecord.getExamName() + ".docx";
        //没用
        String name = "name";

        WordUtil.exportMillCertificateWord(request,response,map,wordTemplateName,fileName,name);
    }

2.3导出工具类代码

package org.jeecg.modules.exam.utils;

import freemarker.cache.FileTemplateLoader;
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;

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();
    private static final String templateFolder = "/opt/exam/upload/template";

    static {
        configuration = new Configuration();
        configuration.setDefaultEncoding("utf-8");
        try {
            System.out.println(templateFolder);
//            configuration.setDirectoryForTemplateLoading(new File(templateFolder));
            FileTemplateLoader fileTemplateLoader = new FileTemplateLoader(new File(templateFolder), true);
            configuration.setTemplateLoader(fileTemplateLoader);
        } 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(String.valueOf(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;
    }
}

3.遇到的问题

开发过程中,遇到的主要问题是ftl模板文件路径问题。开发过程中,模板文件放在resources/templates路径下,用idea运行没有问题。打包部署后,发现找不到模板文件,根据报错日志,发现需要把模板文件放到jar包所在的目录下才可以,经测试,放到jar包同目录下确实能解决问题,但是这样部署起来不太方便,因此想测试一下能够把模板文件放在项目中打包时打到jar包里,或者能够存放在我配置文件中指定的路径下,经测试,这两种方式均未能实现。通过看源码,发现模板文件不能从jar包中读取;在配置文件中配置模板路径后,也未能生效。最终,通过修改读取方法,采用指定绝对路径的方式实现自定义模板文件路径。

问题1:配置文件中的配置不生效

从配置文件看,应该是支持配置模板文件存放路径的,本案例中配置了实际未生效。 

问题2:部分方法不支持自定义路径

 最初是用的“setDirectoryForTemplateLoading”方法进行配置模板文件,后来发现只要修改了路径,就会报错,查看源码后,发现注释中推荐了其他方法。

 因此改用“setTemplateLoader”方法。要注意,new FileTemplateLoader时,第二个参数要传true,传false的话,还是会与baseDir比对,自定义路径后会报错。

  • 13
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值