Java Resource InputStream закрывается?

Я нахожусь в процессе переноса нашей кодовой базы Java с Java 7 (80) на Java 8 (162). (Да... мы на переднем крае технологий.)

После переключения у меня возникли проблемы с загрузкой файлов ресурсов XML из развернутых jar-файлов в среде с большим количеством параллельных операций. Доступ к файлам ресурсов осуществляется с помощью 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, но после развертывания в банке я часто (но не всегда и не всегда с одним и тем же файлом ресурсов) получаю 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)

Вопросы:

  • Что тут происходит?

  • Я делаю что-то не так, как я читаю/разбираю эти файлы ресурсов? (Или вы можете предложить улучшения?)

  • Что я могу сделать, чтобы решить эту проблему?

Первоначальные мысли:

Первоначально, поскольку я видел проблему только тогда, когда код был развернут в банке, я подумал, что это как-то связано с доступом через JarFile - возможно, к файлам ресурсов обращается общий JarFile, и что когда один из этих входов ресурсов потоки закрыты, то есть закрытие JarFile, и это закрытие всех других открытых входных потоков. Например, есть вопрос SO, показывающий похожее поведение (когда OP был напрямую обработка JarFiles). Кроме того, был похожий отчет об ошибке, но он вернулся в 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, или новая ошибка в Java 8.)

(Тем не менее, я не подавал отчет об ошибке, так как не думаю, что у меня есть достаточно, чтобы сказать, что есть ошибка, или достаточно информации, чтобы сообщить любому, кто будет ее искать.)

Решение:

Учитывая, что проблема заключалась в использовании основных библиотек Java 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)

person amaidment    schedule 06.04.2018    source источник
comment
Загрузка ресурсов — это работа ClassLoader. Может быть, ваша среда развертывания использует пользовательские загрузчики классов, которые все еще страдают от этой ошибки?   -  person Robert    schedule 06.04.2018
comment
Возможно, но я бы подумал, что маловероятно. Среда развертывания фактически заключается в том, что код вызывается из командной строки с определенной JRE, с некоторыми аргументами JVM, с путем к классам, основным классом и некоторыми аргументами, что мне кажется довольно ванильным.   -  person amaidment    schedule 06.04.2018
comment
Закрытие одного из входных потоков никогда не должно закрывать весь JarFile. Имейте в виду, что это также нарушит загрузку всего класса, поскольку поиск и чтение файлов классов работает точно так же.   -  person Holger    schedule 06.04.2018
comment
Есть ли ограничение на количество файлов, которые вы можете открыть? Если да, то что произойдет, если это число будет достигнуто? stackoverflow.com/questions/4289447/java-too-many -open-files и stackoverflow.com/questions/16360720/ просто догадываюсь   -  person Christian    schedule 06.04.2018
comment
На какой сервер вы развертываете? На какой операционной системе вы работаете?   -  person VGR    schedule 06.04.2018
comment
@VGR Windows и Unix   -  person amaidment    schedule 06.04.2018
comment
Вы получаете исключение, когда пытаетесь разделить один экземпляр ZipFile между разными потоками, а затем каждый поток пытается отдельно закрыть один и тот же ZipFile (вы можете закрыть ZipFile только один раз), или один поток пытается закрыть ZipFile, в то время как другой все еще пытается читать из него. На самом деле в любом случае нет смысла делиться одним экземпляром ZipFile между потоками, поскольку он накладывает синхронизированную блокировку на все свои методы. Моя библиотека FastClasspathScanner использует один экземпляр ZipFile для каждого потока, и я никогда не сталкивался с этой проблемой, так что это не ошибка JDK. Это ошибка в том, как вызывается ZipFile API.   -  person Luke Hutchison    schedule 08.07.2018
comment
@LukeHutchison - с самого начала вы заметите, что я не вызываю ZipFile API напрямую, а просто получаю доступ к ресурсам из .jar с помощью .getResourceAsStream(). Проблема, я думаю, в JDK, где он открывает соединение с URL-адресом ресурса, используя JNLPCachedJarURLConnection. Вы правы — вы можете решить эту проблему, используя один экземпляр ZipFile — хотя для этого потребуются дополнительные знания о том, в какой банке находится ресурс (что может быть разумным ожидать) — но я обнаружил, используя некэшированные соединения (см. мой оригинальный ответ), чтобы быть более чистой заменой.   -  person amaidment    schedule 11.07.2018
comment
Извините - кроме того, это вряд ли будет JNLPCachedJarURLConnection (что, вероятно, для JWS?), а обычное JarURLConnection. Но обратите внимание, что логическое значение URLConnection defaultUseCaches = true.   -  person amaidment    schedule 11.07.2018
comment
@amaidment Понятно, спасибо за разъяснение - да, я думаю, что именно так они исправили ошибку в Reflections, отключив кеширование. Я думаю, что я имел в виду, что если вы используете один экземпляр ZipFile для каждого потока, то все полученные вами InputStreams могут быть кэшированы или не кэшированы, это не имеет значения, но, по-видимому, API .getResourceAsStream() не ожидает потоки, которые будут разделены между потоками, когда они кэшируются, или что-то в этом роде? Если да, то да, я бы назвал это ошибкой JDK. На каждый поток должен быть один кеш.   -  person Luke Hutchison    schedule 12.07.2018
comment
Это выглядит актуально? bugs.openjdk.java.net/browse/JDK-8246714   -  person Graeme Moss    schedule 08.06.2020


Ответы (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);
} 
person amaidment    schedule 08.04.2018