【问题标题】:Java Resource InputStream being closed?Java 资源 InputStream 被关闭?
【发布时间】:2018-04-06 17:11:09
【问题描述】:

我正在将我们的 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 直接处理 JarFiles 时)。此外,还有一个类似的 bug report,但那是在 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 库,我决定自己编写(主要基于 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)

【问题讨论】:

  • 资源加载是 ClassLoader 的工作。可能是您的部署环境使用了仍然存在此错误的自定义类加载器?
  • 也许,但我认为不太可能。部署环境实际上是从命令行调用代码,使用特定的 JRE,使用一些 JVM 参数,使用类路径和主类以及一些参数 - 这对我来说似乎很普通。
  • 关闭一个入口流不应该关闭整个JarFile。请记住,这也会破坏整个类的加载,因为定位和读取类文件的工作方式完全相同。
  • 您可以打开的文件有限制吗?如果是这样,如果达到这个数字会发生什么? stackoverflow.com/questions/4289447/java-too-many-open-filesstackoverflow.com/questions/16360720/… 只是猜测
  • 这看起来相关吗? bugs.openjdk.java.net/browse/JDK-8246714

标签: java jar java-8 ioexception resource-files


【解决方案1】:

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

【讨论】:

    猜你喜欢
    • 2011-03-03
    • 1970-01-01
    • 2018-05-02
    • 2023-03-31
    • 2012-03-19
    • 2012-08-22
    • 1970-01-01
    • 2010-10-12
    相关资源
    最近更新 更多