Java 高效替换 Word 文档内容(模板填充方案)

Luca Ju
2025-11-17 / 0 评论 / 14 阅读 / 正在检测是否收录...
使用java操作Excel大家应该非常熟悉,但是操作word可能会稍微少一点。本文将提供一套基于 Apache POI 的完整解决方案,支持普通段落+表格单元格的占位符替换,逻辑简洁、可直接复用。

一、核心思路

  1. 模板设计:在 Word 文档中,用 {{占位符名}} 标记需要替换的内容(如 {{name}}{{date}});
  2. 技术选型:使用 Apache POI(poi-ooxml)解析 .docx 文档,遍历段落和表格,替换占位符;
  3. 核心优势:无需依赖第三方付费组件,支持复杂文档结构,兼容主流 Word 版本。

二、实现步骤

1. 环境准备(添加 Maven 依赖)

核心依赖 poi-ooxml 用于处理 Office Open XML 格式(.docx),兼容 5.2.0 及以上版本(推荐使用最新稳定版):

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.0</version> <!-- 你可以使用最新版本 -->
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.0</version>
</dependency>

2. 编写工具类(完整可运行代码)

工具类封装了「读取模板、替换占位符、保存文件」全流程,支持普通段落和表格单元格的替换,还保留了原文本格式(字体、大小、颜色):

package com.water.ocrimagerecognize.util;

import org.apache.poi.xwpf.usermodel.*;

import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WordTemplateExporter {

    public static void main(String[] args) {
        // 输出文件路径
        String outputPath = "output.docx";

        try {

            Map<String,String> contractData = new HashMap<>();
            // 占位符替换
            contractData.put("date", "2025");
            contractData.put("s15", "FF");
            contractData.put("s16", "增效降本");
            contractData.put("name", "张三");
            contractData.put("luca", "lucaju");
            contractData.put("s1", "game");
            // 加载资源文件夹resources/tem文件夹下的合同模板
            //不在resources获取模板文件需要修改此处,通过这个方法只能获取resources中的资源文件
            InputStream templateInputStream = new FileInputStream("/Users/lucaju/Documents/文档/水务/word/template.docx");

            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 filePath = "contract_" + System.currentTimeMillis() + ".docx";
            //检查目录是否存在,不存在则创建
            File directory = new File(filePath);

            // 使用 FileOutputStream 保存填充后的合同
            try (FileOutputStream out = new FileOutputStream(filePath)) {
                document.write(out);  // 将内容写入文件
            }

            System.out.println("输出完成");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //替换占位符方法
    public static void change(XWPFParagraph paragraph, Map<String, String> contractData){
        for (XWPFRun run : paragraph.getRuns()) {
            // 获取当前 run 的文本
            String runText = run.getText(0);
            if (runText == null){
                continue;
            }
            StringBuilder newText = new StringBuilder(runText);

            // 替换占位符
            for (Map.Entry<String, String> 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);
        }
    }

    /**
     * 替换 Word 文件中的占位符(支持普通段落 + 表格单元格)
     *
     * @param document    Word 文档对象
     * @param placeholder 占位符,例如 {{name}}
     * @param replacement 替换后的值,例如 "张三"
     */
    private static void replacePlaceholder(XWPFDocument document, String placeholder, String replacement) {
        // 1. 替换普通段落中的占位符(原有逻辑保留)
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replaceRunInParagraph(paragraph, placeholder, replacement);
        }

        // 2. 替换表格中的占位符(新增核心逻辑)
        for (XWPFTable table : document.getTables()) { // 遍历所有表格
            for (XWPFTableRow row : table.getRows()) { // 遍历表格的所有行
                for (XWPFTableCell cell : row.getTableCells()) { // 遍历行的所有单元格
                    for (XWPFParagraph paragraph : cell.getParagraphs()) { // 遍历单元格内的所有段落
                        replaceRunInParagraph(paragraph, placeholder, replacement); // 替换段落中的占位符
                    }
                }
            }
        }
    }

    /**
     * 替换单个段落中所有 Run 里的占位符(抽取通用逻辑,避免重复代码)
     */
    private static void replaceRunInParagraph(XWPFParagraph para, String placeholder, String replacement) {
        List<XWPFRun> runs = para.getRuns();
        int runCount = runs.size();

        // 情况1:段落只有1个 Run(直接替换)
        if (runCount == 1) {
            XWPFRun run = runs.get(0);
            String text = run.getText(0);
            if (text != null && text.contains(placeholder)) {
                run.setText(text.replace(placeholder, replacement), 0);
            }
            return;
        }

        // 情况2:段落有多个 Run(合并文本后替换)
        StringBuilder mergedText = new StringBuilder();
        // 第一步:合并所有 Run 的文本
        for (XWPFRun run : runs) {
            String text = run.getText(0);
            if (text != null) {
                mergedText.append(text);
            }
        }

        // 检查合并后的文本是否包含占位符
        String finalText = mergedText.toString();
        if (!finalText.contains(placeholder)) {
            return; // 不包含则无需处理
        }

        // 第二步:替换占位符
        finalText = finalText.replace(placeholder, replacement);

        // 第三步:清空原有所有 Run(兼容低版本 POI 的写法)
        // 注意:要倒序删除,避免索引错乱
        for (int i = runCount - 1; i >= 0; i--) {
            para.removeRun(i);
        }

        // 第四步:创建新的 Run,写入替换后的文本
        XWPFRun newRun = para.createRun();
        newRun.setText(finalText, 0);

        // (可选)复制原有文本的格式(如字体、大小、颜色)
        if (runCount > 0) {
            XWPFRun originalFirstRun = runs.get(0); // 取第一个 Run 的格式
            newRun.setFontFamily(originalFirstRun.getFontFamily());
            newRun.setFontSize(originalFirstRun.getFontSize());
            newRun.setColor(originalFirstRun.getColor());
        }
    }
}

3. 模板编辑规范

  1. 占位符格式:统一使用 {{占位符名}}(如 {{name}}{{date}}),可自定义分隔符(需同步修改代码中 placeholder 的拼接逻辑);
  2. 关键注意点

    • 占位符必须作为一个整体输入(直接复制粘贴 {{name}},或一次性输入完成);
    • 避免输入一半保存、再续输的操作(会导致占位符被拆分成多个 Run,替换失败);
    • 模板文件需保存为 .docx 格式(不支持 .doc 旧格式,如需兼容可先转成 .docx)。

三、使用示例

  1. 编辑模板 template.docx,内容如下:

    姓名:{{name}}
    日期:{{date}}
    项目:{{s1}}
    目标:{{s16}}
  2. 运行 main 方法,传入替换数据;
  3. 生成的文件中,占位符会被自动替换为对应值:

    姓名:张三
    日期:2025年11月
    项目:game
    目标:增效降本

这套方案轻量化、无额外依赖,适合合同生成、报表导出、通知书批量制作等场景,可直接集成到 Spring Boot、SSM 等主流 Java 项目中。

0

评论 (0)

取消