Как вывести список файлов внутри JAR-файла?

114

У меня есть этот код, который читает все файлы из каталога.

    File textFolder = new File("text_directory");

    File [] texFiles = textFolder.listFiles( new FileFilter() {
           public boolean accept( File file ) {
               return file.getName().endsWith(".txt");
           }
    });

Отлично работает. Он заполняет массив всеми файлами, которые заканчиваются на «.txt» из каталога «text_directory».

Как я могу прочитать содержимое каталога аналогичным образом в пределах файле JAR?

Итак, что я действительно хочу сделать, это перечислить все изображения в моем файле JAR, чтобы я мог загрузить их с помощью:

ImageIO.read(this.getClass().getResource("CompanyLogo.png"));

(Этот работает, потому что «CompanyLogo» «жестко запрограммирован», но количество изображений в файле JAR может быть от 10 до 200 переменной длины.)

РЕДАКТИРОВАТЬ

Итак, я предполагаю, что моей главной проблемой будет: как узнать имя файла JAR, в котором находится мой основной класс?

Конечно, я мог прочитать это, используя java.util.Zip .

Моя структура такая:

Они похожи на:

my.jar!/Main.class
my.jar!/Aux.class
my.jar!/Other.class
my.jar!/images/image01.png
my.jar!/images/image02a.png
my.jar!/images/imwge034.png
my.jar!/images/imagAe01q.png
my.jar!/META-INF/manifest 

Прямо сейчас я могу загрузить, например, "images / image01.png", используя:

    ImageIO.read(this.getClass().getResource("images/image01.png));

Но только потому, что я знаю имя файла, в остальном мне приходится загружать их динамически.

OscarRyz
источник
Просто мысль - почему бы не заархивировать / jar изображения в отдельный файл и прочитать записи в нем из вашего класса в другой jar?
Vineet Reynolds,
3
Потому что для распространения / установки потребуется «дополнительный» шаг. :( Знаешь, конечные пользователи.
OscarRyz
Учитывая, что вы создали банку, вы также можете включить в нее список файлов, а не пытаться использовать какие-либо трюки.
Том Хотин - tackline
Что ж, могу ошибаться, но банки могут быть встроены в другие банки. На этой основе работает решение для упаковки в одну банку (TM) ibm.com/developerworks/java/library/j-onejar . За исключением того, что в вашем случае вам не требуются классы нагрузки способностей.
Vineet Reynolds,

Ответы:

91
CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();
if (src != null) {
  URL jar = src.getLocation();
  ZipInputStream zip = new ZipInputStream(jar.openStream());
  while(true) {
    ZipEntry e = zip.getNextEntry();
    if (e == null)
      break;
    String name = e.getName();
    if (name.startsWith("path/to/your/dir/")) {
      /* Do something with this entry. */
      ...
    }
  }
} 
else {
  /* Fail... */
}

Обратите внимание, что в Java 7 вы можете создать FileSystemфайл из JAR (zip) файла, а затем использовать механизмы обхода и фильтрации каталогов NIO для поиска в нем. Это упростило бы написание кода, обрабатывающего JAR-файлы и "развернутые" каталоги.

Эриксон
источник
эй, спасибо ... уже несколько часов искал способ сделать это !!
Newtopian
9
Да, этот код работает, если мы хотим перечислить все записи внутри этого файла jar. Но если я просто хочу указать подкаталог внутри jar, например, example.jar / dir1 / dir2 / , как я могу напрямую перечислить все файлы внутри этого подкаталога? Или мне нужно разархивировать этот jar файл? Я очень ценю вашу помощь!
Ensom Hodder
Упомянутый подход Java 7 указан в ответе @acheron55 .
Вадим
@Vadzim Вы уверены, что ответ acheron55 предназначен для Java 7? Я не нашел Files.walk () или java.util.Stream в Java 7, но в Java 8: docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
Брюс Вс
@BruceSun, в java 7 вы можете вместо этого использовать Files.walkFileTree (...) .
Vadzim
80

Код, который работает как для файлов IDE, так и для файлов .jar:

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class ResourceWalker {
    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        Path myPath;
        if (uri.getScheme().equals("jar")) {
            FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap());
            myPath = fileSystem.getPath("/resources");
        } else {
            myPath = Paths.get(uri);
        }
        Stream<Path> walk = Files.walk(myPath, 1);
        for (Iterator<Path> it = walk.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}
ахерон55
источник
5
FileSystems.newFileSystem()принимает Map<String, ?>, поэтому вам нужно указать, Collections.emptyMap()что он должен вернуть типизированный соответствующим образом. Это работает: Collections.<String, Object>emptyMap().
Zero3
6
Фантастика!!! но URI uri = MyClass.class.getResource ("/ resources"). toURI (); должен иметь MyClass.class.getClassLoader (). getResource ("/ resources"). toURI (); то есть getClassLoader (). В противном случае у меня это не сработало.
EMM
8
Не забудьте закрыть fileSystem!
gmjonker
3
Это должен быть первый ответ для версии 1.8 ( walkметод Filesдоступен только в версии 1.8). Единственная проблема заключается в том, что в папке отображается каталог ресурсов Files.walk(myPath, 1), а не только файлы. Думаю, первый элемент можно просто проигнорировать
toto_tico
4
myPath = fileSystem.getPath("/resources");у меня не работает; ничего не находит. В моем случае это должны быть «изображения», а каталог «изображений» определенно включен в мою банку!
phip1611
21

Эриксона ответ работал отлично:

Вот рабочий код.

CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();
List<String> list = new ArrayList<String>();

if( src != null ) {
    URL jar = src.getLocation();
    ZipInputStream zip = new ZipInputStream( jar.openStream());
    ZipEntry ze = null;

    while( ( ze = zip.getNextEntry() ) != null ) {
        String entryName = ze.getName();
        if( entryName.startsWith("images") &&  entryName.endsWith(".png") ) {
            list.add( entryName  );
        }
    }

 }
 webimages = list.toArray( new String[ list.size() ] );

И я только что изменил свой метод загрузки следующим образом:

File[] webimages = ... 
BufferedImage image = ImageIO.read(this.getClass().getResource(webimages[nextIndex].getName() ));

К этому:

String  [] webimages = ...

BufferedImage image = ImageIO.read(this.getClass().getResource(webimages[nextIndex]));
OscarRyz
источник
9

Я хотел бы расширить ответ acheron55 , поскольку это очень небезопасное решение по нескольким причинам:

  1. Он не закрывает FileSystemобъект.
  2. Он не проверяет, существует ли FileSystemуже объект.
  3. Это не потокобезопасный.

Это несколько более безопасное решение:

private static ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();

public void walk(String path) throws Exception {

    URI uri = getClass().getResource(path).toURI();
    if ("jar".equals(uri.getScheme()) {
        safeWalkJar(path, uri);
    } else {
        Files.walk(Paths.get(path));
    }
}

private void safeWalkJar(String path, URI uri) throws Exception {

    synchronized (getLock(uri)) {    
        // this'll close the FileSystem object at the end
        try (FileSystem fs = getFileSystem(uri)) {
            Files.walk(fs.getPath(path));
        }
    }
}

private Object getLock(URI uri) {

    String fileName = parseFileName(uri);  
    locks.computeIfAbsent(fileName, s -> new Object());
    return locks.get(fileName);
}

private String parseFileName(URI uri) {

    String schemeSpecificPart = uri.getSchemeSpecificPart();
    return schemeSpecificPart.substring(0, schemeSpecificPart.indexOf("!"));
}

private FileSystem getFileSystem(URI uri) throws IOException {

    try {
        return FileSystems.getFileSystem(uri);
    } catch (FileSystemNotFoundException e) {
        return FileSystems.newFileSystem(uri, Collections.<String, String>emptyMap());
    }
}   

Нет никакой реальной необходимости синхронизировать по имени файла; можно просто синхронизировать каждый раз один и тот же объект (или создать метод synchronized), это чисто оптимизация.

Я бы сказал, что это все еще проблемное решение, поскольку в коде могут быть другие части, которые используют FileSystemинтерфейс для тех же файлов, и это может мешать им (даже в однопоточном приложении).
Кроме того, он не проверяет nulls (например, on getClass().getResource().

Этот конкретный интерфейс Java NIO ужасен, так как он представляет глобальный / одноэлементный небезопасный ресурс, а его документация чрезвычайно расплывчата (много неизвестных из-за реализаций, специфичных для поставщика). Результаты могут отличаться для других FileSystemпоставщиков (не для JAR). Может быть, для этого есть веская причина; Я не знаю, я не исследовал реализации.

Эяль Рот
источник
1
Синхронизация внешних ресурсов, как и ФС, не имеет большого смысла внутри одной ВМ. К нему могут обращаться другие приложения за пределами вашей виртуальной машины. Кроме того, даже внутри вашего собственного приложения вашу блокировку на основе имен файлов можно легко обойти. При этом лучше полагаться на механизмы синхронизации ОС, например на блокировку файлов.
Эспиноза
@Espinosa Механизм блокировки имени файла можно полностью обойти; мой ответ тоже недостаточно безопасен, но я считаю, что это максимум, что вы можете получить с Java NIO с минимальными усилиями. Полагаться на ОС для управления блокировками или не контролировать, какие приложения получают доступ к каким файлам, - это плохая практика, ИМХО, если вы не создаете приложение на основе клиента, например, текстовый редактор. Если вы не управляете блокировками самостоятельно, это приведет либо к возникновению исключений, либо к блокированию приложения потоками - и того и другого следует избегать.
Рот
8

Итак, я думаю, моя основная проблема будет в том, как узнать имя банки, в которой живет мой основной класс.

Предполагая, что ваш проект упакован в Jar (не обязательно верно!), Вы можете использовать ClassLoader.getResource () или findResource () с именем класса (за которым следует .class), чтобы получить jar, содержащий данный класс. Вам нужно будет проанализировать имя банки из возвращаемого URL (не так сложно), что я оставлю в качестве упражнения для читателя :-)

Обязательно проверьте случай, когда класс не является частью jar.

Кевин Дэй
источник
1
да - интересно, что это было бы пониженным без комментариев ... Мы используем вышеупомянутую технику все время, и она отлично работает.
Кевин Дэй
Старая проблема, но мне кажется, что это отличный способ взлома. Голосование снова до нуля :)
Туукка Мустонен
Проголосовали за, потому что это единственное решение, перечисленное здесь, для случая, когда класс не имеет CodeSource.
Восстановить Монику 2331977
7

Я портировал ответ acheron55 на Java 7 и закрыл FileSystemобъект. Этот код работает в IDE, в файлах jar и в jar во время войны с Tomcat 7; но обратите внимание, что он не работает в банке внутри войны на JBoss 7 (дает FileSystemNotFoundException: Provider "vfs" not installed, см. также этот пост ). Кроме того, как и исходный код, он не является потокобезопасным, как предполагает errr . По этим причинам я отказался от этого решения; однако, если вы согласны с этими проблемами, вот мой готовый код:

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

public class ResourceWalker {

    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        System.out.println("Starting from: " + uri);
        try (FileSystem fileSystem = (uri.getScheme().equals("jar") ? FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap()) : null)) {
            Path myPath = Paths.get(uri);
            Files.walkFileTree(myPath, new SimpleFileVisitor<Path>() { 
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println(file);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }
}
Pino
источник
5

Вот метод, который я написал для «запуска всех JUnits в пакете». Вы сможете адаптировать его к своим потребностям.

private static void findClassesInJar(List<String> classFiles, String path) throws IOException {
    final String[] parts = path.split("\\Q.jar\\\\E");
    if (parts.length == 2) {
        String jarFilename = parts[0] + ".jar";
        String relativePath = parts[1].replace(File.separatorChar, '/');
        JarFile jarFile = new JarFile(jarFilename);
        final Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            final String entryName = entry.getName();
            if (entryName.startsWith(relativePath)) {
                classFiles.add(entryName.replace('/', File.separatorChar));
            }
        }
    }
}

Изменить: Ах, в этом случае вам может понадобиться этот фрагмент (тот же вариант использования :))

private static File findClassesDir(Class<?> clazz) {
    try {
        String path = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
        final String codeSourcePath = URLDecoder.decode(path, "UTF-8");
        final String thisClassPath = new File(codeSourcePath, clazz.getPackage().getName().repalce('.', File.separatorChar));
    } catch (UnsupportedEncodingException e) {
        throw new AssertionError("impossible", e);
    }
}
Ран Бирон
источник
1
Я думаю, что большая проблема заключается в том, чтобы узнать имя файла jar в первую очередь. Это сосуд, в котором живет главный класс.
OscarRyz
5

Вот пример использования библиотеки Reflections для рекурсивного сканирования пути к классам с помощью шаблона имени регулярного выражения, дополненного парой привилегий Guava для получения содержимого ресурсов:

Reflections reflections = new Reflections("com.example.package", new ResourcesScanner());
Set<String> paths = reflections.getResources(Pattern.compile(".*\\.template$"));

Map<String, String> templates = new LinkedHashMap<>();
for (String path : paths) {
    log.info("Found " + path);
    String templateName = Files.getNameWithoutExtension(path);
    URL resource = getClass().getClassLoader().getResource(path);
    String text = Resources.toString(resource, StandardCharsets.UTF_8);
    templates.put(templateName, text);
}

Это работает как с банками, так и с разнесенными классами.

Вадим
источник
Помните, что отражения по-прежнему не поддерживают Java 9 и выше: github.com/ronmamo/reflections/issues/186 . Там есть ссылки на конкурирующие библиотеки.
Вадим
3

Файл jar - это просто zip-файл со структурированным манифестом. Вы можете открыть файл jar с помощью обычных инструментов java zip и таким образом просканировать содержимое файла, раздуть потоки и т. Д. Затем использовать это в вызове getResourceAsStream, и все должно получиться очень круто.

РЕДАКТИРОВАТЬ / после уточнения

Мне потребовалась минута, чтобы вспомнить все мелочи, и я уверен, что есть более чистые способы сделать это, но я хотел убедиться, что я не сумасшедший. В моем проекте image.jpg - это файл в некоторой части основного файла jar. Я получаю загрузчик классов основного класса (SomeClass - это точка входа) и использую его для обнаружения ресурса image.jpg. Затем немного магии потока, чтобы поместить его в эту штуку ImageInputStream, и все в порядке.

InputStream inputStream = SomeClass.class.getClassLoader().getResourceAsStream("image.jpg");
JPEGImageReaderSpi imageReaderSpi = new JPEGImageReaderSpi();
ImageReader ir = imageReaderSpi.createReaderInstance();
ImageInputStream iis = new MemoryCacheImageInputStream(inputStream);
ir.setInput(iis);
....
ir.read(0); //will hand us a buffered image
Mikeb
источник
Эта банка содержит основную программу и ресурсы. Как я могу сослаться на банку self? из файла jar?
OscarRyz
Чтобы обратиться к файлу JAR, просто используйте «blah.JAR» в качестве строки. Вы можете использовать, например, new File("blah.JAR")для создания объекта File, который представляет JAR. Просто замените «blah.JAR» на имя вашего JAR.
Томас Оуэнс,
Если это та же самая банка, из которой у вас уже закончились, загрузчик классов должен иметь возможность видеть вещи внутри банки ... Я неправильно понял, что вы пытались сделать изначально.
Mikeb
2
Ну да, это у меня уже есть, проблема в том, что мне нужно что-то вроде: "... getResourceAsStream (" *. Jpg "); ..." То есть динамически перечислить содержащиеся файлы.
OscarRyz
3

Учитывая фактический файл JAR, вы можете перечислить его содержимое, используя JarFile.entries(). Тем не менее, вам нужно будет знать местоположение файла JAR - вы не можете просто попросить загрузчик классов перечислить все, что он может получить.

Вы должны иметь возможность определить местоположение файла JAR на основе возвращенного URL-адреса ThisClassName.class.getResource("ThisClassName.class"), но это может быть немного неудобно.

Джон Скит
источник
Читая ваш ответ, возник еще один вопрос. Что даст вызов: this.getClass (). GetResource ("/ my_directory"); Он должен вернуть URL-адрес, который, в свою очередь, может быть .... использован в качестве каталога? Нет ... позволь мне попробовать.
OscarRyz
Вы всегда знаете, где находится JAR - он находится в "." Пока известно имя JAR, вы можете где-нибудь использовать константу String. Теперь, если люди изменят название JAR ...
Томас Оуэнс,
@Thomas: Предполагается, что вы запускаете приложение из текущего каталога. Что не так с "java -jar foo / bar / baz.jar"?
Джон Скит,
Я считаю (и должен был бы проверить), что если бы у вас был код в new File("baz.jar)вашем JAR, объект File представлял бы ваш JAR-файл.
Томас Оуэнс,
@ Томас: Я так не думаю. Я считаю, что это будет относиться к текущему рабочему каталогу процесса. Хотя мне бы тоже пришлось проверить :)
Джон Скит
3

Некоторое время назад я сделал функцию, которая получает классы из JAR:

public static Class[] getClasses(String packageName) 
throws ClassNotFoundException{
    ArrayList<Class> classes = new ArrayList<Class> ();

    packageName = packageName.replaceAll("\\." , "/");
    File f = new File(jarName);
    if(f.exists()){
        try{
            JarInputStream jarFile = new JarInputStream(
                    new FileInputStream (jarName));
            JarEntry jarEntry;

            while(true) {
                jarEntry=jarFile.getNextJarEntry ();
                if(jarEntry == null){
                    break;
                }
                if((jarEntry.getName ().startsWith (packageName)) &&
                        (jarEntry.getName ().endsWith (".class")) ) {
                    classes.add(Class.forName(jarEntry.getName().
                            replaceAll("/", "\\.").
                            substring(0, jarEntry.getName().length() - 6)));
                }
            }
        }
        catch( Exception e){
            e.printStackTrace ();
        }
        Class[] classesA = new Class[classes.size()];
        classes.toArray(classesA);
        return classesA;
    }else
        return null;
}
Berni
источник
2
public static ArrayList<String> listItems(String path) throws Exception{
    InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(path);
    byte[] b = new byte[in.available()];
    in.read(b);
    String data = new String(b);
    String[] s = data.split("\n");
    List<String> a = Arrays.asList(s);
    ArrayList<String> m = new ArrayList<>(a);
    return m;
}
Феликс Г.
источник
3
Хотя этот фрагмент кода может решить проблему, он не объясняет, почему и как он отвечает на вопрос. Пожалуйста, включите объяснение вашего кода , так как это действительно помогает улучшить качество вашего сообщения. Помните, что вы отвечаете на вопрос читателей в будущем, и эти люди могут не знать причины вашего предложения кода.
Самуэль Филипп
данные пусты, когда мы выполняем код из файла jar.
Aguid
1

Наиболее надежным механизмом для перечисления всех ресурсов в пути к классам в настоящее время является использование этого шаблона с ClassGraph , поскольку он обрабатывает максимально широкий спектр механизмов спецификации пути к классам , включая новую модульную систему JPMS. (Я автор ClassGraph.)

Как узнать имя файла JAR, в котором находится мой основной класс?

URI mainClasspathElementURI;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("x.y.z")
        .enableClassInfo().scan()) {
    mainClasspathElementURI =
            scanResult.getClassInfo("x.y.z.MainClass").getClasspathElementURI();
}

Как я могу аналогичным образом прочитать содержимое каталога в файле JAR?

List<String> classpathElementResourcePaths;
try (ScanResult scanResult = new ClassGraph().overrideClasspath(mainClasspathElementURI)
        .scan()) {
    classpathElementResourcePaths = scanResult.getAllResources().getPaths();
}

Есть много других способов работать с ресурсами .

Люк Хатчисон
источник
1
Очень хороший пакет, легко использовать в моем проекте Scala, спасибо.
zslim
0

Просто другой способ перечисления / чтения файлов из URL-адреса jar, и он делает это рекурсивно для вложенных jar-файлов.

https://gist.github.com/trung/2cd90faab7f75b3bcbaa

URL urlResource = Thead.currentThread().getContextClassLoader().getResource("foo");
JarReader.read(urlResource, new InputStreamCallback() {
    @Override
    public void onFile(String name, InputStream is) throws IOException {
        // got file name and content stream 
    }
});
Чунг
источник
0

Еще один в дорогу:

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

import static java.nio.file.FileSystems.newFileSystem;
import static java.util.Collections.emptyMap;

public class ResourceWalker {
  private static final PathMatcher FILE_MATCHER =
      FileSystems.getDefault().getPathMatcher( "glob:**.ttf" );

  public static List<Path> walk( final String directory )
      throws URISyntaxException, IOException {
    final List<Path> filenames = new ArrayList<>();
    final var resource = ResourceWalker.class.getResource( directory );

    if( resource != null ) {
      final var uri = resource.toURI();
      final var path = uri.getScheme().equals( "jar" )
          ? newFileSystem( uri, emptyMap() ).getPath( directory )
          : Paths.get( uri );
      final var walk = Files.walk( path, 10 );

      for( final var it = walk.iterator(); it.hasNext(); ) {
        final Path p = it.next();
        if( FILE_MATCHER.matches( p ) ) {
          filenames.add( p );
        }
      }
    }

    return filenames;
  }
}

Это немного более гибко для сопоставления конкретных имен файлов, потому что он использует подстановочные знаки.


Более функциональный стиль:

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.function.Consumer;

import static java.nio.file.FileSystems.newFileSystem;
import static java.util.Collections.emptyMap;

/**
 * Responsible for finding file resources.
 */
public class ResourceWalker {
  private static final PathMatcher FILE_MATCHER =
      FileSystems.getDefault().getPathMatcher( "glob:**.ttf" );

  public static void walk( final String dirName, final Consumer<Path> f )
      throws URISyntaxException, IOException {
    final var resource = ResourceWalker.class.getResource( dirName );

    if( resource != null ) {
      final var uri = resource.toURI();
      final var path = uri.getScheme().equals( "jar" )
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
          : Paths.get( uri );
      final var walk = Files.walk( path, 10 );

      for( final var it = walk.iterator(); it.hasNext(); ) {
        final Path p = it.next();
        if( FILE_MATCHER.matches( p ) ) {
          f.accept( p );
        }
      }
    }
  }
}
Дэйв Джарвис
источник