NiceXWPFDocument与XWPFDocument混用导致NullPointerException
1. 问题现象
为了操作office文档,程序中使用了poi工具库。为了实现某些特殊需求(如文档合并),又引入了poi-tl库。poi-tl库是对poi的增强,起初认为它们不应该有什么冲突,所以程序中有的地方直接使用poi的XWPFDocument,有的地方使用了poi-tl的NiceXWPFDocument。实际运行中,用户反映用一段时间后,再对某些文档进行写操作时就会出现空指针异常,且一旦出现就只能重启才能解决。异常堆栈如下:
java.lang.NullPointerException
at com.deepoove.poi.plugin.comment.XWPFComments.commit(XWPFComments.java:143)
at org.apache.poi.ooxml.POIXMLDocumentPart.onSave(POIXMLDocumentPart.java:490)
at org.apache.poi.ooxml.POIXMLDocumentPart.onSave(POIXMLDocumentPart.java:495)
at org.apache.poi.ooxml.POIXMLDocument.write(POIXMLDocument.java:236)
经过分析,只有存在批注的word文档,才会出现这个问题。
2. 分析过程
2.1. 复现问题
下面测试代码可以复现上述问题:
@Test
public void test1() {
try (InputStream inputStream = CommentTest.class.getResourceAsStream("case1.docx");
OutputStream outputStream = new FileOutputStream("/home/chengda/tmp/case1.docx")) {
NiceXWPFDocument document = new NiceXWPFDocument(inputStream);
document.write(outputStream);
} catch (Exception e) {
e.printStackTrace();
}
try (InputStream inputStream = CommentTest.class.getResourceAsStream("case1.docx");
OutputStream outputStream = new FileOutputStream("/home/chengda/tmp/case1.docx")) {
XWPFDocument document = new XWPFDocument(inputStream);
document.write(outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
只要使用过NiceXWPFDocument,再去使用XWPFDocument写文档就会报上面的异常。
2.2. 定位关键代码
从测试代码看,前后两个document对象都是直接new出来的,不存在实例上的重叠,前者要干扰后者只能通过静态属性。经过几次跟踪和分析,找到一个关键的静态属性:
public final class XWPFRelation extends POIXMLRelation {
private static final Map<String, XWPFRelation> _table = new HashMap<>();
...
public static final XWPFRelation COMMENT = new XWPFRelation(
null,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
null
);
...
private XWPFRelation(String type, String rel, String defaultName,
NoArgConstructor noArgConstructor,
ParentPartConstructor parentPartConstructor) {
super(type, rel, defaultName, noArgConstructor, null, parentPartConstructor);
_table.put(rel, this);
}
...
public static XWPFRelation getInstance(String rel) {
return _table.get(rel);
}
}
从程序上看,这个类是完全封闭的,类定义处用了final关键字,不存在继承,构造函数是private的,只能用来构造静态属性。常规思路下,这确实很让人费解。不过要对这个静态属性进行修改也不是完全不可能,毕竟还有反射这个绝招。于是继续分析代码,在NiceXWPFDocument类中找到以下内容:
static {
try {
Constructor<XWPFRelation> constructor = ReflectionUtils.findConstructor(XWPFRelation.class, String.class,
String.class, String.class, POIXMLRelation.NoArgConstructor.class,
POIXMLRelation.PackagePartConstructor.class);
COMMENTS = constructor.newInstance(
new Object[] { "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
"/word/comments.xml", new POIXMLRelation.NoArgConstructor() {
@Override
public POIXMLDocumentPart init() {
return new XWPFComments();
}
}, new POIXMLRelation.PackagePartConstructor() {
@Override
public POIXMLDocumentPart init(PackagePart part) throws IOException, XmlException {
return new XWPFComments(part);
}
} });
} catch (Exception e) {
logger.warn("init comments releation error: {}", e.getMessage());
}
}
显然,它通过反射构造了COMMENTS这个静态属性,从而注入了自身的特性。
3. 解决方案
poi-tl这个操作非常霸道,这用意就是让用户一旦用了NiceXWPFDocument就要全部都用它,不能再用poi原生的XWPFDocument。那好吧,只好妥协一些,把所有XWPFDocument都替换成NiceXWPFDocument。
License:
CC BY 4.0