前言
项目有需求,通过特定模板导出 .doc 格式的 Word 文档。因为之前有过使用 apache-poi 导出 excel 的经验,所以在一开始选择了 apache-poi 作为实现依赖,但后来发现生成 word 调用及实现过于复杂,遂放弃。查阅资料后,试着使用 easy-poi 实现,但又发现可定制程度太小,无法动态生成表格和图片,遂放弃。
就在山穷水尽之时,我想到了 freemarker。众所周知,Word 的本质就是一个 xml 文件,所以我们使用 freemarker 来动态生成 Word,应该是可行的。
实现过程
总共分为两部分,一部分是文档模板的准备,一部分是代码的准备。
模板文档准备过程
1. 数据位置替换
(注意:如果要代码插入图片,那么这里一定要插入图片)
模板替换后:
其中表格长度会随着list内容动态生成。
2. 另存为 xml 文件
将刚刚改完的模板另存为 xml 文件。
3. 编辑 xml 文件
- 编辑分开的替换符:
打开 xml 文件,将其中的代码格式化后,我们找出刚才替换符的位置,会发现可能有替换符和值分开的情况: - 这样的替换符会失效,我们将中间的内容删除,让他们在一起:
- 选定动态生成范围,添加 list 标签
在这里,一般 <w:tr> 标签包裹的是一行,<w:tc> 标签包裹的是一个单元格,所以找到表格下 <w:tr> 标签,在外面包裹 <#list> 标签: - 其中,specialityList 是代码里 List 的变量名称,speciality 是模板里的变量名称。
- 图片替换(头疼)
如果需要动态生成图片,就相对麻烦一些。如果没有生成图片需求,可以跳过此部分。
以目前的经验看,有两种不同的 xml 模板,一种是直接在正文内容中插入了 base64 的编码,另一种是通过 id 链接的方式插入图片。
- 如果是第一种方式,替换会简单些,直接将 base64 的编码换成占位变量即可。
- 第二种就比较恶心,需要更改三个地方。
- 首先,在文章末尾找到 base64标签,并添加同级标签和包裹标签,并在中间添加替换符、< pkg:name >值变为自增值。
- 其次,找到rId和图片的桥梁元素< Relationship >(一般在文件头部),将相应元素替换为对应自增值。
- 最后,找到文章位置插入标签 < v:imagedata >(可能是别的,建议根据rid全文查找到),改为自增值和包裹标签。
至此,模板准备工作终于结束了。
代码与项目结构
项目结构
- 建议在resource中路径下建立项目建构
代码这块,比准备模板要简单多啦。
- 首先是生成工具类,注意图片要转 base64 哦
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;
import java.io.*;
import java.util.Map;
/**
* @author amber
*/
public class WordDocxUtil {
/**
* 将图片内容转换成Base64编码的字符串
*
* @param imageFile 图片文件的全路径名称
* @return 转换成Base64编码的图片内容字符串
*/
public static String getImageBase64String(String imageFile) {
if (StringUtils.isEmpty(imageFile)) {
return "";
}
File file = new File(imageFile);
if (!file.exists()) {
return "";
}
InputStream is = null;
byte[] data = null;
try {
is = new FileInputStream(file);
data = new byte[is.available()];
is.read(data);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);
}
public static String getImageBase64String(InputStream inputStream){
byte[] data = null;
try {
data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);
}
/**
* 使用FreeMarker自动生成Word文档
*
* @param dataMap 生成Word文档所需要的数据
* @param fileName 生成Word文档的全路径名称
*/
public static void generateWord(Map<String, Object> dataMap, String fileName, String templatePath, String template) throws Exception {
// 设置FreeMarker的版本和编码格式
Configuration configuration = new Configuration(new Version("2.3.28"));
configuration.setDefaultEncoding("UTF-8");
// 设置FreeMarker生成Word文档所需要的模板的路径
configuration.setDirectoryForTemplateLoading(new File(templatePath));
// 设置FreeMarker生成Word文档所需要的模板
Template t = configuration.getTemplate(template, "UTF-8");
// 创建一个Word文档的输出流
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(fileName)), "UTF-8"));
//FreeMarker使用Word模板和数据生成Word文档
t.process(dataMap, out);
out.flush();
out.close();
}
}
然后是如何应用
Map<String, Object> params = new HashMap<>();
params.put("details", setDocDetailsData(detailsList, workMode));
WordDocxUtil.generateWord(params, "/temp/" + fileName, "src/main/resources/word/", "TemplateV1.xml");
大功告成!