Я пытаюсь прочитать содержимое моего собственного 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
Мне кажется это баг, но возможно я ошибаюсь.
Моя цель - прочитать записи каталога.
@ g00se Кажется, да, но почему вместо этого возвращается экземпляр InputStream
null
?
Нет никакой гарантии, какой поток Class::getResourceAsStream
вернется. Единственное, что вы знаете, это то, что поток можно использовать для чтения ресурса, если вызывающий объект может найти этот ресурс и получить к нему доступ. Тот факт, что поток возвращается для «ресурсов каталога», вообще не является документированным поведением и, как известно, не во всех случаях ведет себя одинаково.
Обратите внимание, что способ доступа к ресурсам зависит от реализации ClassLoader
. Для загрузчиков, поддерживающих модули, сам загрузчик обычно зависит от базовой реализации ModuleReader
именованных модулей. Доступ к ресурсам также зависит от механизма URL
, по крайней мере, при использовании getResource(String)
. Это означает, что это зависит от URLStreamHandler
, связанного с URL
, что может зависеть от настроенного URLStreamHandlerFactory
. И тогда это зависит от реализации URLConnection
, возвращаемой обработчиком.
Что касается разницы в поведении, которую вы наблюдаете, то при запуске вашего кода из NetBeans ваш код, скорее всего, запускается непосредственно из файловой системы (т. е. он не упаковывается сначала в файл JAR). Это означает, что вы получаете доступ к ресурсу через FileURLConnection
(внутренний класс). И этот класс, очевидно, реализовал getInputStream()
для возврата потока, который дает вам имена дочерних элементов каталога, когда файл является каталогом. Когда ваш код упакован в JAR, вы получаете JarURLConnection
, который работает по-другому. Хотя опять же, по большей части это все детали реализации.
Если ваша цель — составить список ресурсов, чтобы вы могли выбирать из них для загрузки, то вы можете использовать один трюк — создать из них индекс текстового файла и загрузить его. Это не проблема, поскольку вы будете знать, какие ресурсы у вас есть, когда построите его. Поскольку ресурсы доступны только для чтения, они изменятся только в том случае, если вы создадите их заново с другими. Ох, ты всегда знаешь, что у тебя там
Вы не можете указать каталог ресурсов, потому что на самом деле это не каталог. См. stackoverflow.com/questions/77863392/….
С причинами, почему это не работает, у вас все в порядке, но я все равно считаю, что возврат нефункционального InputStream
вместо null
или Exception
- это ошибка. Тем временем я нашел очень умное решение -> stackoverflow.com/a/32828953/5399598
В зависимости от того, какой инструмент вы использовали для создания файла jar, у вас будут записи нулевой длины для каталогов в файле. API ввода-вывода отражает именно это. Вы получите null
, если такой записи нет, или пустой входной поток, когда присутствует запись нулевой длины. Я не считаю использование Compiler API для такой задачи «очень разумным». Это все равно, что построить ракету, чтобы долететь до следующего супермаркета. Далее отметим, что актуальность «загрузчика контекста» — живучий миф. Метод getContextClassLoader()
потока просто вернет все, что было установлено через setContextClassLoader(…)
.
@Holger Спасибо за объяснение API ввода-вывода, который я не знал, где найти. Я создал JAR с помощью IDE NetBeans. Я думаю, они используют «обычный» jar-инструмент. Но какие альтернативы существуют? И какой инструмент вы предлагаете, если Compiler API не подходит?
Вы можете использовать API FileSystem
, который позволяет выполнять такие операции, как Files.list(…)
. См., например, этот ответ.
@Holger Спасибо за вдохновение. Немного исчерпывающе из-за большого использования потоков и отсутствия рекурсивного сбора. Использование меньшего количества потоков делает код более компактным и, возможно, более быстрым, см.: stackoverflow.com/a/78751388/5399598
@CoSoCo Самое замечательное в использовании API FileSystem — это то, что вы можете выбирать, что делать с путями. Если вы хотите выполнить рекурсию, просто используйте walk
вместо list
. Я не понимаю вашей точки зрения насчет потоков, вы все еще используете метод, возвращающий поток и охотно собирающий поток в список перед его итерацией, это не быстрее, чем итерация потока в первую очередь.
@Holger Возможность walk
я наблюдал. Спасибо за подсказку. Итерация потока в первую очередь становится утомительной, когда я хочу сделать больше, чем System.out.println(p);
, например. функция типа Class.forName()
, которая выдает исключения, которые я хочу обработать вне итерации потока. Также мне нужно определить дополнительный интерфейс и вызвать метод accept()
.
Другая проблема заключается в том, что существует более одного пути к классам (некоторые извлекают ресурсы из стандартных файлов, некоторые из файла JAR). Тогда Paths.get(MyClass.class.getResource("MyClass.class").toURI()).getParent()
недостаточно.
Дополнительный интерфейс в этом ответе предназначен для инкапсуляции действия, чтобы метод можно было повторно использовать для разных действий. Вот почему решение представлено как «наиболее общее решение». Это вообще не имеет ничего общего с Stream API. Если у вас есть только одна задача и вы не хотите повторно использовать метод, вы, конечно, можете опустить эту абстракцию. Тогда это уже не общее решение, но для вашей конкретной задачи оно, конечно, будет более кратким. Но как я мог предложить решение вашей конкретной задачи восемь лет назад? Вот почему в ответе используется абстракция.
@Holger Теперь я обновил свой ответ, используя Files.walk()
. К сожалению, невозможно 1. избежать проверки Files.isDirectory()
и 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
.
Кстати, url -> { urls.add(url); }
можно упростить до url -> urls.add(url)
или даже urls::add
. В качестве альтернативы walk
вы можете использовать walkFileTree и расширить SimpleFileVisitor
и переопределить visitFile, который вызывается только для файлов, не являющихся каталогами, поэтому тогда isDirectory
не требуется.
@Holger Спасибо за подсказку. Я правильно понимаю, что после Paths.get(uri)
я все еще использую File.separatorChar
, как это могло быть в Windows '\'
? Я изменю код соответственно.
@Holger Я подумывал использовать walkFileTree
, но не нашел хорошего примера, как закодировать FileVisitor
. У тебя есть один? Я не понимаю, какое действие поставить visitFile
.
@Holger В javadoc walkFileTree
я также прочитал: «Метод visitFile
вызывается для всех файлов, включая каталоги, встречающихся в maxDepth
...». Так почему же вы думаете, что каталоги пропускаются?
Каталоги не пропускаются, для каталогов вызываются методы preVisitDirectory
и postVisitDirectory
. И в документации прямо сказано: «Если файл не является каталогом, то метод visitFile
вызывается с атрибутами файла».
Paths.get(uri)
использует URI
, который будет использовать /
для иерархических URI. В случае Paths.get(String)
вы должны использовать символ, специфичный для платформы, то есть File.separatorChar
или FileSystems.getDefault().getSeparator()
. Однако у Windows не возникнет проблем, если вместо этого вы используете /
. Гораздо важнее оправдать ожидания (относительно того, как выглядит путь) при взаимодействии с пользователем…
@Holger Итак, документация противоречива, см. некоторые отрывки позже (Надеюсь, эта специальная ссылка для цитирования текста сработает для вас, поскольку Firefox ее игнорирует.)
В моем понимании preVisit...
и postVisit...
не означают visit...
. Так как vistitDirectory
нет, то похоже, что под visitFile
подразумеваются все файлы. А приведенная вами фраза описывает лишь один случай, но не исключает другие случаи, например. каталог.
«Если не идет дождь, я гуляю». это не значит, что я никогда не гуляю, когда идет дождь.
Предложение не противоречит, если не разрезать его так, как вы это сделали. В нем говорится: «Метод visitFile вызывается для всех файлов, включая каталоги, встречающихся в maxDepth», что является особым случаем. Если вы решите предоставить небольшое число для maxDepth
, чтобы его можно было когда-либо достичь (в отличие от значения по умолчанию Integer.MAX_VALUE
), тогда каталоги максимальной глубины, которые не будут пройдены, будут обрабатываться как обычные файлы. Я использую этот метод уже почти десятилетие, знаю, что он делает, не знаю, куда движутся ваши попытки обсудить формулировку.
@Holger Спасибо за исправление. Английский не мой родной язык, поэтому я неправильно истолковал это предложение. Итак, теперь я проверил walkFileTree
и могу подтвердить, что это работает, как вы говорите. С walkFileTree
код намного компактнее, чем с потоковым вариантом walk
.
Это невозможно. Вам нужно будет создать файловую систему zip