Apache POI修改形状文本后修改段落报ConcurrentModificationException异常
问题:Apache POI修改形状文本后触发ConcurrentModificationException
使用Apache POI 5.2.2版本修改Word文档中矩形形状内的文本占位符,修改形状文本的代码可正常运行,但后续修改其他段落文本时,会抛出java.util.ConcurrentModificationException,异常提示为Document changed during select。
修改文本框内容的代码
private void replacePlaceholdersWithInAShape(XWPFParagraph paragraph) throws XmlException { XmlCursor cursor = paragraph.getCTP().newCursor(); cursor.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' .//*/w:txbxContent/w:p/w:r"); List<XmlObject> ctrsintxtbx = new ArrayList<XmlObject>(); while(cursor.hasNextSelection()) { cursor.toNextSelection(); XmlObject obj = cursor.getObject(); ctrsintxtbx.add(obj); } for (XmlObject obj : ctrsintxtbx) { CTR ctr = CTR.Factory.parse(obj.xmlText()); XWPFRun bufferrun = new XWPFRun(ctr, (IRunBody)paragraph); String text = bufferrun.getText(0); if (text != null && !text.equalsIgnoreCase("")) { if(text.contains("[{project_number}]")) { System.out.println("contains project number"); text = text.replace("[{project_number}]", "test"); bufferrun.setText("test", 0); } else { bufferrun.setText(text, 0); } } obj.set(bufferrun.getCTR()); } }
修改段落文本的代码
protected void replaceParagraphPlaceholders(String paraText, XWPFParagraph paragraph, String key, String value, Map<String, Object> fields) { //paragraph text's empty and null check. String paragraphText = paragraph.getText(); if(paragraphText!=""&¶graphText!=null) { if(paragraph.getRuns().size()<=1) { String runText = "Sample text"; paragraph.getRuns().get(0).setText(runText, 0); } } }
抛出的异常
java.util.ConcurrentModificationException: Document changed during select at org.apache.xmlbeans.impl.xpath.xmlbeans.XmlbeansXPathEngine.next(XmlbeansXPathEngine.java:100) at org.apache.xmlbeans.impl.store.Cursor._toSelection(Cursor.java:749) at org.apache.xmlbeans.impl.store.Cursor._getSelectionCount(Cursor.java:764) at org.apache.xmlbeans.impl.store.Cursor.notifyChange(Cursor.java:697) at org.apache.xmlbeans.impl.store.Locale.notifyChange(Locale.java:995) at org.apache.xmlbeans.impl.store.Cur.moveChars(Cur.java:1772) at org.apache.xmlbeans.impl.store.Cur.moveNodeContents(Cur.java:2016) at org.apache.xmlbeans.impl.store.Cur.moveNodeContents(Cur.java:1986) at org.apache.xmlbeans.impl.store.Xobj.store_text(Xobj.java:1833) at org.apache.xmlbeans.impl.values.XmlObjectBase.set_String(XmlObjectBase.java:1106) at org.apache.xmlbeans.impl.values.XmlObjectBase.setStringValue(XmlObjectBase.java:1774) at org.apache.poi.xwpf.usermodel.XWPFRun.setText(XWPFRun.java:363) at net.sharesuite.formletter.builder.poibuilder.docx.PoiDocxBuilder.replaceParagraphPlaceholders(PoiDocxBuilder.java:596) at net.sharesuite.formletter.builder.poibuilder.docx.PoiDocxBuilder.lambda$1(PoiDocxBuilder.java:205) at java.base/java.util.ArrayList$Itr.forEachRemaining(ArrayList.java:1003) at java.base/java.util.Collections$UnmodifiableCollection$1.forEachRemaining(Collections.java:1061) at net.sharesuite.formletter.builder.poibuilder.docx.PoiDocxBuilder.createFormLetterFile(PoiDocxBuilder.java:175) at net.sharesuite.formletter.service.impl.FormLetterServiceImpl.createFormLetter(FormLetterServiceImpl.java:42) at net.sharesuite.test.formletter.DocumentTest.testNLBLFormletter(DocumentTest.java:646) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) at org.junit.runners.ParentRunner.run(ParentRunner.java:309) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:93) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
问题根源与修复方案
核心原因
异常触发是因为XmlCursor未被关闭:遍历形状内文本节点后,XmlCursor仍持有文档的引用,后续修改文档内容时,XmlCursor的内部状态与实际文档结构不一致,触发了并发修改检测机制。
修复步骤
关闭XmlCursor释放资源
在replacePlaceholdersWithInAShape方法中,遍历完成后必须显式关闭XmlCursor,添加cursor.dispose();,建议放在finally块中确保执行。优化形状文本修改逻辑
无需重新解析XML文本生成CTR对象,直接强转XmlObject为CTR即可,减少不必要的文档修改操作;同时去掉重复设置原文本的冗余代码。
修改后的replacePlaceholdersWithInAShape代码:
private void replacePlaceholdersWithInAShape(XWPFParagraph paragraph) throws XmlException { XmlCursor cursor = paragraph.getCTP().newCursor(); try { cursor.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' .//*/w:txbxContent/w:p/w:r"); List<XmlObject> ctrsintxtbx = new ArrayList<>(); while (cursor.hasNextSelection()) { cursor.toNextSelection(); XmlObject obj = cursor.getObject(); ctrsintxtbx.add(obj); } for (XmlObject obj : ctrsintxtbx) { CTR ctr = (CTR) obj; // 直接强转,无需重新解析XML XWPFRun bufferrun = new XWPFRun(ctr, (IRunBody) paragraph); String text = bufferrun.getText(0); if (text != null && !text.isBlank()) { if (text.contains("[{project_number}]")) { bufferrun.setText("test", 0); } // 无需重复设置原文本,避免不必要的文档变更 } // 无需调用obj.set(ctr),CTR是直接引用文档内的对象 } } finally { cursor.dispose(); // 确保关闭cursor,释放文档引用 } }
- 修复段落文本的空值判断错误
原代码中paragraphText!=""是引用比较,会导致空字符串判断失效,改为!paragraphText.isEmpty():
修改后的replaceParagraphPlaceholders代码片段:
String paragraphText = paragraph.getText(); if (paragraphText != null && !paragraphText.isEmpty()) { if (paragraph.getRuns().size() <= 1) { String runText = "Sample text"; paragraph.getRuns().get(0).setText(runText, 0); } }
内容的提问来源于stack exchange,提问作者Furqan Ahmed
相关产品推荐
相关产品推荐

