按要求格式替换word文档内容的实现

最近工作中有一个业务需求,是替换word文档的内容。这里面的难点在于,如果替换内容过长,api工具会自动进行换行,对齐方式也是自动的,这种结果就不符合业务的需求,遇到的问题难点1: 什么时候换行,这个值是计算出来的;难点2:换行后对齐是自定义的,就是前面要空出指定多少空格,而且换行后格式要与上一行的格式保持一致;难点3:替换内容的长度是不定的,有可能会产生好几行数据。由于业务的原因,模板文件就不能提供给大家了,大家可根据demo自己创建个word文档进行研究。接下来上代码:

  1. Maven依赖
XML 复制代码
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.2</version>
</dependency>
  1. properties对应Java类,用来定义替换的要求,计算后替换内容的分割长度要求,换行后前面要空多格式,与Nacos的配置文件对应,实现动态刷新,对细节进行调整,避免重启系统。
java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "project")
@Data
public class ProjectEstablishNoticeProperties {

    /** 模板URL   */
    private String noticeCertTemplateUrl;

    /** 课题名称换行后前面要增加的空格数量 */
    private int projectNameSpaceNumber;

    /** 课题名称分割的长度位置 */
    private int splitProjectNameLength;

    /** 课题成员名字分割的长度位置  */
    private int splitProjectMemberLength;

    /** 课题成员名字换行后前面要增加的空格数量 */
    private int projectMemberSpaceNumber;

}
  1. 最后是完整的调试代码,可以直接运行;关键的技术难点借助AI生成代码,有详细的注解,请仔细阅读便能明白其中的逻辑。
java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.*;
import java.util.*;

@Component
@Slf4j
public class WordNoticeGenerator {

    @Autowired
    private ProjectEstablishNoticeProperties projectEstablishNoticeProperties;


    public static void main(String[] args) throws IOException {
        String inPath = "D:\\file\\模板文件2025.docx";
        String uuid = UUID.randomUUID().toString();
        uuid = uuid.replace("-", "");

        String outputPath = "D:\\file\\"+uuid+"\\notice22015.docx";
        String outPath = "D:\\file\\"+uuid+"\\notice22015.pdf";
        Map<String, String> replacements = new HashMap<>();
        replacements.put("schoolName", "北京市海淀区第三中学");
        replacements.put("projectNumber", "PDG2023333");
        replacements.put("projectName", "人工智能赋能高校教育课程标准制定与实施研究");
        replacements.put("projectMember", "fengxuanfen   muleijuan   pingfenjun   miaoyangjian");
        WordNoticeGenerator wordNoticeGenerator = new WordNoticeGenerator();
        wordNoticeGenerator.replaceDocxContent(inPath, outputPath, replacements);
    }

    public void replaceDocxContent(String templatePath, String outputPath, Map<String, String> replacements) throws IOException {
        // 加载模板文件
        XWPFDocument document = new XWPFDocument(new FileInputStream(templatePath));

        // 遍历文档中的所有段落
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            // 替换段落中的占位符
            for (Map.Entry<String, String> entry : replacements.entrySet()) {
                String placeholder = entry.getKey();
                String replacement = entry.getValue();
                replaceTextInParagraph(paragraph, placeholder, replacement);
            }
        }

        File outputFile = new File(outputPath);
        if(!outputFile.exists()){
            outputFile.getParentFile().mkdirs();
        }

        // 保存到新的文件
        try (FileOutputStream out = new FileOutputStream(outputPath)) {
            document.write(out);
        }

        document.close();
    }

    private void replaceTextInParagraph(XWPFParagraph paragraph, String placeholder, String replacement) {
        List<XWPFRun> runs = paragraph.getRuns();
        // 倒序遍历避免并发修改异常
        for (int i = runs.size() - 1; i >= 0; i--) {
            XWPFRun run = runs.get(i);
            String text0 = run.getText(0);
            if (text0 == null) continue;

            String text1 = text0.replace(placeholder, replacement);
            // 处理项目名称拆分
            if ("projectName".equals(text0.trim()) && !text0.equals(text1)) {
                int splitLength = projectEstablishNoticeProperties.getSplitProjectNameLength();
                int spaceNumber = projectEstablishNoticeProperties.getProjectNameSpaceNumber();
                splitAndInsertRuns(paragraph, run, i, replacement, splitLength, spaceNumber);
            }
            // 处理项目成员拆分
            else if ("projectMember".equals(text0.trim()) && !text0.equals(text1)) {
                int splitLength = projectEstablishNoticeProperties.getSplitProjectMemberLength();
                int spaceNumber = projectEstablishNoticeProperties.getProjectMemberSpaceNumber();
                splitAndInsertRuns(paragraph, run, i, replacement, splitLength, spaceNumber);
            }
            // 普通文本替换
            else {
                run.setText(text1, 0);
            }
        }
    }

    private void splitAndInsertRuns(XWPFParagraph paragraph, XWPFRun originalRun, int runIndex,
                                    String content, int splitLength, int indentSpaces) {
        int contentLength = content.length();
        // 内容长度未超过单行限制,直接替换
        if (contentLength <= splitLength) {
            originalRun.setText(content, 0);
            return;
        }

        // 生成缩进空格字符串
        String indent = generateIndent(indentSpaces);
        // 处理第一段(使用原始Run)
        String firstPart = content.substring(0, splitLength);
        originalRun.setText(firstPart, 0);
        originalRun.addBreak(); // 换行

        // 处理剩余部分(循环拆分多行)
        int remainingStart = splitLength;
        int currentInsertIndex = runIndex + 1; // 插入位置从原始Run后开始

        while (remainingStart < contentLength) {
            // 计算当前段的结束位置(不超过剩余长度)
            int remainingEnd = Math.min(remainingStart + splitLength, contentLength);
            String currentPart = content.substring(remainingStart, remainingEnd);

            // 创建新Run并设置内容
            XWPFRun newRun = paragraph.insertNewRun(currentInsertIndex);
            newRun.setText(indent + currentPart.trim());
            // 复制原始格式
            copyRunStyle(originalRun, newRun);

            // 不是最后一段则添加换行
            if (remainingEnd < contentLength) {
                newRun.addBreak();
            }

            // 更新索引,准备下一段
            remainingStart = remainingEnd;
            currentInsertIndex++;
        }
    }

    /**
     * 生成指定数量的空格缩进
     */
    private String generateIndent(int spaceNumber) {
        if (spaceNumber <= 0) {
            return "";
        }
        return String.join("", Collections.nCopies(spaceNumber, " "));
    }

    /**
     * 把 srcRun 的字体、字号、加粗、颜色等样式复制到 destRun(JDK 1.8 写法)
     */
    private void copyRunStyle(XWPFRun srcRun, XWPFRun destRun) {
        // 字体名称(西方/东亚)
        if (srcRun.getFontFamily() != null) {
            destRun.setFontFamily(srcRun.getFontFamily());
        }
        /*if (srcRun.getFontFamily(FontCharRange.EAST_ASIA) != null) {
            destRun.setFontFamily(srcRun.getFontFamily(FontCharRange.EAST_ASIA), FontCharRange.EAST_ASIA);
        }*/

        // 字号(以半磅为单位)
        if (srcRun.getFontSize() != -1) {
            destRun.setFontSize(srcRun.getFontSize());
        }

        // 加粗、斜体、下划线、删除线
        destRun.setBold(srcRun.isBold());
        destRun.setItalic(srcRun.isItalic());
        destRun.setUnderline(srcRun.getUnderline());
        destRun.setStrikeThrough(srcRun.isStrikeThrough());

        // 颜色
        if (srcRun.getColor() != null) {
            destRun.setColor(srcRun.getColor());
        }
    }


}