最近工作中有一个业务需求,是替换word文档的内容。这里面的难点在于,如果替换内容过长,api工具会自动进行换行,对齐方式也是自动的,这种结果就不符合业务的需求,遇到的问题难点1: 什么时候换行,这个值是计算出来的;难点2:换行后对齐是自定义的,就是前面要空出指定多少空格,而且换行后格式要与上一行的格式保持一致;难点3:替换内容的长度是不定的,有可能会产生好几行数据。由于业务的原因,模板文件就不能提供给大家了,大家可根据demo自己创建个word文档进行研究。接下来上代码:
- Maven依赖
XML
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.2</version>
</dependency>
- 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;
}
- 最后是完整的调试代码,可以直接运行;关键的技术难点借助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());
}
}
}