Freemarker按模板导出Word
前言
公司要求按照客户提供的四个word文档来导出文件,要求样式不变,将内容填充到文档中,并且文档中包含图片。
需求分解
需要先根据Word文档来制作模板,然后使用freemarker提供的替换符来替换文档中需要填充字体的地方,如果有图片,需要获取图片并转成Base64编码再替换文档中对应的属性值即可。
开发步骤
模板制作
打开Word文件,在需要填充字体的地方使用${}
符号进行占位,内部填充属性名,如果有图片填充需要,需要先找一张图片进行占位(图片大小和位置要设置好)
转换文档类型
右键Word文档,另存为xml
结尾的文件,将文件中图片对应的Base64编码删除,便于稍后替换。
创建FTL文件
在项目中创建ftl
结尾的文件,将xml文件中的代码复制到ftl文件中,按ctrl+alt+l
键进行格式化。
修改FTL文件
格式化之后的ftl文件需要从上到下进行检查,对替换符位置错误的手动进行修改,将刚刚删除Base64编码的地方用对应的属性值替换(注意上面的图片格式也要同步替换,否则图片无法显示)
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>
占位符
 
说明
- 导出前先确定模板替换值得样式需要和文档中字体的样式一样,否则后面转成
xml
文件后修改比较麻烦,最好另存为之前确认模板没有问题再转格式。 - 非空判断一定要做!!!