7 votes

Ressource Java InputStream en cours de fermeture ?

Je suis en train de migrer notre base de code Java de Java 7 (80) à Java 8 (162). (Oui, nous sommes à la pointe de la technologie).

Après le basculement, j'ai rencontré des problèmes lors du chargement de fichiers de ressources XML à partir de jarres déployées dans un environnement fortement concurrentiel. Les fichiers de ressources sont accessibles à l'aide de try-with-resources et analysé via 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);
} 

Veuillez me corriger si je me trompe, mais il semble que ce soit l'approche généralement conseillée pour la lecture des fichiers de ressources.

Cela fonctionne bien dans un IDE, mais une fois qu'il a été déployé dans un jar, j'obtiens fréquemment (mais pas universellement, et pas toujours avec le même fichier de ressources) une erreur de type IOException avec la trace de pile suivante :

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)

Questions :

  • Que se passe-t-il ici ?

  • Est-ce que je fais quelque chose de mal, avec la façon dont je lis/comprend ces fichiers de ressources (ou pouvez-vous suggérer des améliorations ?).

  • Que puis-je faire pour résoudre ce problème ?

Premières réflexions :

Au départ, comme je n'ai constaté le problème que lorsque le code était déployé dans un jar, j'ai pensé qu'il s'agissait d'un problème d'accès par l'intermédiaire de JarFile - Il se peut que les fichiers de ressources soient accédés par un serveur partagé. JarFile et que lorsque l'un de ces flux d'entrée de ressources est fermé, c'est-à-dire que l'on ferme l'application JarFile et qui ferme tous les autres flux d'entrée ouverts. Par exemple, il existe un flux d'entrée Question de l'OS présentant un comportement similaire (alors que le PO s'occupait directement de la JarFile s). De même, il y avait un rapport de bogue mais cela remonte à Java 6 et a apparemment été corrigé dans Java 7.

Mise à jour 1 :

Après un débogage plus approfondi, ce problème semble être dû au fait que l'analyseur XML ferme l'élément InputStream lorsqu'il a fini de l'analyser. (Cela me semble un peu étrange - en effet, cela a suscité ces questions en ce qui concerne l'application DOM y SAX mais c'est ainsi). En tant que tel, ma meilleure hypothèse actuelle est que l'élément SAXParser (ou en fait en bas de la XMLEntityManager ) appelle InputStream.close() mais il y a une sorte de condition de course à propos de l'état ?

Cela ne semble pas lié à l'utilisation de try-with-resources - c'est-à-dire, étant donné que le SAXParser ferme l'InputStream, j'ai essayé de supprimer try-with-resources, et j'obtiens toujours les mêmes erreurs/trace de pile.

Mise à jour 2 :

Après avoir débogué un peu plus, j'ai découvert que la fonction XMLEntityManager$RewindableInputStream est en cours de fermeture, antes de il a fini de lire le fichier XML. Il est intéressant de noter que je n'observe ce phénomène que dans un environnement fortement concurrent, mais je le constate toujours même si je place des verrous autour de tous nos chargements possibles de ressources XML - c'est-à-dire lorsqu'une seule ressource XML est lue à la fois.

La trace de pile de l'endroit où le XMLEntityManager$RewindableInputStream est fermé - antes de il a fini de lire le fichier - est le suivant :

  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)

Donc, pour l'instant, ma meilleure hypothèse (et ce n'est que cela) est qu'il y a un bogue de concurrence dans le gestionnaire de fichiers XML de Java, le flux d'entrée, etc. Peut-être un résultat de l'élision de synchro, peut-être ? (Si c'est le cas, je ne sais pas s'il s'agit d'un bogue préexistant qui n'a été révélé que par les améliorations de la concurrence dans Java 8 ou d'un nouveau bogue dans Java 8).

(Cela dit, je n'ai pas déposé de rapport de bogue, car je ne pense pas avoir assez d'éléments pour affirmer qu'il y a un bogue, ni assez d'informations pour informer quiconque le chercherait).

Contournement :

Étant donné que le problème provenait de l'utilisation des bibliothèques XML de Java, j'ai décidé d'écrire la mienne (largement basée sur StAX). Heureusement, nos fichiers de ressources XML sont assez simples et directs, de sorte que je n'ai eu besoin d'implémenter qu'une fraction de la fonctionnalité des analyseurs XML Java de base.

Mise à jour 3 :

La solution décrite ci-dessus a permis d'améliorer les choses, c'est-à-dire qu'elle a résolu les cas particuliers du problème que je rencontrais. Cependant, par la suite, j'ai constaté que j'obtenais encore des cas où un InputStream, provenant d'une ressource dans un JAR, était fermé alors qu'il était en cours de lecture. La trace de pile est maintenant la suivante :

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)

La recherche de problèmes liés à cette trace de pile m'a conduit à ceci question et de suggérer que je contrôle la URLConnection afin de ne pas mettre en cache les connexions pour qu'elles ne soient pas partagées : [URLConnection.setUseCaches(boolean)][6]

J'ai donc essayé ceci (voir la réponse ci-dessous pour la mise en œuvre) et cela semble fonctionner et être stable. Je suis même revenu en arrière et j'ai essayé cela avec mes précédents analyseurs Java StAX, et tout semblait fonctionner et être stable. (Par ailleurs, je suis actuellement indécis quant à la conservation de mes analyseurs XML personnalisés - ils semblent être un peu plus performants du fait qu'ils sont éclairés, mais c'est un compromis avec les exigences supplémentaires en matière de maintenance). Il ne s'agit donc probablement pas d'un bogue de concurrence dans les analyseurs XML Java de base, mais d'un problème lié aux chargeurs de classe dynamiques de la JVM.

Mise à jour 4 :

Je suis de plus en plus convaincu qu'il s'agit d'un bogue de concurrence dans le cœur de Java, en ce qui concerne la façon dont il gère l'accès aux fichiers de ressources, en tant que flux, à partir de jars. Par exemple, il y a ce problème dans org.reflections.reflections que j'ai également rencontrée.

J'ai également constaté ce problème dans le cas de JBLAS de telle sorte que j'obtiens l'exception suivante (et le fichier question soulevée) :

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)

1voto

amaidment Points 2349

Comme je l'explique dans la "Mise à jour 3", j'ai trouvé que la solution suivante était viable et stable :

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);
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X