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

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

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

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

Ответы

Ответ 1

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 и "взорванные" каталоги.

Ответ 2

Код, который работает для файлов 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());
        }
    }
}

Ответ 3

erickson ответ работал отлично:

Здесь рабочий код.

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

Ответ 4

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

  • Он не закрывает объект FileSystem.
  • Он не проверяет, существует ли объект FileSystem.
  • Он не является потокобезопасным.

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

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 по тем же файлам, и могут помешать им (даже в однопоточном приложении).
Кроме того, он не проверяет null (например, на getClass().getResource().

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

Ответ 5

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

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

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

Ответ 6

Вот метод, который я написал для "запускать все 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);
    }
}

Ответ 7

Ниже приведен пример использования библиотеки 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);
}

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

Ответ 8

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

ИЗМЕНИТЬ/после уточнения

Мне потребовалась минута, чтобы запомнить все кусочки и я уверен, что есть более чистые способы сделать это, но я хотел видеть, что я не сумасшедший. В моем проекте 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

Ответ 9

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

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

Ответ 10

Некоторое время назад я создал функцию, которая становится классной изнутри 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;
}

Ответ 11

Я портировал acheron55 ответ на Java 7 и закрыл объект FileSystem. Этот код работает в IDE, в файлах 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;
                }
            });
        }
    }
}

Ответ 13

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

Ответ 14

Просто другой способ листинга/чтения файлов из URL-адреса 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 
    }
});

Ответ 15

Самый надежный механизм для перечисления всех ресурсов в classpath в настоящее время использует этот шаблон с ClassGraph, потому что он обрабатывает максимально широкий массив механизмов спецификации classpath, включая новую систему модулей 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();
}

Существует также множество других способов работы с ресурсами.