使用 Apache POI 实现 Java Word 模板占位符替换功能的方法和一些坑 使用 Apache POI 实现 Java Word 模板占位符替换功能的方法和一些坑
使用 Apache POI 实现 Java Word 模板占位符替换功能的方法和一些坑
文章目录
使用场景
在日常开发中,我们经常会遇到生成 [Word 文档](https://so.csdn.net/so/search?q=Word 文档&spm=1001.2101.3001.7020)的需求,特别是在需要从模板导出 Word 文件时,比如生成合同、报告等。通过使用模板,开发者可以减少重复的工作,将预定义的占位符替换为实际的数据,生成定制化的 Word 文件。本文将介绍如何使用 Apache POI 库实现 Java 程序中的 Word 模板占位符替换功能,并最终导出定制化的 Word 文件。
开始使用
合同模板示例
前端(vue)数据示例
contractData: {
mcc:'中文名',
mce:'yingwen',
jcc:'中简',
jce:'yingjian',
dz:'地址',
llr:'联络人甲方',
zw:'植物',
wz:'网址',
dh:'电话',
dy:'电邮甲方',
llr2:'联络人乙方',
sj:'手机',
dy2:'电邮乙方'
}
后端代码
后端maven中pom.xml配置,引入依赖
org.apache.poi
poi-ooxml
5.2.3
org.apache.poi
poi
5.2.3
资源文件编译pom.xml配置
src/main/resources
false
**/*.docx
如果资源文件不是从resources中获取可以不用管这一块。
读取并遍历文件
import com.cust.trafficsupervise.common.utils.DataResult;
import org.apache.poi.xwpf.usermodel.*;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
@RestController
@RequestMapping("/gethetong")
public class hetong {
@PostMapping("/generateContract")
//DataResult是封装好的返回结果类,根据实际业务更改对应的返回结果的封装类或者直接删除
//contractData是前端传过来的数据,最好封装为实体接收。
//业务逻辑应在service层完成,此处为了简便直接编写在controller层。
public DataResult generateContract(@RequestBody Map contractData) {
try {
// 加载资源文件夹resources/tem文件夹下的合同模板
//不在resources获取模板文件需要修改此处,通过这个方法只能获取resources中的资源文件
InputStream templateInputStream = getClass().getClassLoader().getResourceAsStream("tem/x.docx");
if (templateInputStream == null){
return DataResult.error();
}
XWPFDocument document = new XWPFDocument(templateInputStream);
// 遍历文档中的段落
for (XWPFParagraph paragraph : document.getParagraphs()) {
if (paragraph == null || paragraph.getRuns().size() == 0){
continue;
}
//替换占位符
change(paragraph,contractData);
}
// 遍历文档中的表格
for (XWPFTable table : document.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
// 遍历单元格中的段落
for (XWPFParagraph cellParagraph : cell.getParagraphs()) {
//替换占位符
change(cellParagraph,contractData);
}
}
}
}
// 遍历文档中的图片
for (XWPFPictureData picture : document.getAllPictures()) {
// 图片不会被修改,直接跳过
//需要修改图片在此处
System.out.println("图片: " + picture.getFileName());
}
//该路径为相对路径,默认创建保存位置为项目文件所在盘符的根目录下
String path = "/file";
// 保存填充后的合同(将毫秒数添加到文件名防止命名冲突)
String filePath = path + "/contract_" + System.currentTimeMillis() + ".docx";
//检查目录是否存在,不存在则创建
File directory = new File(path);
if (!directory.exists()) {
boolean created = directory.mkdirs();
if (!created) {
throw new IOException("创建目录失败: " + path);
}
}
// 使用 FileOutputStream 保存填充后的合同
try (FileOutputStream out = new FileOutputStream(filePath)) {
document.write(out); // 将内容写入文件
}
// 返回合同文件路径,将保存的文件路径传给前端即可,前端拿到路径下载文件。
return DataResult.success(filePath);
} catch (Exception e) {
e.printStackTrace();
return DataResult.error("错误");
}
}
替换占位符工具类(change方法)
//替换占位符方法
public static void change(XWPFParagraph paragraph,Map contractData){
for (XWPFRun run : paragraph.getRuns()) {
// 获取当前 run 的文本
String runText = run.getText(0);
if (runText == null){
continue;
}
StringBuilder newText = new StringBuilder(runText);
// 替换占位符
for (Map.Entry entry : contractData.entrySet()) {
//此处的占位符“{{”和“}}”可以任意更改,合同模板文件随着替换即可。
String placeholder = "{{" + entry.getKey() + "}}";
int startIndex = newText.indexOf(placeholder);
while (startIndex != -1) {
newText.replace(startIndex, startIndex + placeholder.length(), entry.getValue());
startIndex = newText.indexOf(placeholder, startIndex + entry.getValue().length());
}
}
// 更新替换后的文本
run.setText(newText.toString(), 0);
}
}
问题,坑
文档中不同内容格式的处理
文档中段落、表格、图片等文字格式都是需要单独处理的,通过不同的对象接收,如果不进行接收处理则会丢失。导致文档不一致且格式发生变化。
POI版本和第三方软件导致的run划分问题
在POI3.9版本之前一个段落就是一个run,3.10之后获取到的一个段落会被不知什么规则分成一个List数组中有多个run,导致目标表占位符被分割
例如:合同名称(中文):{{mcc}} 被分割成:合同名称(中文):{{ 、mcc}},如果对于每个run进行查找替换就会导致识别不到预设占位符**{{mcc}}**
解决方法
添加占位符时
因为合同模板一旦确认一般情况下不会更改和变动,所以在对合同模板添加占位符时候,将占位符整个( 例**{{mcc}}** )一口气输入完后保存(未保存可退格)。
如果中途保存,例如:输入 {{mcc 后保存,继续输入 }} 则会出现被划分到多个run中的情况。
如果输入错误保存后重新修改,例如:输入 {{mcc}$ 保存后发现输入错误,重新修改需要整个删除重新输入,或者修改成 {{mcc}} 后ctrl+x剪切重新整体粘贴。
将run合并
将分成多个run的List重新合并在一起,removeRun貌似可以,但是不确定格式是否会丢失,没进行尝试,可以参考文章:
POI不同版本替换Word模板时的问题 - yujj_cn - 博客园