目录
工作上要搞一个合同签署功能,小程序上登录人进入功能会对合同进行电子签名,然后后端根据登录人信息和合同word文档模板生成一个合同word文档并保存,踩了不少坑。一开始是用了apache的poi,使用简单,读取word模板,然后遍历每一个段落和节点,判断节点是不是我定义的参数名,是就替换文本值,本以为会很简单就能搞定,结果每次遍历获取的节点经常不完整,${name}获取的时候可能会被分成三段,【${】【name】【}】,导致参数替换不上去,想着直接获取所有文本信息,然后直接string的replace方法替换,结果也不行,他不给直接对段落进行修改,直接麻了,而且pio包引入后,如果当前springboot版本太低,有些包就会出问题,又得特别处理,贼麻烦,后来就换成了freemarker,支持替换图片,不过步骤有些繁琐。
(1)定义word文档模板
既然是对word文档进行填值,那肯定得先定义一个word文档模板了,建一个docx后缀的word文档,并设置好格式,参数等,比如我建了一个word文件名为:wordTemplate.docx,内容是这样的,参数名是用${}包起来
(2)模板二次处理
格式全部调好后保存,并把文件后缀名改成zip,让它变成压缩包,记住你的word模板一定要是docx后缀的,doc后缀是老版的,搞不了。
打开压缩包是这个样子。
进入word文件夹里面是这样的,我们只需要关注【document.xml】【_rels】【media】,如果你不打算放图片,那只关注【document.xml】就行了。
处理模板图片,不涉及图片可以跳过
打开_rels文件夹是这样的。
【document.xml.rels】文件就是存放图片和文档之间的关系,我们把它解压出来并打开。
【打开xml时没有格式化过的,我用了Notepad++打开的,并装了XML Tools插件,然后把它格式化后才成了这个样子的】具体插件安装在文章最后面讲。
接着讲,image1名称是word文档生成的,为了后面填值方便区别,把 media/image1.png 改成 media/headImage.png,然后保存,回到压缩包里面,把原来的【document.xml.rels】替换掉,再打开【media】文件夹,会发现里面有个图片,名为image1.png,就是我们提前放入的头像,我们得把它改名成【headImage.png】,因为上一步,我们修改了【document.xml.rels】文件的映射,这里也得改。
这时候再把压缩包后缀改成docx,你会发现一样还能打开,不过这里我们改回去打开检查没问题后,再把后缀改回成zip。
这里再说一个点,如果模板图片太多,势必会造成模板文件很大,我们可以找一个透明或纯白色的宽高都是1px的图片, 反正很小的图片就行,名字改成【media】里面的图片名,放入【media】里面,相当于占位。也可以一开始创建word模板的时候,放图片直接放这个小体积的图片,把宽高调整到合适的就行。
处理模板内容
打开压缩包把【document.xml】模板内容文件解压出来打开并xml格式化,找到你设置的参数名。
这时候会发现,参数名乱了,${name}可能被分隔七零八落,我们需要重新调一下,确保参数名完整,然后再保存。
这里再讲讲图片跟【document.xml】的关联,我们通过【document.xml.rels】图片映射文件可以看到,每一个图片标签都会有一个id,这个头像这个图片的id是rId4。
我们复制它去【document.xml】里面找,就会发现这个图片的标签以及格式了。
了解一下就行了,回归正题,我们调整好【document.xml】文件并保存,这个文件暂时不需要放回压缩包替换原来文件,到这一步,我们就有两个文件了,这两个文件缺一不可。
找一张头像准备好,用来替换模板里的头像。头像就是下面这个狗头。至于说这个图片放哪,随意,反正到时候java能用IO流读取到就行了,准备图片的base64也行,后面我写了代码支持base64格式。
(3)java对word模板填值
处理完word模板后,来到java代码。
先引入包。不推荐用最新的包,我引了最新的包,用main方法测试的时候没问题,结果启动spring服务的时候,就不行了,报找不到那个版本的参数,后来降级就可以了。
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
代码大致思路是这样的:
- 读取模板【document.xml】
- 值填充
- 重新生成一个【document.xml】
- 把这个新的【document.xml】和头像图片写入定义好的压缩包模板,也就是【wordTemplate.zip】
- 然后把压缩包输出成word文档
写一个main方法用来测试。
public static void main (String[] args) {
// 模板存放路径
String templastPath = "D:/A";
// zip模板名称
String zipTemplastName = "wordTemplate.zip";
// zip模板存放路径
String zipTemplastPath = new StringBuffer(templastPath).append(File.separator).append(zipTemplastName).toString();
// docx文档模板名称
String docxTemplateName = "document.xml";
// 输出路径
String outPath = "D:/A/out";
// docx模板填充后输出路径,这里的【document.xml】不能改
String outputDocxTemplatePath = new StringBuffer(outPath).append(File.separator).append("document.xml").toString();
// 最终生成的docx文档输出路径,这里的word文档输出文件名随意
String outputDocxFilePath = new StringBuffer(outPath).append(File.separator).append("output.docx").toString();
File outPathFile = new File(outPath);
if (!outPathFile.exists() && !outPathFile.mkdirs()) {
throw new RuntimeException("输出路径创建失败");
}
try (
FileOutputStream out = new FileOutputStream(outputDocxTemplatePath);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(out);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
// zip模板压缩包输入流
FileInputStream zipTemplastInput = new FileInputStream(zipTemplastPath);
// 最终docx文档输出流
FileOutputStream finalDocxOutput = new FileOutputStream(outputDocxFilePath);
){
// 将要替换的值
Map<String, Object> map = new HashMap<String, Object>() {{
put("name", "韩西景");
put("sex", "男");
put("age", "51");
put("homeAddress", "广东省广州市白云区景山路3612号");
}};
//创建配置实例 VERSION_2_3_28是pom文件引入时的版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
//设置编码
configuration.setDefaultEncoding("UTF-8");
// 设置模板路径
configuration.setDirectoryForTemplateLoading(new File(templastPath));
// 获取xml模板
Template template = configuration.getTemplate(docxTemplateName);
// 参数值填充
template.process(map, bufferedWriter);
// 读取模板zip压缩包
ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(zipTemplastInput);
// 最终生成的word文档输出流
ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(finalDocxOutput);
// zip压缩包要替换的项
Map<String, String> replaceItemMap = new HashMap<String, String>(){{
// 替换图片,本地路径可以,网络路径不行
// put("word/media/headImage.png", "D:/A/headImage.png");
// 替换图片,base64写入
put("word/media/headImage.png", new StringBuffer("data:image/png;base64,").append(imageToBase64("D:/A/headImage.png")).toString());
// 替换内容
put("word/document.xml", outputDocxTemplatePath);
}};
ZipUtils.replaceItem(zipInputStream, zipOutputStream, replaceItemMap);
} catch (Exception e) {
System.out.println("报错了,这里自己打log啥的,该处理的处理");
} finally {
// 删除填充后xml模板
new File(outputDocxTemplatePath).delete();
}
}
/**
* 图片转base64
* @param inputPath 图片路径
* @return base64字符串
*/
private static String imageToBase64(String inputPath) {
File file = new File(inputPath);
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
try(
FileInputStream fileInputStream = new FileInputStream(inputPath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
) {
int b;
byte[] bytes = new byte[4096];
while ((b = bufferedInputStream.read(bytes)) > -1) {
byteBuffer.put(bytes, 0, b);
}
return Base64.encodeBase64String(byteBuffer.array());
} catch (Exception e) {
System.out.println("报错了,这里自己打log啥的,该处理的处理");
}
return null;
}
这是压缩包内容替换util类
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class ZipUtils {
private static final Logger log = LoggerFactory.getLogger(ZipUtils.class);
/**
* 替换某个 item,
* @param zipInputStream zip文件的zip输入流
* @param zipOutputStream 输出的zip输出流
* @param replaceItemMap 要替换的项 key为项名称,value为新文件的路径
*/
public static void replaceItem(ZipInputStream zipInputStream,
ZipOutputStream zipOutputStream,
Map<String, String> replaceItemMap
){
if(Objects.isNull(zipInputStream) || Objects.isNull(zipOutputStream) || Objects.isNull(replaceItemMap)){
return;
}
ZipEntry entryIn;
try {
// 缓冲区
byte [] buf = new byte[4096];
int len;
while((entryIn = zipInputStream.getNextEntry())!=null) {
String entryName = entryIn.getName();
ZipEntry entryOut = new ZipEntry(entryName);
// 只使用 name
zipOutputStream.putNextEntry(entryOut);
// 是否已替换
boolean replaceFlag = false;
for (Map.Entry<String, String> entry : replaceItemMap.entrySet()) {
if(entryName.equals(entry.getKey()) && StringUtils.isNotBlank(entry.getValue())){
String value = entry.getValue();
if (StringUtils.startsWith(value, "data:image/")) {
// 如果是base64格式的图片
value = value.substring(value.indexOf(";base64,") + 8);
byte[] bytes = Base64.decodeBase64(value);
zipOutputStream.write(bytes, 0, bytes.length);
} else {
InputStream itemInputStream = new FileInputStream(entry.getValue());
BufferedInputStream bufferedInputStream = new BufferedInputStream(itemInputStream);
// 使用替换流
while((len = (bufferedInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
close(bufferedInputStream);
close(itemInputStream);
}
replaceFlag = true;
break;
}
}
if (!replaceFlag) {
// 输出普通Zip流
while((len = (zipInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}
// 关闭此 entry
zipOutputStream.closeEntry();
}
} catch (IOException e) {
log.error("压缩包内容替换出错", e);
} finally {
close(zipInputStream);
close(zipOutputStream);
}
}
/**
* 包装输入流
*/
public static ZipInputStream wrapZipInputStream(InputStream inputStream){
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
return zipInputStream;
}
/**
* 包装输出流
*/
public static ZipOutputStream wrapZipOutputStream(OutputStream outputStream){
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
return zipOutputStream;
}
private static void close(InputStream inputStream){
if (null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
log.error("输入流关闭错误", e);
}
}
}
private static void close(OutputStream outputStream){
if (null != outputStream){
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("输出流关闭错误", e);
}
}
}
}
执行main方法之后,可以看到生成word文档了。
部署到服务器的话,就把模板文件放到服务器上面,然后配置模板路径,具体使用引入模板路径就行了。
至于对word文档里面的表格生成这种,暂时没有研究。
(4)Notepad++的XML Tools插件安装
仅用于格式化模板xml文件,方便调整xml文件,可装可不装,问题不大。
插件安装:顶部菜单栏【插件】->【插件管理】打开后
好了,到这结束了,真够累人,这该死的996,永无止境的打工。
码字不易,于你有利,勿忘点赞