文章

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