Apache POI替换Word占位符后文本加粗混合格式丢失问题
问题根因
格式丢失完全是现有替换逻辑的实现缺陷导致的,核心问题有两点:
- Word的docx格式中,所有文本格式(加粗、斜体、字体、字号等)都是绑定在
w:r(即XWPFRun对象)上的,同一段落内不同格式的文本会被拆成多个独立的run存储。你处理正文段落时,把所有run的文本拼接成一个字符串,删除多余run只留第一个,再把整段替换后的文本全塞到这一个run里,自然所有非第一个run的格式全部丢失,整段只剩第一个run的默认格式。 - 处理文本框内的内容时,你通过
CTR.Factory.parse重新解析生成了新的CTR对象副本,没有直接操作原文档内的run节点,后续全量替换文本回写时也没有保留原run的格式关联,同样会导致格式丢失。
修复方案
核心原则:禁止跨run拼接整段文本后回写到单个run,替换操作尽量在单个run内完成,仅在占位符被Word自动拆分到相邻run时做最小范围的合并,全程保留原run的格式属性。
具体修改步骤:
- 提前把所有占位符和替换值存入Map,避免写重复的replace逻辑
- 封装通用的段落替换方法,正文、文本框、页眉页脚内的段落都复用这套逻辑,不要分开写不同的替换实现
- 处理文本框内的CTR节点时,直接做类型转换,不要通过parse新建对象副本
- 废弃原有拼接全段文本、删除多余run的逻辑
核心实现代码如下:
// 提前构造占位符映射 Map<String, String> placeholderMap = new HashMap<>(); placeholderMap.put("[RECEPIENT_NAME]", name); placeholderMap.put("[ADDRESS1]", address1); placeholderMap.put("LETTER_GEN_DT", date); placeholderMap.put("[ID]", sbsbid); placeholderMap.put("MEMR_RX_ID", memrid); placeholderMap.put("MEMR_MCTR_RX_GROUP", memrgroup); placeholderMap.put("MEMR_MCTR_RXBIN", memrrxbin); placeholderMap.put("MEMR_MCTR_PCN", memrpcn); // 按原有的地址判断逻辑把ADDRESS2/ADDRESS3等占位符的值提前算好存入map,不要在替换循环里写分支判断 /** * 通用段落占位符替换,保留原文本格式 */ private void replaceInParagraph(XWPFParagraph paragraph, Map<String, String> placeholderMap) { List<XWPFRun> runs = paragraph.getRuns(); if (runs == null || runs.isEmpty()) { return; } // 第一步:单个run内的完整占位符直接替换,完全不改动原run格式 for (XWPFRun run : runs) { String text = run.getText(0); if (text == null || text.isEmpty()) { continue; } String replaced = text; for (Map.Entry<String, String> entry : placeholderMap.entrySet()) { replaced = replaced.replace(entry.getKey(), entry.getValue()); } run.setText(replaced, 0); } // 第二步:处理被Word自动拆分到相邻run的占位符(最多连续查5个run,覆盖绝大多数自动拆分场景) int runCount = runs.size(); for (int i = 0; i < runCount; i++) { StringBuilder segBuffer = new StringBuilder(); for (int j = i; j < Math.min(i + 5, runCount); j++) { XWPFRun segRun = runs.get(j); String segText = segRun.getText(0); segText = segText == null ? "" : segText; segBuffer.append(segText); String segStr = segBuffer.toString(); boolean hit = false; for (Map.Entry<String, String> entry : placeholderMap.entrySet()) { if (segStr.contains(entry.getKey())) { hit = true; segStr = segStr.replace(entry.getKey(), entry.getValue()); break; } } if (hit) { // 替换后的文本写入起始位置的run,保留该run的格式,其余参与合并的run清空文本 runs.get(i).setText(segStr, 0); for (int k = i + 1; k <= j; k++) { runs.get(k).setText("", 0); } break; } } } }
原有遍历逻辑修改点:
- 正文段落直接调用
replaceInParagraph(p, placeholderMap)即可,删掉原来isPrg分支下拼接runTxt、删除run、回写文本的全部逻辑 - 处理文本框内容时,把原来的
CTR ctr = CTR.Factory.parse(obj.newInputStream())改为CTR ctr = (CTR) obj;,拿到文本框内的段落对象后同样调用replaceInParagraph方法处理,不要单独写替换逻辑
注意:如果占位符本身被手动设置了跨格式(比如占位符一半是加粗、一半是普通文本),需要提前在模板里把占位符设置为统一格式,否则替换时会以占位符起始位置的run格式为准。正常模板里的占位符都是统一格式的,不会有这个问题。
内容的提问来源于stack exchange,提问作者user9093941
相关产品推荐
相关产品推荐

