Как правильно читать из JarURLInputStream? (Джава)

Я пытаюсь прочитать содержимое моего собственного JAR.

С getResourceAsStream(path) я получаю sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream. Но этот поток кажется пустым, если путь — это каталог.

Это мой тестовый код:

package de.CoSoCo.testzone;

import java.io.*;

/**
 *
 * @author Ulf Zibis <[email protected]>
 * @version 
 */
public class ResourceAsStream {

  /**
   * @param args the command line arguments
   */
  public static void main(String[] args) {
    String packagePath = ResourceAsStream.class.getPackageName().replace('.', '/');
    String path;
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    Class c = ResourceAsStream.class;
    InputStream in;
    byte [] bytes = new byte[16];
    try {
      System.out.println("path = "+(
          path = packagePath));
      System.out.println("Class = "+(
          in = cl.getResourceAsStream(path)));
      BufferedReader r = new BufferedReader(new InputStreamReader(in));
      System.out.println("entries = ");
      for (String line; (line = r.readLine()) != null; )
        System.out.println("  "+line);
      in = cl.getResourceAsStream(path);
      System.out.println("read bytes = "+in.read(bytes));
    } catch (Exception ex) { System.out.println(ex); }
    try {
      System.out.println("path = "+(
          path = packagePath+'/'+"A_Picture.png"));
      System.out.println("Class = "+(
          in = cl.getResourceAsStream(path)));
      System.out.println("read bytes = "+in.read(bytes));
    } catch (Exception ex) { System.out.println(ex); }
    try {
      System.out.println("path = "+(
          path = ""));
      System.out.println("Class = "+(
          in = c.getResourceAsStream(path)));
      BufferedReader r = new BufferedReader(new InputStreamReader(in));
      System.out.println("entries = ");
      for (String line; (line = r.readLine()) != null; )
        System.out.println("  "+line);
      in = c.getResourceAsStream(path);
      System.out.println("read bytes = "+in.read(bytes));
    } catch (Exception ex) { System.out.println(ex); }
    try {
      System.out.println("path = "+(
          path = "A_Picture.png"));
      System.out.println("Class = "+(
          in = c.getResourceAsStream(path)));
      System.out.println("read bytes = "+in.read(bytes));
    } catch (Exception ex) { System.out.println(ex); }
  }
    
}

Когда я запускаю это с отдельными файлами классов, например. внутри моей IDE NetBeans — я получаю ожидаемые записи каталога:

path=de/CoSoCo/testzone
Class=java.io.ByteArrayInputStream@4e50df2e
entries=
  A_Picture.png
  ClassFinder.class
  ClassFinder$1.class
  FileChooserDemo.class
  FileTimesFromCalendar.class
  ResourceAsStream.class
read bytes=16
path=de/CoSoCo/testzone/A_Picture.png
Class=java.io.BufferedInputStream@3941a79c
read bytes=16
path=
Class=java.io.ByteArrayInputStream@506e1b77
entries=
  A_Picture.png
  ClassFinder.class
  ClassFinder$1.class
  FileChooserDemo.class
  FileTimesFromCalendar.class
  ResourceAsStream.class
read bytes=16
path=A_Picture.png
Class=java.io.BufferedInputStream@4fca772d
read bytes=16

Но когда я запускаю его из JAR, он терпит неудачу:

$ java -jar TestZone.jar
path=de/CoSoCo/testzone
Class=sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@3d4eac69
entries=
read bytes=-1
path=de/CoSoCo/testzone/A_Picture.png
Class=sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@135fbaa4
read bytes=16
path=
Class=sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@330bedb4
entries=
read bytes=-1
path=A_Picture.png
Class=sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@7ea987ac
read bytes=16

Мне кажется это баг, но возможно я ошибаюсь.

Моя цель - прочитать записи каталога.

Это невозможно. Вам нужно будет создать файловую систему zip

g00se 26.06.2024 23:00

@ g00se Кажется, да, но почему вместо этого возвращается экземпляр InputStreamnull?

CoSoCo 27.06.2024 00:47

Нет никакой гарантии, какой поток Class::getResourceAsStream вернется. Единственное, что вы знаете, это то, что поток можно использовать для чтения ресурса, если вызывающий объект может найти этот ресурс и получить к нему доступ. Тот факт, что поток возвращается для «ресурсов каталога», вообще не является документированным поведением и, как известно, не во всех случаях ведет себя одинаково.

Slaw 27.06.2024 01:28

Обратите внимание, что способ доступа к ресурсам зависит от реализации ClassLoader. Для загрузчиков, поддерживающих модули, сам загрузчик обычно зависит от базовой реализации ModuleReader именованных модулей. Доступ к ресурсам также зависит от механизма URL, по крайней мере, при использовании getResource(String). Это означает, что это зависит от URLStreamHandler, связанного с URL, что может зависеть от настроенного URLStreamHandlerFactory. И тогда это зависит от реализации URLConnection, возвращаемой обработчиком.

Slaw 27.06.2024 01:50

Что касается разницы в поведении, которую вы наблюдаете, то при запуске вашего кода из NetBeans ваш код, скорее всего, запускается непосредственно из файловой системы (т. е. он не упаковывается сначала в файл JAR). Это означает, что вы получаете доступ к ресурсу через FileURLConnection (внутренний класс). И этот класс, очевидно, реализовал getInputStream() для возврата потока, который дает вам имена дочерних элементов каталога, когда файл является каталогом. Когда ваш код упакован в JAR, вы получаете JarURLConnection, который работает по-другому. Хотя опять же, по большей части это все детали реализации.

Slaw 27.06.2024 01:57

Если ваша цель — составить список ресурсов, чтобы вы могли выбирать из них для загрузки, то вы можете использовать один трюк — создать из них индекс текстового файла и загрузить его. Это не проблема, поскольку вы будете знать, какие ресурсы у вас есть, когда построите его. Поскольку ресурсы доступны только для чтения, они изменятся только в том случае, если вы создадите их заново с другими. Ох, ты всегда знаешь, что у тебя там

g00se 27.06.2024 02:04

Вы не можете указать каталог ресурсов, потому что на самом деле это не каталог. См. stackoverflow.com/questions/77863392/….

VGR 27.06.2024 04:34

С причинами, почему это не работает, у вас все в порядке, но я все равно считаю, что возврат нефункционального InputStream вместо null или Exception - это ошибка. Тем временем я нашел очень умное решение -> stackoverflow.com/a/32828953/5399598

CoSoCo 28.06.2024 01:07

В зависимости от того, какой инструмент вы использовали для создания файла jar, у вас будут записи нулевой длины для каталогов в файле. API ввода-вывода отражает именно это. Вы получите null, если такой записи нет, или пустой входной поток, когда присутствует запись нулевой длины. Я не считаю использование Compiler API для такой задачи «очень разумным». Это все равно, что построить ракету, чтобы долететь до следующего супермаркета. Далее отметим, что актуальность «загрузчика контекста» — живучий миф. Метод getContextClassLoader() потока просто вернет все, что было установлено через setContextClassLoader(…).

Holger 01.07.2024 11:19

@Holger Спасибо за объяснение API ввода-вывода, который я не знал, где найти. Я создал JAR с помощью IDE NetBeans. Я думаю, они используют «обычный» jar-инструмент. Но какие альтернативы существуют? И какой инструмент вы предлагаете, если Compiler API не подходит?

CoSoCo 02.07.2024 17:46

Вы можете использовать API FileSystem, который позволяет выполнять такие операции, как Files.list(…). См., например, этот ответ.

Holger 03.07.2024 09:50

@Holger Спасибо за вдохновение. Немного исчерпывающе из-за большого использования потоков и отсутствия рекурсивного сбора. Использование меньшего количества потоков делает код более компактным и, возможно, более быстрым, см.: stackoverflow.com/a/78751388/5399598

CoSoCo 15.07.2024 20:26

@CoSoCo Самое замечательное в использовании API FileSystem — это то, что вы можете выбирать, что делать с путями. Если вы хотите выполнить рекурсию, просто используйте walk вместо list. Я не понимаю вашей точки зрения насчет потоков, вы все еще используете метод, возвращающий поток и охотно собирающий поток в список перед его итерацией, это не быстрее, чем итерация потока в первую очередь.

Holger 16.07.2024 13:15

@Holger Возможность walk я наблюдал. Спасибо за подсказку. Итерация потока в первую очередь становится утомительной, когда я хочу сделать больше, чем System.out.println(p);, например. функция типа Class.forName(), которая выдает исключения, которые я хочу обработать вне итерации потока. Также мне нужно определить дополнительный интерфейс и вызвать метод accept().

CoSoCo 23.07.2024 01:12

Другая проблема заключается в том, что существует более одного пути к классам (некоторые извлекают ресурсы из стандартных файлов, некоторые из файла JAR). Тогда Paths.get(MyClass.class.getResource("MyClass.class").toURI()‌​).getParent() недостаточно.

CoSoCo 23.07.2024 01:28

Дополнительный интерфейс в этом ответе предназначен для инкапсуляции действия, чтобы метод можно было повторно использовать для разных действий. Вот почему решение представлено как «наиболее общее решение». Это вообще не имеет ничего общего с Stream API. Если у вас есть только одна задача и вы не хотите повторно использовать метод, вы, конечно, можете опустить эту абстракцию. Тогда это уже не общее решение, но для вашей конкретной задачи оно, конечно, будет более кратким. Но как я мог предложить решение вашей конкретной задачи восемь лет назад? Вот почему в ответе используется абстракция.

Holger 23.07.2024 10:06

@Holger Теперь я обновил свой ответ, используя Files.walk(). К сожалению, невозможно 1. избежать проверки Files.isDirectory() и 2. избежать перехода по ссылкам.

CoSoCo 24.07.2024 19:23
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
17
122
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вот решение с использованием StandardJavaFileManager, которое находит все ресурсы, а также все классы из пакета и его подпакетов. Он работает с файлами файловой системы и JAR-пакетами, а также смешанными.

package de.CoSoCo.testzone;

import java.io.*;
import java.util.*;
import javax.tools.*;
import static javax.tools.JavaFileObject.Kind.*;

/**
 *
 * @author Ulf Zibis <[email protected]>
 * @version 
 */
public class ResourceList {

  // A sorted set of resource names of this package:
  protected static final Set<String> RESOURCES = new TreeSet<>();
  protected static final Set<Class> CLASSES // A sorted set of subclasses of this package
      = new TreeSet<>((Class o1, Class o2) -> o1.getName().compareTo(o2.getName()));

  /**
   * Scans all classes accessible from the context class loader which belong to
   * the current package and subpackages.
   */
  static {
    String packageName = ResourceList.class.getPackageName();
    StandardJavaFileManager fm = ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null);
    try {
      for (JavaFileObject file : fm.list(StandardLocation.CLASS_PATH, packageName,
          new TreeSet<>(Arrays.asList(SOURCE, CLASS, HTML, OTHER)), true)) {
        RESOURCES.add(file.getName()
            .replaceAll(".*("+packageName.replace('.', File.separatorChar)+".*?)\\)*$", "$1"));
      }
      for (JavaFileObject file : fm.list(StandardLocation.CLASS_PATH, packageName,
          Collections.singleton(CLASS), true)) {
        try {
          CLASSES.add(Class.forName(file.getName().replace(File.separatorChar, '.')
            .replaceAll(".*("+packageName+".*)\\.class.*", "$1")));
        } catch (ClassNotFoundException | NoClassDefFoundError ex) { System.err.println(ex); }
      }
    } catch (Exception ex) { ex.printStackTrace(); }
  }

  /**
   * @param args the command line arguments
   */
  public static void main(String[] args) {
    for (String str : RESOURCES)
      System.out.println("resource name "+str);
    for (Class clazz : CLASSES)
      System.out.println(clazz);
  }
    
}
Ответ принят как подходящий

Вот еще одно решение без использования StandardJavaFileManager, которое рекурсивно находит все ресурсы, а также все классы из пакета и его подпакетов. Он работает с файлами файловой системы и JAR-пакетами, а также смешанными.

Чтобы избежать проверки Files.isDirectory(), используйте Files.walkFileTree().

package de.CoSoCo.testzone;

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

/**
 *
 * @author Ulf Zibis <[email protected]>
 * @version 
 */
public class ResourceList {

  private static final String PACKAGE = ResourceList.class.getPackageName();
  // A sorted set of resource names of this package:
  protected static final Set<String> RESOURCES = new TreeSet<>();
  protected static final Set<Class> CLASSES // A sorted set of subclasses of this package
      = new TreeSet<>((Class o1, Class o2) -> o1.getName().compareTo(o2.getName()));

  /**
   * Scans all resources accessible from the context class loader which belong to
   * the current package and subpackages.
   */
  static {
    try {
      ResourceList.class.getClassLoader().getResources(PACKAGE.replace('.', '/'))
          .asIterator().forEachRemaining(url -> {
        try {
          collectResources(Paths.get(url.toURI()));
        } catch (FileSystemNotFoundException e) {
          try (FileSystem fs = FileSystems.newFileSystem(url.toURI(), Collections.<String, Object>emptyMap())) {
            collectResources(Paths.get(url.toURI()));
          } catch (Exception ex) { ex.printStackTrace(); }
        } catch (Exception ex) { ex.printStackTrace(); }
      });
    } catch (Exception ex) { ex.printStackTrace(); }
  }

  private static void collectResources(Path path) throws IOException {
    try (var stream = Files.walk(path, FileVisitOption.FOLLOW_LINKS)) {
      stream.forEach(p -> {
        if (Files.isDirectory(p, (LinkOption)null))  return;
        RESOURCES.add(p.toString());
        try {
          String name = p.toString().replace(File.separatorChar, '.').replaceAll(".*(" + PACKAGE + ".*)", "$1");
          if (name.endsWith(".class")) {
            CLASSES.add(Class.forName(name.substring(0, name.length() - ".class".length())));
          }
        } catch (ClassNotFoundException | NoClassDefFoundError ex) { System.err.println(ex); }
      });
    }
  }

  // Alternative:
  private static void collectResources2(Path path) throws IOException {
    Files.walkFileTree(path, new SimpleFileVisitor<>() {
      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        RESOURCES.add(file.toString());
        String name = file.toString().replace(File.separatorChar, '.').replaceAll(".*(" + PACKAGE + ".*)", "$1");
        if (name.endsWith(".class"))  try {
          CLASSES.add(Class.forName(name.substring(0, name.length() - ".class".length())));
        } catch (ClassNotFoundException | NoClassDefFoundError ex) { System.err.println(ex); }
        return FileVisitResult.CONTINUE;
      }
    });
  }

  /**
   * @param args the command line arguments
   */
  public static void main(String[] args) {
    for (String str : RESOURCES)
      System.out.println("resource path "+str);
    for (Class clazz : CLASSES)
      System.out.println(clazz);
  }
}

Не используйте Thread.currentThread().getContextClassLoader(). Если вы хотите посещать занятия в (под)пакетах пакета ResourceList.class, вам следует использовать ResourceList.class.getClassLoader(). Загрузчик классов контекста — это свойство, которому любой может присвоить произвольное значение, совершенно не связанное с пакетом(ами), который вы ищете. Кроме того, разделителем getResources должен быть '/', а не File.separatorChar.

Holger 26.07.2024 15:35

Кстати, url -> { urls.add(url); } можно упростить до url -> urls.add(url) или даже urls::add. В качестве альтернативы walk вы можете использовать walkFileTree и расширить SimpleFileVisitor и переопределить visitFile, который вызывается только для файлов, не являющихся каталогами, поэтому тогда isDirectory не требуется.

Holger 26.07.2024 15:40

@Holger Спасибо за подсказку. Я правильно понимаю, что после Paths.get(uri) я все еще использую File.separatorChar, как это могло быть в Windows '\'? Я изменю код соответственно.

CoSoCo 28.07.2024 18:00

@Holger Я подумывал использовать walkFileTree, но не нашел хорошего примера, как закодировать FileVisitor. У тебя есть один? Я не понимаю, какое действие поставить visitFile.

CoSoCo 28.07.2024 19:12

@Holger В javadoc walkFileTree я также прочитал: «Метод visitFile вызывается для всех файлов, включая каталоги, встречающихся в maxDepth ...». Так почему же вы думаете, что каталоги пропускаются?

CoSoCo 28.07.2024 19:35

Каталоги не пропускаются, для каталогов вызываются методы preVisitDirectory и postVisitDirectory. И в документации прямо сказано: «Если файл не является каталогом, то метод visitFile вызывается с атрибутами файла».

Holger 29.07.2024 10:52
Paths.get(uri) использует URI, который будет использовать / для иерархических URI. В случае Paths.get(String) вы должны использовать символ, специфичный для платформы, то есть File.separatorChar или FileSystems.getDefault().getSeparator(). Однако у Windows не возникнет проблем, если вместо этого вы используете /. Гораздо важнее оправдать ожидания (относительно того, как выглядит путь) при взаимодействии с пользователем…
Holger 29.07.2024 10:57

@Holger Итак, документация противоречива, см. некоторые отрывки позже (Надеюсь, эта специальная ссылка для цитирования текста сработает для вас, поскольку Firefox ее игнорирует.)

CoSoCo 29.07.2024 13:44

В моем понимании preVisit... и postVisit... не означают visit.... Так как vistitDirectory нет, то похоже, что под visitFile подразумеваются все файлы. А приведенная вами фраза описывает лишь один случай, но не исключает другие случаи, например. каталог.

CoSoCo 29.07.2024 13:54

«Если не идет дождь, я гуляю». это не значит, что я никогда не гуляю, когда идет дождь.

CoSoCo 29.07.2024 14:02

Предложение не противоречит, если не разрезать его так, как вы это сделали. В нем говорится: «Метод visitFile вызывается для всех файлов, включая каталоги, встречающихся в maxDepth», что является особым случаем. Если вы решите предоставить небольшое число для maxDepth, чтобы его можно было когда-либо достичь (в отличие от значения по умолчанию Integer.MAX_VALUE), тогда каталоги максимальной глубины, которые не будут пройдены, будут обрабатываться как обычные файлы. Я использую этот метод уже почти десятилетие, знаю, что он делает, не знаю, куда движутся ваши попытки обсудить формулировку.

Holger 29.07.2024 15:44

@Holger Спасибо за исправление. Английский не мой родной язык, поэтому я неправильно истолковал это предложение. Итак, теперь я проверил walkFileTree и могу подтвердить, что это работает, как вы говорите. С walkFileTree код намного компактнее, чем с потоковым вариантом walk.

CoSoCo 29.07.2024 23:55

Другие вопросы по теме