背景介绍
最近项目中提出了通过现有数据导出word文档的需求;开发语言java,运行系统为linux;导出word文档的结构比较复杂,包含单条数据展示,多条数据展示,插入图片等等,同时样式要求比较高,项目里虽然有Apache POI包,但操作word比较繁琐,同时一些功能无法实现;
网上有人通过Freemarker模板的方式来处理word文档生成,所以尝试了一下,实现了word文档的生成;
大体原理如下:
Microsoft Office遵循OpenXml标准,即,可以用xml的方式描述一个word文档,这种格式可以被各office软件提供商识别;
Freemarker可以将数据绑定到.ftl模板中,将模板存储成.doc格式即可用Microsoft Office Word 打开;
相关概念
- OpenXml:Ecma Office Open XML(“Open XML”)是针对字处理文档、演示文稿和电子表格的国际化开放标准,可免费供多个应用程序在多个平台上实现。Microsoft Office(2007、2003、XP、2000)、OpenOffice Novell Edition、开源项目 Gnumeric、Neo-Office 2.1 和 PalmOS (Dataviz) 已经支持 Open XML。
- FreeMarker:FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。
依赖包
测试项目用Maven构建,依赖如下:
<!-- 通过模板生成word文档 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.13</version>
</dependency>
模板
word模板:
具体模板请参考:Freemarker生成word文档
模板生成方法:
打开Word程序(建议用Microsoft Office,WPS也可以创建word文档,但兼容性不是很好,用WPS创建的模板生成之后Microsoft Office Word很多时候打不开),新建空白文档,插入需要的内容,将文件另存为“Word 2003 XML 文档(.xml)”;将文件后缀名改为“.ftl”,此时就拿到了需要的模板;
简单说明一下这个模板:
-
<w:tbl></w:tbl>
表示一个word表格; -
<w:tr></w:tr>
表示表格里的一行; -
<w:tc></w:tc>
表示表格里的一列; -
<w:p></w:p>
表示一个段落; -
<Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.jpeg"/>
表示一个图片的声明; -
<w:drawing></w:drawing>
表示一个图片的引用; -
<pkg:part pkg:name="/word/media/image1.jpeg" pkg:contentType="image/jpeg" pkg:compression="store"> <pkg:binaryData>****</pkg:binaryData> </pkg:part>
表示一个图片的实际内容;图片用base64字符串的方式表示,每76个字符需要换行;
Freemarker语法
这里不具体介绍Freemarker语法,仅说明用demo中用到的几个语法;
${sim.qymc}
数据绑定;<#list cpls as cpl>
循环列表;
代码
- 数据实体类
public class ExportProductBean {
private String qymc;// 企业名
private String hgbm;// 海关编码
private String cksp;// 出口商品
private String ckje;// 出口金额
public String getQymc() {
return qymc;
}
public void setQymc(String qymc) {
this.qymc = qymc;
}
public String getHgbm() {
return hgbm;
}
public void setHgbm(String hgbm) {
this.hgbm = hgbm;
}
public String getCksp() {
return cksp;
}
public void setCksp(String cksp) {
this.cksp = cksp;
}
public String getCkje() {
return ckje;
}
public void setCkje(String ckje) {
this.ckje = ckje;
}
}
- 测试方法
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import freemarker.cache.FileTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
public class App {
private static final String ENCODE = "UTF-8";
private static final int PIC_LINE = 76;
public static void main(String[] args) throws Exception {
String tmplPath = "C:\\word\\";
String tmplName = "word_ftl.ftl";
String filePath = "C:\\word\\";
String fileName = "word_ftl.doc";
// 获取数据
Map<String, Object> dataMap = getData();
Configuration configuration = new Configuration();
FileTemplateLoader templateLoader = new FileTemplateLoader(new File(tmplPath));
configuration.setTemplateLoader(templateLoader);
File outFile = new File(filePath + fileName);
// 如果输出目标文件夹不存在,则创建
if (!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdirs();
}
// 将模板和数据模型合并生成文件
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), ENCODE));
Template t = configuration.getTemplate(tmplName, ENCODE);
t.process(dataMap, out);
out.flush();
out.close();
}
private static Map<String, Object> getData() {
Map<String, Object> dataMap = new HashMap<String, Object>();
// 单条数据
dataMap.put("sim", getSimple());
// 多条数据
dataMap.put("cpls", getComplex());
// 图片
dataMap.put("pic", getPic());
return dataMap;
}
private static ExportProductBean getSimple() {
ExportProductBean export = new ExportProductBean();
export.setCkje("10万");
export.setCksp("玻璃内胆制");
export.setHgbm("340****333");
export.setQymc("XX进出口有限公司");
return export;
}
private static List<ExportProductBean> getComplex() {
List<ExportProductBean> exs = new ArrayList<ExportProductBean>();
ExportProductBean export1 = new ExportProductBean();
export1.setCkje("10万");
export1.setCksp("玻璃内胆制");
export1.setHgbm("340****333");
export1.setQymc("XX进出口有限公司");
ExportProductBean export2 = new ExportProductBean();
export2.setCkje("15万");
export2.setCksp("保温瓶");
export2.setHgbm("340****333");
export2.setQymc("XX进出口有限公司");
ExportProductBean export3 = new ExportProductBean();
export3.setCkje("20万");
export3.setCksp("其他真空容器及零件(玻璃胆除外)");
export3.setHgbm("340****333");
export3.setQymc("XX进出口有限公司");
ExportProductBean export4 = new ExportProductBean();
export4.setCkje("25万");
export4.setCksp("烟斗及烟斗头");
export4.setHgbm("340****333");
export4.setQymc("XX进出口有限公司");
exs.add(export1);
exs.add(export2);
exs.add(export3);
exs.add(export4);
return exs;
}
private static String getPic() {
String pic = "/9j/4*****";// 这里省略了图片内容,具体内容请参考下载资源
String pic_n = "";
while (pic.length() >= PIC_LINE) {
String lineStr = pic.substring(0, PIC_LINE);
pic = pic.substring(PIC_LINE);
if (pic_n.length() == 0) {
pic_n = lineStr;
continue;
}
pic_n = pic_n + "\r\n" + lineStr;
}
if (pic_n.length() == 0) {
pic_n = pic;
} else {
if (pic.length() > 0) {
pic_n = pic_n + "\r\n" + pic;
}
}
return pic_n;
}
}
效果
最终生成效果如下:
以上是Freemarker生成word文档的全部内容,具体代码请参考:
代码包描述:
- word/word_ftl.ftl:成word需要的模板;
- word/word_ftl.doc:最终效果;
- freemarker-word:测试代码,一个简单的Maven项目;
代码中会有些不太规范或不太优雅的写法,还请诸君不吝指证;
2018-12-14 补充:
工作中经常出现一个问题:本地(windows系统)生成的文档可以打开,但服务器上(linux系统)生成的同样的报告无法打开,仔细对比研究后发现是windows和linux中换行不同引起的
Windows下换行符号是“\r\n”,而linux下是“\n”没有”\r”
解决方法:
代码中强制将“\n”替换成“\r\n”;
2020-07-31 补充:
这种方式生成的word有诸多问题:
1.体积过大,由于文档是xml格式的,一旦内容比较多的时候,文档体积会很大,打开过程会变的很慢;
2.无法用程序插入目录;
解决方法:
.NET中用C#操作word相对比较简单,也更灵活;我们的处理方式是写一个.NET的WebService,在这个WebService里加载这个word文档,另存成普通的word格式,并添加自动生成的目录;