一、需求背景
在实际工作中,我们经常需要将多个Word文档合并成一个文件。但当文档中包含批注(Comments)时,传统的复制粘贴会导致批注丢失或引用错乱。本文将介绍如何通过Java和Apache POI库实现保留批注及引用关系的文档合并功能。
二、技术选型
核心依赖:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version> <!-- 建议使用最新版本 -->
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-full</artifactId>
<version>5.3.0</version>
</dependency>
三、实现原理详解
核心思路
- 创建目标文档作为合并容器
- 遍历每个源文档的段落
- 重建批注映射关系(避免ID冲突)
- 复制段落内容并更新批注引用
- 保存合并后的文档
关键代码解析
public static void mergeDocuments(List<String> sourcePaths, String outputPath) throws Exception {
// 参数校验
if (sourcePaths == null || sourcePaths.isEmpty()) {
throw new IllegalArgumentException("sourcePaths is empty");
}
// 1. 创建目标文档
var targetDoc = new XWPFDocument();
// 创建批注容器(重要!)
var targetComments = targetDoc.createComments();
// 批注ID计数器(从0开始)
BigInteger nextCommentId = BigInteger.ZERO;
for (String sourceFile : sourcePaths) {
try (var srcDoc = new XWPFDocument(new FileInputStream(sourceFile))) {
// 2. 遍历每个段落
for (var sourcePara : srcDoc.getParagraphs()) {
var newPara = targetDoc.createParagraph();
// 3. 批注ID映射表(旧ID -> 新ID)
Map<BigInteger, BigInteger> commentIdMap = new HashMap<>();
// 4. 处理含批注引用的文本
for (var sourceRun : sourcePara.getRuns()) {
if (sourceRun.getCTR().sizeOfCommentReferenceArray() <= 0) {
continue; // 跳过无批注的文本
}
// 处理每个批注引用
for (var commentRef : sourceRun.getCTR().getCommentReferenceList()) {
// 获取源批注内容
var sourceComment = srcDoc.getCommentByID(commentRef.getId().toString());
// 在目标文档创建新批注
var targetComment = targetComments.createComment(nextCommentId);
// 复制批注内容(关键步骤!)
targetComment.getCtComment().set(sourceComment.getCtComment().copy());
// 设置新ID
targetComment.getCtComment().setId(nextCommentId);
// 保存ID映射关系
commentIdMap.put(commentRef.getId(), nextCommentId);
// ID自增(避免重复)
nextCommentId = nextCommentId.add(BigInteger.ONE);
}
}
// 5. 复制段落XML并更新批注ID
String xml = sourcePara.getCTP().xmlText();
// 替换所有批注ID引用
for (var comment : commentIdMap.entrySet()){
xml = xml.replaceAll(
"w:id=\"" + comment.getKey() + "\"",
"w:id=\"" + comment.getValue() + "\""
);
}
// 将修改后的XML载入新段落
newPara.getCTP().set(CTP.Factory.parse(xml));
}
}
}
// 6. 保存合并结果
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
targetDoc.write(fos);
}
targetDoc.close();
}
四、关键技术点
1. 批注ID重映射机制
- 问题:不同文档可能有重复的批注ID
- 解决方案:
- 创建全局计数器
nextCommentId
- 为每个批注生成新ID
- 维护
commentIdMap
映射表
- 创建全局计数器
2. XML层级操作
- 直接操作CTP对象:获取段落底层XML结构
- 正则替换:批量更新批注引用ID
xml = xml.replaceAll("w:id=\"" + oldId + "\"", "w:id=\"" + newId + "\"");
3. 内存管理
- 使用try-with-resources确保资源释放
try (var srcDoc = new XWPFDocument(new FileInputStream(sourceFile))) {
// 处理文档...
} // 自动关闭流
五、功能扩展建议
- 支持表格合并:
for (XWPFTable table : srcDoc.getTables()) {
// 复制表格到targetDoc
}
- 处理图片/图表:
for (XWPFPictureData picture : srcDoc.getAllPictures()) {
// 复制图片数据
}
- 保留格式样式:
newPara.getCTP().setPPr(sourcePara.getCTP().getPPr());
六、注意事项
- 性能优化:处理大文档时建议分块处理
- ID冲突:必须重新映射批注ID
- 格式兼容性:
- 支持wps、office。
- 支持docx格式
- 不同Word版本可能有样式差异
- 异常处理:实际生产需增加:
catch (IOException | XmlException e) { // 处理解析异常 }
七、总结
本文实现的合并方案具有以下优势:
- ✅ 完美保留批注及引用关系
- ✅ 避免ID冲突的智能映射
- ✅ 底层XML操作确保格式兼容
- ✅ 灵活的扩展性
适用场景:法律文档合并、论文修订稿整合、团队协作文档汇总等需要保留批注的场景。
技术交流:欢迎在评论区留言讨论!
附录:核心依赖说明
依赖包 | 作用 |
---|---|
poi-ooxml | 提供XWPFDocument等基础操作类 |
poi-ooxml-full | 支持完整的OOXML特性解析 |
xmlbeans | 底层XML操作依赖(自动传递) |
建议在实际使用时注意:
- 使用POI版本(本文基于5.3.0)
- 处理10MB+文档时增加JVM内存:
java -Xmx512m -jar yourApp.jar
此方案已通过以下环境验证:
- Java 11+
- Apache POI 5.3.0
- Microsoft Word 2016/365