本文记录一下利用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比对,自定义路径后会报错。