Java 资源 InputStream 被关闭了吗?

8
我正在将我们的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问题显示类似的行为(当OP直接处理JarFile时)。此外,还有一个类似的错误报告,但那是在Java 6中,显然已在Java 7中修复。

更新1:

经过进一步调试,这个问题似乎是因为XML解析器在完成解析后关闭了InputStream。(这对我来说有点奇怪 - 实际上它促使了这些问题与DOMSAX解析有关 - 但是我们还是继续吧。)因此,我目前的最佳猜测是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中只是由于并发改进而被揭示出来,还是Java 8中的新错误。)
也就是说,我没有提交错误报告,因为我认为我没有足够的信息证明存在错误,也没有足够的信息告诉任何寻找错误的人。
解决方法:
考虑到问题来自使用核心Java XML库,我决定编写自己的XML库(主要基于StAX)。幸运的是,我们的XML资源文件非常简单直接,因此我只需要实现核心Java XML解析器功能的一小部分即可。
更新3:
上述解决方法确实有所改善,即解决了我面临的特定问题。但是,在此之后,我发现仍然存在InputStream从JAR资源中读取时被关闭的情况。现在堆栈跟踪如下:
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)

寻找与该堆栈跟踪有关的问题,我找到了这个问题,建议我控制URLConnection,以便不缓存连接,以便它们不会被共享:[URLConnection.setUseCaches(boolean)][6] 因此,我尝试了这个(请参见下面的答案实现),它似乎是工作和稳定的。我甚至回头尝试了我的之前的核心Java StAX解析器,它们似乎都在工作和稳定。(顺便说一句,我目前还没有决定是否保留我的自定义XML解析器——它们似乎更加高效,因为它们比较轻量级,但这是与额外维护要求的权衡。)所以,这可能不是核心Java XML解析器中的并发错误,而是JVM中动态类加载器的问题。
更新4:
我越来越认为这是核心Java中的并发错误,涉及到如何处理来自jar文件中的资源文件流。例如,在org.reflections.reflections中存在这个问题,我也遇到了这个问题。
我还看到了这个问题与JBLAS有关,以至于我得到了以下异常(并引发了问题):
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)

2
关闭其中一个条目流不应该关闭整个JarFile。请记住,这也会破坏整个类加载,因为定位和读取类文件的方式完全相同。 - Holger
当您尝试在不同的线程之间共享一个ZipFile实例,并且每个线程都尝试单独关闭相同的ZipFile(您只能关闭一次ZipFile),或者一个线程尝试关闭ZipFile而另一个线程仍在尝试从中读取时,就会出现异常。无论如何,在线程之间共享一个ZipFile实例其实是没有意义的,因为它会在所有方法周围强制执行同步锁。我的库FastClasspathScanner每个线程使用一个ZipFile实例,我从未遇到过这个问题,所以这不是JDK的错误,而是ZipFile API调用方式的错误。 - Luke Hutchison
@LukeHutchison - 你会从帖子开头注意到,我并没有直接调用ZipFile API,而是使用.getResourceAsStream()从.jar文件中访问资源。问题在于 - 我认为 - 在JDK中,它使用JNLPCachedJarURLConnection打开与资源URL的连接。你是对的 - 你可以通过使用单个ZipFile实例来解决这个问题 - 尽管这需要额外的知识来确定资源所在的jar文件(这可能是合理的预期)- 但我发现使用未缓存的连接(请参阅我的原始答案)是一个更干净的替代方案。 - amaidment
@amaidment 我明白了,感谢您的澄清 - 是的,我相信这也是他们在Reflections中修复错误的方法,通过关闭缓存。我想我所说的是,如果您为每个线程使用一个ZipFile实例,则可以缓存或不缓存您获得的所有InputStream,这并不重要 - 但显然.getResourceAsStream() API不希望在缓存时在线程之间共享流,或者其他什么?如果是这样,那么是的,我会称其为JDK的错误。每个线程应该有一个缓存。 - Luke Hutchison
2
这个看起来相关吗?https://bugs.openjdk.java.net/browse/JDK-8246714 - Graeme Moss
显示剩余6条评论
1个回答

2

正如我在“更新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);
} 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接