使用Java操作Word文档:基于docx4j的实战指南

前言

在企业级应用开发中,生成各类报表和证书是常见需求。本文将分享如何使用Java中的docx4j库来操作Word文档,实现动态填充内容、设置复选框状态等功能,以实际项目为例进行实战讲解。

技术栈

  • Java Spring Boot
  • docx4j 库
  • Word OOXML 格式

核心功能实现

1. 文档生成流程概述

Word文档生成的基本流程包括:加载模板、填充内容、设置控件状态、导出文档。下面是一个完整的实现示例:

public Resource generateCertificate(StandardComplianceCertificate certificate) throws IOException {
    try {
        // 1. 加载模板文件
        Resource templateResource = resourceLoader.getResource(certificateTemplatePath);
        try (InputStream is = templateResource.getInputStream()) {
            // 2. 使用docx4j加载Word文档
            WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(is);
            
            // 3. 替换文本内容
            replaceTextContent(wordMLPackage, certificate);
            
            // 4. 设置复选框状态
            setCheckboxesStatus(wordMLPackage, certificate);

            // 5. 将处理后的文档写入字节数组输出流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Docx4J.save(wordMLPackage, baos);
            
            // 6. 返回处理后的文档资源
            return new ByteArrayResource(baos.toByteArray());
        }
    } catch (Docx4JException e) {
        throw new IOException("处理Word文档时发生错误", e);
    }
}

2. 替换文本内容

在Word模板中,我们通常使用占位符来标记需要动态替换的内容。以下代码演示如何遍历文档并替换文本:

private void replaceTextContent(WordprocessingMLPackage wordMLPackage, StandardComplianceCertificate certificate) {
    Map<String, String> replacements = new HashMap<>();
    replacements.put("产品名称", certificate.getProductName());
    replacements.put("数量(重量)", certificate.getQuantity());
    replacements.put("产地", certificate.getOrigin());
    replacements.put("生产者盖章或签名", certificate.getProducer());
    replacements.put("联系方式", certificate.getContact());

    try {
        // 获取主文档部分
        MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
        
        // 获取文档内容
        org.docx4j.wml.Document document = documentPart.getJaxbElement();
        
        // 遍历文档中的所有段落
        for (Object bodyElement : document.getBody().getContent()) {
            if (bodyElement instanceof org.docx4j.wml.P) {
                org.docx4j.wml.P paragraph = (org.docx4j.wml.P) bodyElement;
                
                // 遍历段落中的所有文本运行
                for (Object run : paragraph.getContent()) {
                    if (run instanceof org.docx4j.wml.R) {
                        for (Object runContent : ((org.docx4j.wml.R) run).getContent()) {
                            if (runContent instanceof jakarta.xml.bind.JAXBElement) {
                                Object value = ((jakarta.xml.bind.JAXBElement<?>) runContent).getValue();
                                
                                if (value instanceof org.docx4j.wml.Text) {
                                    org.docx4j.wml.Text text = (org.docx4j.wml.Text) value;
                                    String textValue = text.getValue();
                                    
                                    // 遍历所有需要替换的文本
                                    for (Map.Entry<String, String> entry : replacements.entrySet()) {
                                        if (entry.getValue() != null && textValue.contains(entry.getKey() + ":")) {
                                            // 替换文本
                                            text.setValue(textValue.replace(entry.getKey() + ":", entry.getKey() + ":" + entry.getValue()));
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3. 处理内容控件复选框

Word文档中的复选框通常以内容控件的形式存在。以下代码展示如何设置复选框的状态:

private void setCheckboxesStatus(WordprocessingMLPackage wordMLPackage, StandardComplianceCertificate certificate) {
    try {
        // 创建复选框与其状态的映射
        Map<String, Boolean> checkboxMap = new HashMap<>();
        checkboxMap.put("不使用禁用农药兽药、停用兽药和非法添加物", certificate.getNoIllegalDrugs());
        checkboxMap.put("常规农药兽药残留不超标", certificate.getNoExcessiveResidues());
        checkboxMap.put("对承诺的真实性负责", certificate.getResponsible());
        checkboxMap.put("委托检测", certificate.getThirdPartyInspection());
        checkboxMap.put("自我检测", certificate.getSelfInspection());
        checkboxMap.put("内部质量控制", certificate.getInternalQualityControl());
        checkboxMap.put("自我承诺", certificate.getSelfCommitment());

        // 获取文档的主体部分
        MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
        org.docx4j.wml.Document document = documentPart.getJaxbElement();
        Body body = document.getBody();

        // 递归查找所有SDT内容控件
        processBodyContent(body.getContent(), checkboxMap);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

4. 递归处理文档内容

Word文档结构复杂,需要递归遍历处理:

private void processBodyContent(List<Object> content, Map<String, Boolean> checkboxMap) {
    for (Object obj : content) {
        if (obj instanceof P) {
            // 处理段落中的内容控件
            P paragraph = (P) obj;
            for (Object paragraphContent : paragraph.getContent()) {
                if (paragraphContent instanceof SdtRun) {
                    // 处理行内内容控件
                    SdtRun sdtRun = (SdtRun) paragraphContent;
                    processSdt(sdtRun, checkboxMap);
                } else if (paragraphContent instanceof JAXBElement) {
                    // 处理可能被JAXBElement包装的内容
                    JAXBElement<?> element = (JAXBElement<?>) paragraphContent;
                    Object value = element.getValue();
                    
                    // 检查元素的声明类型和实际类型
                    String declaredType = element.getDeclaredType().getName();
                    
                    if (value instanceof SdtRun || declaredType.endsWith("SdtRun")) {
                        if (value instanceof SdtRun) {
                            processSdt((SdtRun) value, checkboxMap);
                        }
                    }
                }
            }
        }
    }
}

5. 处理内容控件

针对内容控件的处理,需要获取其标签值并设置状态:

private void processSdt(SdtRun sdtRun, Map<String, Boolean> checkboxMap) {
    SdtPr sdtPr = sdtRun.getSdtPr();
    if (sdtPr != null) {
        // 尝试直接获取Tag值
        Tag tag = sdtPr.getTag();
        if (tag != null) {
            String tagValue = tag.getVal();
            if (checkboxMap.containsKey(tagValue)) {
                Boolean checked = checkboxMap.get(tagValue);
                setCheckboxValue(sdtRun, checked);
                return;
            }
        }
    }
}

6. 设置复选框状态

最后,使用Wingdings字体的特殊字符来表示复选框的选中状态:

private void updateCheckboxState(List<Object> content, Boolean checked) {
    // Word中使用Wingdings字体的复选框字符
    char checkedSymbol = '\u00FE'; // Wingdings字体中字符代码254
    char uncheckedSymbol = '\u00A8'; // Wingdings字体中字符代码168(空框)
    
    for (Object obj : content) {
        if (obj instanceof R) {
            R run = (R) obj;
            
            // 设置Wingdings字体
            RPr rPr = run.getRPr();
            if (rPr == null) {
                rPr = new RPr();
                run.setRPr(rPr);
            }
            
            // 设置字体为Wingdings
            RFonts rFonts = new RFonts();
            rFonts.setAscii("Wingdings");
            rFonts.setHAnsi("Wingdings");
            rPr.setRFonts(rFonts);
            
            for (Object runContent : run.getContent()) {
                if (runContent instanceof JAXBElement) {
                    JAXBElement<?> element = (JAXBElement<?>) runContent;
                    if (element.getValue() instanceof Text) {
                        Text text = (Text) element.getValue();
                        // 替换为选中或未选中的字符
                        text.setValue(String.valueOf(checked ? checkedSymbol : uncheckedSymbol));
                    }
                }
            }
        }
    }
}

实现要点与技巧

1. Word文档结构理解

Word文档的OOXML结构是基于XML的,主要包含:

  • 段落(Paragraph)
  • 文本运行(Run)
  • 内容控件(StructuredDocumentTag)
  • 表格(Table)等元素

理解这些元素的层次关系对于正确操作Word文档至关重要。

2. 内容控件处理技巧

在Word中创建内容控件时,可以为其设置标题(Tag),这样在代码中可以通过标题来识别和操作特定的控件。对于复选框,我们使用Wingdings字体的特殊字符来表示选中和未选中状态。

3. 递归遍历文档

Word文档结构复杂,需要递归遍历处理各种嵌套元素。在实现中,我们通过递归函数处理文档中的各种元素,确保不遗漏任何内容。

4. 异常处理

在处理Word文档时,可能会遇到各种异常情况,如文档格式不正确、缺少预期的元素等。良好的异常处理机制能够提高程序的健壮性。

常见问题与解决方案

1. 复选框无法正确显示

问题:设置复选框状态后,在Word中打开文档时复选框状态不正确。

解决方案:确保使用正确的Wingdings字体字符,并正确设置字体属性。在我们的实现中,使用字符代码254表示选中状态,字符代码168表示未选中状态。

2. 文本替换不完整

问题:某些文本内容没有被正确替换。

解决方案:检查文本替换的逻辑,确保占位符格式一致。在我们的实现中,使用"字段名:"作为占位符的格式。

3. docx4j依赖问题

问题:运行时出现ClassNotFoundException等依赖相关错误。

解决方案:确保添加了正确的docx4j依赖,包括核心库和JAXB实现。

<dependency>
    <groupId>org.docx4j</groupId>
    <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
    <version>11.4.8</version>
</dependency>
<dependency>
    <groupId>org.docx4j</groupId>
    <artifactId>docx4j-core</artifactId>
    <version>11.4.8</version>
</dependency>

总结

使用docx4j库操作Word文档是一种强大且灵活的解决方案,适用于各种企业级应用场景。通过本文介绍的技术和方法,可以实现Word文档的动态生成、内容填充和控件操作,满足各种业务需求。

在实际应用中,可以根据具体需求扩展和优化这些方法,例如添加表格操作、图片插入、样式设置等功能,进一步提升文档处理的能力和效率。

希望本文对您在Java环境下处理Word文档有所帮助。如有任何问题或建议,欢迎在评论区留言交流。