代码之家  ›  专栏  ›  技术社区  ›  amaidment

Java资源InputStream正在关闭?

  •  8
  • amaidment  · 技术社区  · 6 年前

    我正在将Java代码库从Java 7(80)迁移到Java 8(162)。(是的……我们处于技术的前沿。)

    切换之后,在高度并发的环境中从部署的JAR加载XML资源文件时,我遇到了一些问题。正在使用访问资源文件 try-with-resources 并通过SAX解析:

    try {
      SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
      try (InputStream in = MyClass.class.getResourceAsStream("resource.xml")) {
        parser.parse(in, new DefaultHandler() {...});
      }
    } catch (Exception ex) {
      throw new RuntimeException("Error loading resource.xml", ex);
    } 
    

    如果我错了,请纠正我,但这似乎是通常建议用于读取资源文件的方法。

    这在IDE中工作得很好,但一旦它部署到jar中,我经常(但不是普遍的,也不总是使用相同的资源文件)得到 IOException ,具有以下堆栈跟踪:

    Caused by: java.io.IOException: Stream closed 
        at java.util.zip.InflaterInputStream.ensureOpen(InflaterInputStream.java:67)
        at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:142)
        at java.io.FilterInputStream.read(FilterInputStream.java:133)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityManager$RewindableInputStream.read(XMLEntityManager.java:2919)
        at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(UTF8Reader.java:302)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1895)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.scanName(XMLEntityScanner.java:728)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanStartElement(XMLDocumentFragmentScannerImpl.java:1279)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2784)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:602)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:505)
        at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:842)
        at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:771)
        at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
        at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1213)
        at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:643)
        at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.parse(SAXParserImpl.java:327)
        at javax.xml.parsers.SAXParser.parse(SAXParser.java:195)
    

    问题:

    • 这是怎么回事?

    • 我读取/解析这些资源文件的方式是否有问题?(或者你能提出改进建议吗?)

    • 我能做些什么来解决这个问题?

    初步想法:

    最初,因为我只在jar中部署代码时才看到问题,所以我认为这与通过 JarFile -可能资源文件正被共享 JarFile文件 ,并且当其中一个资源输入流关闭时,即关闭 JarFile文件 ,即关闭所有其他打开的输入流。例如,有一个 SO question showing similar behaviour (当OP直接处理 JarFile文件 s) 。另外,还有一个相似的外观 bug report ,但这早在Java 6中就出现了,而且显然在Java 7中得到了修复。

    更新1:

    在进一步调试之后,这个问题似乎是因为XML解析器正在关闭 InputStream 当它完成解析时。(这对我来说似乎有点奇怪——事实上,这引发了这些关于 DOM SAX 正在分析-但我们开始了。)因此,我目前的最佳猜测是 SAXParser (或实际上在 XMLEntityManager )正在呼叫 InputStream.close() ,但这个州有某种种族条件?

    这似乎与try with resources的使用无关,也就是说,假设SAXParser正在关闭InputStream,我已经尝试删除try with resources,但仍然得到相同的错误/堆栈跟踪。

    更新2:

    经过更多的调试,我发现 XMLEntityManager$RewindableInputStream 正在关闭, 之前 它已完成XML文件的读取。有趣的是,我只在高度并发的环境中看到了这一点,但即使我对所有可能的XML资源加载(即一次只读取一个XML资源)设置了锁,我仍然看到了这一点。

    XMLEntityManager$RewindableInputStream关闭位置的堆栈跟踪- 之前 文件读取完毕-如下所示:

      at java.util.zip.InflaterInputStream.close(InflaterInputStream.java:224)
      at java.util.zip.ZipFile$ZipFileInflaterInputStream.close(ZipFile.java:417)
      at java.io.FilterInputStream.close(FilterInputStream.java:181)
      at sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream.close(JarURLConnection.java:108)
      at com.sun.org.apache.xerces.internal.impl.XMLEntityManager$RewindableInputStream.close(XMLEntityManager.java:3005)
      at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.close(UTF8Reader.java:674)
      at com.sun.xml.internal.stream.Entity$ScannedEntity.close(Entity.java:422)
      at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.endEntity(XMLEntityManager.java:1387)
      at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1916)
      at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.skipSpaces(XMLEntityScanner.java:1629)
      at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$TrailingMiscDriver.next(XMLDocumentScannerImpl.java:1371)
      at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:602)
      at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:112)
      at com.sun.org.apache.xerces.internal.impl.XMLStreamReaderImpl.next(XMLStreamReaderImpl.java:553)
      at com.sun.xml.internal.stream.XMLEventReaderImpl.nextEvent(XMLEventReaderImpl.java:83)
    

    因此,目前,我的最佳猜测(仅此而已)是,核心Java XML文件管理器/输入流等中存在一些小规模的并发错误。可能是同步省略的结果吧?(如果是这样的话,我不确定这是一个只在Java 8的并发性改进中暴露出来的预先存在的bug,还是Java 8中的一个新bug。)

    (也就是说,我还没有提交bug报告,因为我认为我没有足够的信息来继续说有bug,也没有足够的信息来通知任何想要寻找它的人。)

    解决方法:

    考虑到问题是由于使用了核心的Java XML库,我决定编写自己的(主要基于StAX)。幸运的是,我们的XML资源文件非常简单明了,因此我只需要在核心Java XML解析器中实现一小部分功能。

    更新3:

    上述解决方法确实改善了情况,例如,它解决了我所面临的问题的特定实例。然而,在那之后,我发现我仍然会遇到这样的情况:来自JAR中资源的InputStream在读取时被关闭。现在堆栈跟踪如下所示:

    java.lang.IllegalStateException: zip file closed
    at java.util.zip.ZipFile.ensureOpen(ZipFile.java:686)
    at java.util.zip.ZipFile.access$200(ZipFile.java:60)
    at java.util.zip.ZipFile$ZipEntryIterator.hasNext(ZipFile.java:508)
    at java.util.zip.ZipFile$ZipEntryIterator.hasMoreElements(ZipFile.java:503)
    at java.util.jar.JarFile$JarEntryIterator.hasNext(JarFile.java:253)
    at java.util.jar.JarFile$JarEntryIterator.hasMoreElements(JarFile.java:262)
    

    搜索与堆栈跟踪相关的问题使我找到了 question ,并建议我控制 URLConnection ,以避免缓存连接,从而不会共享连接: [URLConnection.setUseCaches(boolean)][6]

    因此,我尝试了这个方法(请参见下面的实现答案),它似乎工作正常且稳定。我甚至回去用我以前的核心Java StAX解析器进行了尝试,一切似乎都正常且稳定。(除此之外,我目前还没有决定是否保留我的自定义XML解析器——它们似乎由于被点亮而性能更好一些,但这是一种与额外维护要求的权衡。)因此,这可能不是核心Java XML解析器中的并发错误,而是JVM中动态类加载器的问题。

    更新4:

    我越来越认为,这是核心Java中的一个并发缺陷,它是如何处理从JAR中以流的形式访问资源文件的。例如,在 org.reflections.reflections ,我也遇到过。

    我也看到了关于 JBLAS ,这样我可以得到以下异常(以及 issue 提出):

    Caused by: java.lang.NullPointerException: Inflater has been closed
    at java.util.zip.Inflater.ensureOpen(Inflater.java:389)
    at java.util.zip.Inflater.inflate(Inflater.java:257)
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:152)
    at java.io.FilterInputStream.read(FilterInputStream.java:133)
    at java.io.FilterInputStream.read(FilterInputStream.java:107)
    at org.jblas.util.LibraryLoader.loadLibraryFromStream(LibraryLoader.java:261)
    at org.jblas.util.LibraryLoader.loadLibrary(LibraryLoader.java:186)
    at org.jblas.NativeBlasLibraryLoader.loadLibraryAndCheckErrors(NativeBlasLibraryLoader.java:32)
    at org.jblas.NativeBlas.<clinit>(NativeBlas.java:77)
    
    1 回复  |  直到 6 年前
        1
  •  2
  •   amaidment    6 年前

    正如我在“更新3”中所解释的,我发现以下是一个可行且稳定的解决方案:

    try {
      SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
      URLConnection connection = MyClass.class.getResource("resource.xml").openConnection()
      connection.setUseCaches(false);  
      try (InputStream in = connection.getInputStream()) {
        parser.parse(in, new DefaultHandler() {...});
      }
    } catch (Exception ex) {
      throw new RuntimeException("Error loading resource.xml", ex);
    }