前言
正常业务中,可能涉及到和合作方签约电子合同,此时,我们需要先设计合同模板,维护固定内容,将可变的内容通过占位符替代,等签章的时候,生成pdf,然后可以根据设计的合同章的坐标,调用签章系统盖电子章。
此时涉及三步:
- 读取合同word模板,替换占位符,比如签约人,合同金额,签约时间等
- 将word文档转换为pdf文件
- 上传pdf文件到电子签章系统签章
这时候就需要我们掌握java代码读写word文档的技能。
1. 前期准备与说明
1.1 doc文件后缀与docx文件后缀区别
- 运行环境不同,docx格式的文件是Office2007及以上版本保存的新型文档,而doc是Word2003以及之前版本保存的文档。
- 它们所占用的内存空间不同,docx更加节省空间。
- 它们的响应速度有所不同,docx比doc的响应速度更加快捷,并且更加方便修改文件。
- docx格式的文件本质上是一个ZIP文件,是docx文件的容器。而doc则容纳文字格式、脚本语言及复原等资讯的文件。
- docx文件改后缀为zip文件,解压后可以看到如下所示:
其中word里面是很多xml文件:
因为doc文件版本比较老,现在主流都用的是docx文件,下面示例统一以docx文件为演示格式。
1.2 测试合同docx文件准备
假设合同模板内容如下,可变的信息我们通过${变量名}代替,后续真实业务中,基于不同的用户和合作方,替换文档内容。
合同模板我上传到csdn里面了下载资源
2 POI 处理文档
2.1 介绍
POI可以灵活操作基于word文档。Apache POl中的HWPF具体提供了读写MicrosoftWord格式文件的功能。
优点:跨平台支持windows unix和linux
缺点:操作word的功能比较弱,必须针对doc和docx两种文档格式写不同的代码,兼容性差。相对与对word文件的处理来说,POI更适合excel处理,对于word实现一些简单文件的操作凑合,不能设置样式且生成的word文件格式不够规范。
综合来说:但是如果只是基于合同模板生成word稳定,不手动拼接word,则POI就已经能满足我们的业务需求。具体选哪种方案,视具体业务而定
2.2 pom坐标引入
<!--解析docx文档的XWPFDocument对象在这个包里-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
<!--解析doc文档的HWPFDocument对象在这个包里,操作docx文件可不要这个包,这里只是一起记录一下-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.9</version>
</dependency>
<!--lombok组件,可以不接入,这里方便测试-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
2.3 使用
2.3.1 常用api
XWPFDocument类(文档类):
xd.getAllPictures(); 返回图片列表
xd.getParagraphs(); 返回段落列表
xd.getTables(); 返回表格列表
xd.getFooterList(); 返回脚脚列表
xd.getFootnotes(); 返回脚注列表
xd.getHeaderList(); 返回页眉列表
XWPFParagraph类(段落类):
xwpfParagraph.getText(); 返回段落的文本内容,包括图片中的文本和其中的sdt元素。
xwpfParagraph.getParagraphText(); 返回段落的文本,但不返回段落中的任何对象
xwpfParagraph.getRuns()); 返回段落run的列表
XWPFTable类(表格类):
xwpfTable.getRows(); 返回行对象XWPFTableRow
XWPFTableRow类(表格行类):
xwpfTableRow.getTableCells(); 返回单元格对象XWPFTableCell
xwpfTableCell.getParagraphs(); 返回单元格里面的段落
xwpfTableCell.getTables(); 返回单元格里面的表格
2.3.2 测试案例
测试demo:
@Slf4j
public class DocXUtils {
public static void main(String[] args) {
String filePath = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板.docx";
String suffix = filePath.substring(filePath.lastIndexOf("."));
if(".docx".equals(suffix)){
System.out.println("文件类型是.docx");
dealDocXFile(filePath);
}
}
public static void dealDocXFile(String filePath){
InputStream input = null;
try {
//实例化解析docx文档的对象
input = new FileInputStream(filePath);
XWPFDocument xd = new XWPFDocument(input);
log.info("开始解析文档中的所有图片----------------");
//1 getAllPackagePictures()此包装中的所有图片
List<XWPFPictureData> xwpfPictureDataList2 = xd.getAllPictures();
if(CollUtil.isNotEmpty(xwpfPictureDataList2)){
for (XWPFPictureData xwpfPictureData : xwpfPictureDataList2) {
//图像的文件名
System.out.println("图片名称:" + xwpfPictureData.getFileName());
}
}
log.info("开始解析文档中的所有段落----------------");
//文本的段落
List<XWPFParagraph> xwpfParagraphList = xd.getParagraphs();
dealParagraph(xwpfParagraphList);
log.info("开始解析文档中的所有表格----------------");
//文本的表格
List<XWPFTable> xwpfTableList = xd.getTables();
dealTable(xwpfTableList);
log.info("开始解析文档中的页脚----------------");
//页脚列表
List <XWPFFooter> xwpfFooterList = xd.getFooterList();
//System.out.println("页脚列表size:"+xwpfFooterList.size());
if(CollUtil.isNotEmpty(xwpfFooterList)){
for (XWPFFooter xwpfFooter : xwpfFooterList) {
//页脚的文本的段落
dealParagraph(xwpfFooter.getParagraphs());
dealTable(xwpfFooter.getTables());
System.out.println("getText:"+xwpfFooter.getText());
}
}
log.info("开始解析文档中的脚注----------------");
//脚注列表
List <XWPFFootnote> xwpfFootnoteList = xd.getFootnotes();
for(XWPFFootnote xwpfFootnote : xwpfFootnoteList){
//处理脚注段落
dealParagraph(xwpfFootnote.getParagraphs());
dealTable(xwpfFootnote.getTables());
}
log.info("开始解析文档中的页眉----------------");
//页眉列表
List <XWPFHeader> xwpfHeaderList = xd.getHeaderList();
if(CollUtil.isNotEmpty(xwpfHeaderList)){
for(XWPFHeader xwpfHeader : xwpfHeaderList){
dealParagraph(xwpfHeader.getParagraphs());
dealTable(xwpfHeader.getTables());
System.out.println(xwpfHeader.getText());
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件没有找到");
} catch (IOException e) {
e.printStackTrace();
System.out.println("发生io异常");
}
}
public static void dealParagraph(List<XWPFParagraph> xwpfParagraphListList){
if(CollUtil.isNotEmpty(xwpfParagraphListList)){
for(XWPFParagraph xwpfParagraph : xwpfParagraphListList){
if(ObjectUtil.isNull(xwpfParagraph) || StrUtil.isEmpty(xwpfParagraph.getText())){
continue;
}
System.out.println("______________________________________________________________");
//返回段落的文本内容,包括图片中的文本和其中的sdt元素。String
System.out.println("getText~~~~~~~~~~~:"+xwpfParagraph.getText());
//返回段落的文本,但不返回段落中的任何对象.String
System.out.println("getParagraphText:~~"+xwpfParagraph.getParagraphText());
System.out.println("getRuns~~~~~~~~~~~:"+xwpfParagraph.getRuns());
}
}
}
public static void dealTable(List<XWPFTable> xwpfTableList){
if(CollUtil.isNotEmpty(xwpfTableList)){
for(XWPFTable xwpfTable : xwpfTableList){
//遍历行
for(XWPFTableRow xwpfTableRow : xwpfTable.getRows()){
//遍历单元格
for(XWPFTableCell xwpfTableCell : xwpfTableRow.getTableCells()){
//处理段落
dealParagraph(xwpfTableCell.getParagraphs());
dealTable(xwpfTableCell.getTables());
}
}
}
}
}
}
代码输出结果:
文件类型是.docx
2023-04-25 17:01:49.282 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的所有图片----------------
2023-04-25 17:01:49.282 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的所有段落----------------
______________________________________________________________
getText~~~~~~~~~~~:编号:${CONTRACT_NO}
getParagraphText:~~编号:${CONTRACT_NO}
getRuns~~~~~~~~~~~:[编号:, ${CONTRACT_NO}]
______________________________________________________________
getText~~~~~~~~~~~:${PRODUCT_NAME}产品开通申请
getParagraphText:~~${PRODUCT_NAME}产品开通申请
getRuns~~~~~~~~~~~:[${PRODUCT_NAME}, 产品开通申请]
______________________________________________________________
getText~~~~~~~~~~~:特别提示:本申请项下的以下${PRODUCT_NAME}产品服务(以下简称“本服务”),是由XXX为YYY所提供的服务
getParagraphText:~~特别提示:本申请项下的以下${PRODUCT_NAME}产品服务(以下简称“本服务”),是由XXX为YYY所提供的服务
getRuns~~~~~~~~~~~:[特别提示:, 本, 申请, 项下的以下, ${PRODUCT_NAME}, 产品服务, (以下简称“本服务”), ,是, 由, XXX为YYY, 所提供, 的服务]
______________________________________________________________
getText~~~~~~~~~~~:甲方:
getParagraphText:~~甲方:
getRuns~~~~~~~~~~~:[甲方, : ]
______________________________________________________________
getText~~~~~~~~~~~:用户姓名:${PERSONAL_NAME}
getParagraphText:~~用户姓名:${PERSONAL_NAME}
getRuns~~~~~~~~~~~:[用户姓名:, ${PERSONAL_NAME}]
______________________________________________________________
getText~~~~~~~~~~~:证件类型:${PERSONAL_ID_TYPE}
getParagraphText:~~证件类型:${PERSONAL_ID_TYPE}
getRuns~~~~~~~~~~~:[证件类型:, ${PERSONAL_ID_TYPE}]
______________________________________________________________
getText~~~~~~~~~~~:证件号码:${PERSONAL_ID_NO}
getParagraphText:~~证件号码:${PERSONAL_ID_NO}
getRuns~~~~~~~~~~~:[证件号码:, ${PERSONAL_ID_NO}]
______________________________________________________________
getText~~~~~~~~~~~:乙方:
getParagraphText:~~乙方:
getRuns~~~~~~~~~~~:[乙方, : ]
______________________________________________________________
getText~~~~~~~~~~~:公司名称:${ENT_NAME}
getParagraphText:~~公司名称:${ENT_NAME}
getRuns~~~~~~~~~~~:[公司名称, :, ${ENT_NAME}]
______________________________________________________________
getText~~~~~~~~~~~:统一社会信用代码:${ENT_ID_NO}
getParagraphText:~~统一社会信用代码:${ENT_ID_NO}
getRuns~~~~~~~~~~~:[统一社会信用代码, :, ${ENT_ID_NO}]
______________________________________________________________
getText~~~~~~~~~~~:甲方:${SIGNER_JIAFANG}
getParagraphText:~~甲方:${SIGNER_JIAFANG}
getRuns~~~~~~~~~~~:[甲方, :, ${SIGNER_JIAFANG}]
______________________________________________________________
getText~~~~~~~~~~~:乙方:${SIGNER_YIFANG}}
getParagraphText:~~乙方:${SIGNER_YIFANG}}
getRuns~~~~~~~~~~~:[乙方, :, ${SIGNER_, YI, FANG, }, }]
2023-04-25 17:01:49.317 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的所有表格----------------
2023-04-25 17:01:49.317 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的页脚----------------
2023-04-25 17:01:49.317 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的脚注----------------
2023-04-25 17:01:49.317 [main] INFO com.wanlong.word.DocXUtils - 开始解析文档中的页眉----------------
上面注意一个细节:
- 合同模板甲乙方下面是留着签章的,但是为了签章能定位到具体的坐标位置,所以有两个隐藏的占位符
- 不同poi版本的jar包会有一些类不存在的问题,为了后面可以使用word转pdf,建议poi的版本不要太高
2.4 使用场景
2.4.1 按照模板替换报文
模板规则约定如下:
- 占位符统一用${}包含,比如name,则文档占位符为 ${name}
- 签章位置加一个隐藏的占位符,肉眼看不到,但是实际模板有隐藏的字体,这里通过设置字体为白色隐藏,但是代码可以正常读取,主要用于后续签章好锁定坐标位置
- 签章占位符统一约定占位符为${sign}开头,比如甲方签章,则可以为 ${sign_jiafang}
2.4.1.1 校验占位符工具类
package com.wanlong.word;
import org.apache.poi.xwpf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 校验word占位符
*/
public class CheckWordElementValue {
private final static Logger logger = LoggerFactory.getLogger(CheckWordElementValue.class);
public static List<String> getContractTemplateKey(InputStream is){
logger.info("=== 校验word占位符--进入解析word文档方法 ===");
List<String> list = new ArrayList<String>();
try {
logger.info("=== 校验word占位符--解析word文档 ===");
XWPFDocument document = new XWPFDocument(is);
//获取文本对象
logger.info("=== 校验word占位符--获取文本对象 ===");
List<XWPFParagraph> allParagraph = document.getParagraphs();
logger.info("=== 校验word占位符--调用convertStr方法 ===");
convertStr(allParagraph, list);
logger.info("=== 校验word占位符--获取全部表格对象 ===");
//获取全部表格对象
List<XWPFTable> allTable = document.getTables();
for (XWPFTable xwpfTable : allTable) {
logger.info("=== 校验word占位符--获取表格行数据 ===");
//获取表格行数据
List<XWPFTableRow> rows = xwpfTable.getRows();
for (XWPFTableRow xwpfTableRow : rows) {
//获取表格单元格数据
logger.info("=== 校验word占位符--获取表格单元格数据 ===");
List<XWPFTableCell> cells = xwpfTableRow.getTableCells();
for (XWPFTableCell xwpfTableCell : cells) {
List<XWPFParagraph> paragraphs = xwpfTableCell.getParagraphs();
convertStr(paragraphs, list);
}
}
}
} catch (IOException e) {
logger.error("---=== 校验word是否还存在占位符时'文档解析异常' ===---");
}
return list;
}
public static void convertStr(List<XWPFParagraph> allParagraph, List<String> list) {
for(XWPFParagraph xwpfParagraph : allParagraph) {
String str = xwpfParagraph.getRuns().toString().replaceAll(", ", "");
Matcher matcher = matcher(str);
while(matcher.find()) {
list.add(matcher.group(1).trim());
}
}
}
public static Matcher matcher(String str) {
Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(str);
return matcher;
}
}
2.4.1.2. word文档替换占位符工具类
package com.wanlong.word;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.POIXMLDocument;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 通过word模板生成新的word工具类
*/
public class WorderToNewWordUtils {
private final static Logger logger = LoggerFactory.getLogger(WorderToNewWordUtils.class);
public static void changeText(XWPFDocument document, Map<String, Object> textMap) {
logger.info("=== 进入文本段落方法 === ");
// 获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
logger.info("=== 开始遍历获取paragraph === ");
// 判断此段落时候需要进行替换
String text = paragraph.getText();
logger.info("=== 获取paragraphs文本段落内容 === : " + text);
if (checkText(text)) {
List<XWPFRun> runs = paragraph.getRuns();
logger.info("=== 获取paragraph.getRuns()列表里面单个runs元素内容 === ");
for (XWPFRun run : runs) {
logger.info("=== 遍历XWPFRun内容=== :" + run.toString());
if (checkUserCode(run.toString())) {
logger.info("=== 进入签署人列表占位符替换方法 === :" + run.toString());
if (!run.toString().equals("${SIGN_TIME}")) {
CTRPr rpr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr();
logger.info("=== 判断占位符内容是否有字体颜色 === :" + rpr.getColor());
if (rpr.getColor() != null) {
rpr.unsetColor();
}
run.setColor("FFFFFF");
run.setText(changeUserCodeValue(run.toString(), textMap), 0);
}
} else {
logger.info("=== 普通占位符替换方法 === :" + run.toString());
run.setText(changeValue(run.toString(), textMap), 0);
}
}
}
}
}
public static void changeTable(XWPFDocument document, Map<String, Object> textMap) {
logger.info("=== changeTable方法,判断table内容是替换还是动态生成 ===");
List<XWPFTable> tables = document.getTables();
for (int i = 0; i < tables.size(); i++) {
XWPFTable table = tables.get(i);
if (table.getRows().size() > 1) {
logger.info("=== 校验table内容是否含有占位符标识===");
if (checkText(table.getText())) {
List<XWPFTableRow> rows = table.getRows();
String text = table.getRow(1).getCell(0).getText();
logger.info("=== 获取table占位符标识=== :" + text);
if (checkText(text)) {
logger.info("=== table内容动态生成 ===");
insertTable(table, textMap);
} else {
logger.info("=== table内容替换 ===");
eachTable(rows, textMap);
}
}
}
if (table.getRows().size() <= 1) {
logger.info("=== table.getRows().size() <= 1 table内容替换 ===");
if (checkText(table.getText())) {
List<XWPFTableRow> rows = table.getRows();
eachTable(rows, textMap);
}
}
}
}
public static void insertTable(XWPFTable table, Map<String, Object> textMap) {
logger.info("=== insertTable方法,创建table内容 ===");
String value = null;
String text = table.getRow(1).getCell(0).getText();
logger.info("=== 获取动态生成table占位符 === :" + text);
Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
logger.info("=== 获取动态生成table占位符去匹配替换内容内容 ===");
for (Map.Entry<String, Object> textSet : textSets) {
String key = "${" + textSet.getKey() + "}";
if (text.indexOf(key) != -1) {
value = (String) textSet.getValue();
}
}
if (StringUtils.isNotEmpty(value)) {
table.getRow(1).getCell(0).removeParagraph(0);
table.getRow(1).getCell(0).setText("");
List<String[]> tableValue = getListValue(value);
if (tableValue.size() >= 1) {
for (int i = 1; i < tableValue.size(); i++) {
XWPFTableRow row = table.createRow();
}
List<XWPFTableRow> tableRow = table.getRows();
logger.info("=== 校验动态生成table头的单元格格式是否与替换内容格式符合 ===");
if (tableRow.get(0).getTableCells().size() == tableValue.get(0).length) {
for (int i = 1; i < tableRow.size(); i++) {
logger.info("=== 校验动态生成table单元格数据大小是否与替换内容数据大小符合 ===");
logger.info("=== for i === :" + i);
logger.info("=== for tableValue.size() === : " + tableValue.size());
if (i <= tableValue.size()) {
XWPFTableRow newRow = table.getRow(i);
List<XWPFTableCell> cells = newRow.getTableCells();
try {
for (int j = 0; j < cells.size(); j++) {
logger.info("=== 遍历添加table单元格内容 ===");
XWPFTableCell cell = cells.get(j);
cell.setText(tableValue.get(i - 1)[j]);
CTTc cttc = cell.getCTTc();
CTP ctp = cttc.getPList().get(0);
CTPPr ctppr = ctp.getPPr();
if (ctppr == null) {
ctppr = ctp.addNewPPr();
}
CTJc ctjc = ctppr.getJc();
if (ctjc == null) {
ctjc = ctppr.addNewJc();
}
ctjc.setVal(STJc.LEFT); //水平向左
}
} catch (Exception e) {
logger.error("---=== 动态插入table内容错误 ===---", e);
throw new RuntimeException(e);
}
}
}
}
}
}
}
public static void eachTable(List<XWPFTableRow> rows, Map<String, Object> textMap) {
logger.info("=== 进入表格替换方法 ===");
for (XWPFTableRow row : rows) {
List<XWPFTableCell> cells = row.getTableCells();
for (XWPFTableCell cell : cells) {
//判断单元格是否需要替换
logger.info("=== 判断表格替换单元格是否需要替换 ===");
logger.info("=== 表格替换内容 === : " + cell.getText());
if (checkText(cell.getText())) {
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
logger.info("=== 表格替换占位符 === :" + cell.getText());
run.setText(changeValue(run.toString(), textMap), 0);
}
}
}
}
}
}
public static List<String[]> getListValue(String value) {
logger.info("=== 进入getListValue方法,解析table内容 ===");
List<String[]> listValue = new ArrayList<String[]>();
try {
if (StringUtils.isNotBlank(value)) {
String[] splitValue = value.split("\\|");
try {
for (int i = 0; i < splitValue.length; i++) {
String[] splitLaterValue = splitValue[i].split(",", -1);
listValue.add(splitLaterValue);
}
} catch (Exception e) {
logger.error("---=== 获取table内容,截取内容时出现错误 ===---", e);
throw new RuntimeException(e);
}
}
} catch (Exception e) {
logger.error("---=== 获取table内容,截取内容时出现错误 ===---", e);
throw new RuntimeException(e);
}
return listValue;
}
public static boolean checkText(String text) {
boolean check = false;
if (text.indexOf("$") != -1) {
check = true;
}
return check;
}
public static String changeValue(String value, Map<String, Object> textMap) {
try {
Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
for (Map.Entry<String, Object> textSet : textSets) {
String key = "${" + textSet.getKey() + "}";
if (value.indexOf(key) != -1) {
value = (String) textSet.getValue();
}
}
} catch (Exception e) {
logger.error("---=== 比较${}格式与替换内容是否匹配错误 ===---", e);
throw new RuntimeException();
}
return value;
}
public static String changeUserCodeValue(String value, Map<String, Object> textMap) {
try {
Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
for (Map.Entry<String, Object> textSet : textSets) {
// 匹配模板与替换值 格式# 预计
String key = "${" + textSet.getKey() + "}";
if (value.indexOf(key) != -1) {
value = (String) textSet.getValue();
}
}
} catch (Exception e) {
logger.error("---=== 比较${}格式与替换内容是否匹配错误 ===---", e);
throw new RuntimeException(e);
}
return value;
}
public static boolean checkUserCode(String text) {
boolean check = false;
if (text.indexOf("${SIGNER_") != -1) {
check = true;
}
return check;
}
public static boolean changWord(String inputUrl, String outputUrl,
Map<String, Object> textMap, List<String[]> tableList) {
//模板转换默认成功
boolean changeFlag = true;
try {
//获取docx解析对象
XWPFDocument document = new XWPFDocument(POIXMLDocument.openPackage(inputUrl));
//解析替换文本段落对象
WorderToNewWordUtils.changeText(document, textMap);
//解析替换表格对象
WorderToNewWordUtils.changeTable(document, textMap);
//生成新的word
File file = new File(outputUrl);
FileOutputStream stream = new FileOutputStream(file);
document.write(stream);
stream.close();
} catch (IOException e) {
e.printStackTrace();
changeFlag = false;
}
return changeFlag;
}
}
2.4.1.3 word转pdf测试类
@Slf4j
public class TestWord {
@Test
public void testWord() throws Exception{
String path="E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\";
//维护待替换的参数和值
Map<String,Object> map=new HashMap<>();
map.put("PRODUCT_NAME","余额宝");
map.put("PERSONAL_NAME","张三");
map.put("PERSONAL_ID_TYPE","身份证");
map.put("PERSONAL_ID_NO","620234199902122302X");
map.put("CONTRACT_NO","hetong123456");
map.put("ENT_NAME","支付宝测试有限公司");
map.put("ENT_ID_NO","90232340341234");
log.info("=== 解析word文档 ===");
XWPFDocument document = new XWPFDocument(new FileInputStream("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板.docx"));
//解析替换文本对象
log.info("=== 解析替换文本对象 ===");
WorderToNewWordUtils.changeText(document, map);
//解析替换表格对象
log.info("=== 解析替换表格对象 ===");
WorderToNewWordUtils.changeTable(document, map);
log.info("=== 生成word ===");
InputStream foundWordInputStream = foundWord(document);
log.info("=== 校验生成的word是否含有占位符 ===");
Boolean isContainPlaceholder = checkCreateWord(foundWordInputStream);
if (isContainPlaceholder == true) {
throw new Exception("文档没替换完毕");
} else {
log.info("=== 调生成word文档方法 ===");
document.write(new FileOutputStream(path + "result.docx"));
}
}
2.4.1.4. 生成替换后的合同如下
2.4.2 word转pdf
2.4.2.1 添加依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
<version>1.0.4</version>
</dependency>
<!-- 工具类用到apache的类,可以重新替换-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
2.4.2.2 测试类
@Test
public void testWordToPdf() throws Exception {
String in = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\result.docx";
FileInputStream inputStream = new FileInputStream(in);
XWPFDocument document = new XWPFDocument(inputStream);
String out = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\test.pdf";
FileOutputStream outputStream = new FileOutputStream(out);
PdfOptions pdfOptions = PdfOptions.create();
PdfConverter.getInstance().convert(document, outputStream, pdfOptions);
}
2.4.2.3 转换结果
2.5 注意事项
- 在代码解析word文档的时候,可能会出现某些占位字符被分割解析,比如上面占位符的PERSONAL_ID_NO 变量,POI可能会切割为三个字符,PERSONAL ,ID,NO处理,此时替换的时候,会出现无法替换。此时建议调整模板的格式,多次尝试,直到能正常解析(可以尝试将完整字符放到记事本里,修改完粘贴过来)。因为模板调整完一般就不会变了,不建议代码做兼容处理。
- POI的版本和POIConverter的POM版本会有关联关系,上面的版本号是经过测试的,版本不匹配会报错类不存在
3 POI-TL
3.1 介绍
poi-tl(poi template language)是Word模板引擎,使用Word模板和数据创建很棒的Word文档。
3.2 pom坐标引入
<!-- word模板引擎-->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.8.2</version>
</dependency>
3.3 使用
3.3.1 合同模板
3.3.2 测试代码
package com.wanlong.word;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.util.BytePictureUtils;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* @author wanlong
* @version 1.0
* @description:
* @date 2023/4/25 10:11
*/
public class TestTLWord {
@Test
public void testCreate() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("PRODUCT_NAME", "余额宝");
map.put("PERSONAL_NAME", "张三");
map.put("PERSONAL_ID_TYPE", "身份证");
map.put("PERSONAL_ID_NO", "620234199902122302X");
map.put("CONTRACT_NO", "hetong123456");
map.put("ENT_NAME", "支付宝测试有限公司");
map.put("ENT_ID_NO", "90232340341234");
// 读取本地磁盘图片
map.put("QIANZHANG_JIAFANG", new PictureRenderData(100, 100, "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\picture\\logo.jpeg"));
map.put("QIANZHANG_YIFANG", new PictureRenderData(100, 100, "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\picture\\logo.jpeg"));
// // 通过url读取网络图片
// map.put("picture", new PictureRenderData(200, 400, ".png", BytePictureUtils.getUrlByteArray("https://res.wx.qq.com/a/wx_fed/weixin_portal/res/static/img/1EtCRvm.png")));
File file = new File("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板2.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);
FileOutputStream out = new FileOutputStream(new File("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\testtl.docx"));
template.write(out);
out.flush();
out.close();
template.close();
}
}
3.3.3 运行结果
3.3.4 word转pdf
这里要留意,如果用了POI-TL的jar做模板替换,因为这个jar依赖了OOXML的高版本jar包,所以转换PDF不能用上面的converterjar包,否则两个jar包都对ooxml有依赖,但是版本不一样,会有问题。
笔者尝试了好几个版本都不太行,最后折中换了一个其他依赖,实现word转PDF.
3.3.4.1 添加依赖
<dependency>
<groupId>com.documents4j</groupId>
<artifactId>documents4j-local</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>com.documents4j</groupId>
<artifactId>documents4j-transformer-msoffice-word</artifactId>
<version>1.0.3</version>
</dependency>
3.3.4.2 测试类
@Test
public void testWordToPdf() throws Exception {
String in = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\testtl.docx";
String out = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\test3.pdf";
InputStream docxInputStream = null;
OutputStream outputStream = null;
IConverter converter=null;
try {
docxInputStream = new FileInputStream(new File(in));
outputStream = new FileOutputStream(new File(out));
converter = LocalConverter.builder().build();
converter.convert(docxInputStream)
.as(DocumentType.DOCX)
.to(outputStream)
.as(DocumentType.PDF)
.execute();
} catch (Exception e) {
log.error("word转pdf异常",e);
e.printStackTrace();
} finally {
outputStream.close();
docxInputStream.close();
converter.kill();
}
}
3.3.4.3 运行结果
PDF正常生成
3.4 注意事项
- 项目里面如果还依赖oomxml版本的话,要留意版本不要冲突,否则会有奇怪的报错,这里可以看到,只需要引用一个依赖,即可完成word文本占位符替换。POI-Tl包本身会有ooxml的一些引用
- 可以看到POI-TL在模板操作替换方面,使用极其简单,这个jar包初衷就是模板操作word
- 更多关于POI-TL的使用,参考文末的文档网站
- 如果有需求在一个项目中既实现word模板替换生成新的word文档,又想将word转换为PDF,笔者个人建议选择第一种方案,第二种方案相对来说没有经过我完整的生产验证,第一种是经过我们验证的。
4 其他类似类库简介
4.1 FreeMarker
FreeMarker生成word文档的功能是由XML+FreeMarker来实现的。先把word文件另存为xml,在xml文件中插入特殊的字符串占位符,将xml翻译为FreeMarker模板,最后用Java来解析FreeMarker模板,编码调用FreeMarker实现文本替换并输出Doc。
优点:比Java2word功能强大,代码相对简单,跨平台,也是纯Java编程。
缺点:模板制作复杂,生成的文件本质上是xml,不是真正的word文件格式,有很多常用的word格式无法处理或表现怪异,比如:超链、换行、乱码、部分生成的文件打不开等。
4.2 Java2word
Java2word是一个在Java程序中调用MS Office Word文档的组件(类库)。
该组件提供了一组简单的接口,以便Java程序调用他的服务操作Word文档。
这些服务包括:打开文档、新建文档、查找文字、替换文字,插入文字、插入图片、插入表格,在书签处插入文字、插入图片、插入表格等。
优点:足够简单,操作起来要比FreeMarker简单的多。
缺点:没有FreeMarker强大,不能够根据模版生成Word文档,word的文档的样式等信息都不能够很好的操作。
参考文档:
http://deepoove.com/poi-tl/
https://www.freesion.com/article/22151181181/