JAVA 使用Apache POI合并Word文档并保留批注的实现

一、需求背景

在实际工作中,我们经常需要将多个Word文档合并成一个文件。但当文档中包含批注(Comments)时,传统的复制粘贴会导致批注丢失或引用错乱。本文将介绍如何通过Java和Apache POI库实现保留批注及引用关系的文档合并功能。

二、技术选型

核心依赖

xml 复制代码
<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>

三、实现原理详解

核心思路

  1. 创建目标文档作为合并容器
  2. 遍历每个源文档的段落
  3. 重建批注映射关系(避免ID冲突)
  4. 复制段落内容并更新批注引用
  5. 保存合并后的文档

关键代码解析

java 复制代码
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
java 复制代码
xml = xml.replaceAll("w:id=\"" + oldId + "\"", "w:id=\"" + newId + "\"");

3. 内存管理

  • 使用try-with-resources确保资源释放
java 复制代码
try (var srcDoc = new XWPFDocument(new FileInputStream(sourceFile))) {
    // 处理文档...
} // 自动关闭流

五、功能扩展建议

  1. 支持表格合并
java 复制代码
for (XWPFTable table : srcDoc.getTables()) {
    // 复制表格到targetDoc
}
  1. 处理图片/图表
java 复制代码
for (XWPFPictureData picture : srcDoc.getAllPictures()) {
    // 复制图片数据
}
  1. 保留格式样式
java 复制代码
newPara.getCTP().setPPr(sourcePara.getCTP().getPPr());

六、注意事项

  1. 性能优化:处理大文档时建议分块处理

  2. ID冲突:必须重新映射批注ID

  3. 格式兼容性

    • 支持wps、office。
    • 支持docx格式
    • 不同Word版本可能有样式差异
  4. 异常处理 :实际生产需增加:

    java 复制代码
    catch (IOException | XmlException e) {
        // 处理解析异常
    }

七、总结

本文实现的合并方案具有以下优势:

  • ✅ 完美保留批注及引用关系
  • ✅ 避免ID冲突的智能映射
  • ✅ 底层XML操作确保格式兼容
  • ✅ 灵活的扩展性

适用场景:法律文档合并、论文修订稿整合、团队协作文档汇总等需要保留批注的场景。

技术交流:欢迎在评论区留言讨论!


附录:核心依赖说明

依赖包 作用
poi-ooxml 提供XWPFDocument等基础操作类
poi-ooxml-full 支持完整的OOXML特性解析
xmlbeans 底层XML操作依赖(自动传递)

建议在实际使用时注意:

  1. 使用POI版本(本文基于5.3.0)
  2. 处理10MB+文档时增加JVM内存:
bash 复制代码
java -Xmx512m -jar yourApp.jar

此方案已通过以下环境验证:

  • Java 11+
  • Apache POI 5.3.0
  • Microsoft Word 2016/365
相关推荐
·薯条大王12 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
ZJH__GO15 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络
程序员黑豆15 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程
命运之光15 小时前
【C语言完整代码】就诊信息管理系统
java·c语言·开发语言
凤山老林16 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端
marvelyu16 小时前
每天10分钟学会OceanBase系列(Day 20):跨机房容灾实战——构建多数据中心高可用架构
java·大数据·数据库
AI人工智能+电脑小能手18 小时前
大白话说Java设计模式-04-工厂方法模式(业务实战篇)
java·设计模式·工厂方法模式·架构设计·代码解耦
BUG指挥官18 小时前
Sa-Token和Spring Security对比
java·后端·spring
小当家.10518 小时前
深入理解 ReAct Agent:从原理到 Java 实战
java·react.js·agent·react·架构设计·agent设计
geminigoth18 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring