前言
相信很多人都会遇到Java导出的业务,Java导出主要有导出数据到Excel和Word,但是相比较而言对于导出数据到Excel的实现方案会比较成熟一些,网上的教程也是非常之多的.但是导出到Word的方案还是有所欠缺,一些简单的导出还是可以的,但是遇到一些比较复杂的Word模板导出就会很头疼.比如说:Word模板中有相对复杂的表格样式的时候,处理起来也就相对的复杂一些。
本次项目中,我就遇到了一个需要将数据库的数据导出到Word模板中的需求,而且Word模板中包含比较复杂的关系表格。
由于本人之前没有接触过导出之类的业务,所以走了不少弯路。
网上的方案
经过网上查询发现Java导出Word主要有以下几种方案:
以下内容转自子午线,需要详细参考请跳转
- Jacob是Java-COM Bridge的缩写,它在Java与微软的COM组件之间构建一座桥梁。通过Jacob实现了在Java平台上对微软Office的COM接口进行调用。
- Apache POI包括一系列的API,它们可以操作基于MicroSoft OLE 2 Compound Document Format的各种格式文件,可以通过这些API在Java中读写Excel、Word等文件。
- Java2word是一个在java程序中调用 MS Office Word 文档的组件(类库)。该组件提供了一组简单的接口,以便java程序调用他的服务操作Word 文档。 这些服务包括: 打开文档、新建文档、查找文字、替换文字,插入文字、插入图片、插入表格,在书签处插入文字、插入图片、插入表格等。
- FreeMarker生成word文档的功能是由XML+FreeMarker来实现的。先把word文件另存为xml,在xml文件中插入特殊的字符串占位符,将xml翻译为FreeMarker模板,最后用java来解析FreeMarker模板,编码调用FreeMarker实现文本替换并输出Doc。
- PageOffice生成word文件。PageOffice封装了微软Office繁琐的vba接口,提供了简洁易用的Java编程对象,支持生成word文件,同时实现了在线编辑word文档和读取word文档内容。
需求介绍
本人这里,是需要将数据库中的数据查询出,并且填充到Word模板中,并提供下载和导出到指定位置。
模板
简介
在本模板中,红色框中的表格有且可能有多个,蓝色框中的表格行有且可能有多条。需要从数据库中查询出相对应的数据并且填充到Word模板中,模板中红色框中的表格只有一个,需要根据查询出的数据动态的循环出多个,蓝色框中的同理有一个循环出多行。
属于一个典型的一对多对多的数据结构。
使用体验
看到这5中方案的时候,我大概的了解了以下,发现第二种POI是最近比较流行,并且便捷的,为了方便,便毫不犹豫的选择使用POI提供的 poi-tl 。
poi-tl
poi-tl(poi template language)是基于Apache POI的Word模板引擎。纯Java组件,跨平台,代码短小精悍,通过插件机制使其具有高度扩展性。支持DOCX格式的Word模板。
poi-tl 更为详细的介绍请自行跳转,http://deepoove.com/poi-tl/
我在使用poi-tl 的时候,很快的就将蓝框中的循环行给实现了,红色框内的基本数据可以填充,但是这个时候,我发现我无法实现将红色框内的表格循环多个。这就很蛋疼,我在网上查了好久也没有找到解决方案。于是我自己想到一个比较笨的方案,就是将上面的基础信息表格单独到一个Word模板中,然后将基础信息导出到基础的Word模板中,将下面的红色框中的表格单独到一个Word模板中,然后查询到若干条数据,就导出若干个Word文档,最后将这些Word文档合并为一个文档,然后将生成的多余的Word文档删除。现在想想这种方案也是很傻。但是这种方案也没有实现,应为每次删除文档的时候有权限问题,poi-tl 会将本次服务生成的Word文档抓着不放,只有将服务重启或关闭的时候才会删除掉,这就很鸡肋,为此还咨询了好多大佬,也不了了之。
Freemarker
FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。
之后我便尝试使用Freemarker来实现此功能,其实之前我是使用过的,但由于了解不是很深,以为Freemarker的list循环只可以循环一层(说起来也是尴尬)。
操作步骤
使用Freemarker首先我们需要将Word模板转换成可以被Freemarker所识别操作的xml文件,我个人感觉最麻烦的就是准备模板,不知道为什么Word文档转换成xml形式的时候里面有些标签会被分隔开来,所以一定要细心。
- 首先将需要填充的数据对应的标签填入到模板中。
2. 将模板另存为xml格式,并将后缀改为(.ftl)。
将xml文件后缀更改为ftl
3. 将Word文档另存为xml文件时会出现之前写入的标签位置错乱问题。
这里我们需要将错误的格式,调整为可识别的状态。
如图。将选中的蓝色部分删除
修改完成后,如图。
注意,修改模板的时候,可能会遇到图片。
需要将这一长串删除替换为 ${image} 即可。
我们可以将图片存放在项目中,然后获取图片的绝对路径,并将图片进行转码,即可正常显示在导出的Word文档中了。
//获取图片的路径
String imagePath = this.getClass().getClassLoader().getResource("image.jpg").getPath();
//获得图片的Base64码
public static String getImageBase(String src) throws Exception {
if (src == null || src == "") {
return "";
}
File file = new File(src);
if (!file.exists()) {
return "";
}
InputStream in = null;
byte[] data = null;
try {
in = new FileInputStream(file);
data = new byte[in.available()];
in.read(data);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
Base64.Encoder encoder = Base64.getEncoder();
return String.valueOf(encoder.encode(data));
}
在修改模板时,我们本次的重点是循环表格。
我们直接在1级列表的最外层添加标签。
嵌套循环在表格中需要再次循环的标签上加标签。
<#-- 循环外层表格 -->
<#list xxxList as listKey>
<w:tbl>
<#-- 循环表格内的行 -->
<#list listKey.aaaaList as act>
<w:tr>
.........
</w:tr>
</#list>
</w:tbl>
</#list>
到这个时候,我们的模板就准备好了。
- 写Java代码获取数据并填充。
//这里我就省略部分参数
Map<String, Object> datas = new HashMap<>();
datas.put("qq",qq );
datas.put("ww",ww );
//图片
datas.put("image", getImageBase(imagePath));
List<Map<String, Object>> xxxList = new ArrayList<>();
Map<String, Object> xxxMap = new HashMap<>();
xxxMap.put("aa",aa);
xxxMap.put("ss",ss);
List<Map<String, Object>> aaaaList = new ArrayList<>();
Map<String, Object> aaaaMap = new HashMap<>();
aaaaMap.put("zz",zz);
aaaaMap.put("xx",xx);
aaaaList.add(aaaaMap);
xxxMap.put("aaaaList",aaaaList);
xxxList.add(xxxMap);
datas.put("xxxList",xxxList);
ResponseEntity<byte[]> entity = wordUtil.createDoc(datas, "xxx.doc");
```
//createDoc
ByteArrayOutputStream outputStream = null;
OutputStreamWriter writer = null;
configuration.setClassForTemplateLoading(this.getClass(), "/template");
configuration.setEncoding(Locale.getDefault(),"utf-8");
Template t=null;
try {
t = configuration.getTemplate("aa.ftl","utf-8");
} catch (IOException e) {
e.printStackTrace();
}
//输出文档路径及名称
File outFile = new File(fileName);
FileOutputStream fos=null;
try {
fos = new FileOutputStream(outFile);
outputStream = new ByteArrayOutputStream();
writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
try {
t.process(dataMap, writer);
writer.close();
fos.close();
} catch (TemplateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//设置请求头
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attchement;filename=" + fileName);
headers.add("Access-Control-Expose-Headers", "*");
//设置请求状态
HttpStatus statusCode = HttpStatus.OK;
ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(outputStream.toByteArray(), headers, statusCode);
return entity;
}
哦,对了,还有的朋友可能会遇到导出后,文件无法打开的问题。
有这么几种可能,就是在修改ftl模板的时候破坏了文档的结构,
还有就是 有几个不能够替换的字符我们需要特殊的处理。
.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">")
单个表格中,换行如何处理。
.replaceAll("\n", "<w:br/>")
总结
至此,我们就完成了一次较为复杂的Word导出任务。
第一次遇到这种需求,所以学习到了很多,也走了很多弯路,希望本文可以帮助到,被类似问题所困惑的小伙伴。
谢谢大家的阅读。