Como listar os arquivos dentro de um arquivo JAR?


114

Eu tenho este código que lê todos os arquivos de um diretório.

    File textFolder = new File("text_directory");

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

Funciona muito bem. Ele preenche o array com todos os arquivos que terminam com ".txt" do diretório "text_directory".

Como posso ler o conteúdo de um diretório de maneira semelhante em um arquivo JAR?

Então, o que eu realmente quero fazer é listar todas as imagens dentro do meu arquivo JAR, para que possa carregá-las com:

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

(Esse funciona porque o "CompanyLogo" é "codificado", mas o número de imagens dentro do arquivo JAR pode ser de 10 a 200 de comprimento variável.)

EDITAR

Portanto, acho que meu principal problema seria: Como saber o nome do arquivo JAR onde mora minha classe principal?

Admito que eu poderia ler usando java.util.Zip.

Minha estrutura é assim:

Eles são como:

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 

No momento, consigo carregar, por exemplo, "images / image01.png" usando:

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

Mas só porque sei o nome do arquivo, quanto ao resto, tenho que carregá-los dinamicamente.


Apenas um pensamento - por que não compactar as imagens / jar em um arquivo separado e ler as entradas de sua classe em outro jar?
— Vineet Reynolds,

3
Porque seria necessário um passo "extra" para distribuição / instalação. :( Você sabe, usuários finais.
— OscarRyz

Considerando que você criou o jar, você também pode incluir a lista de arquivos dentro dele, em vez de tentar quaisquer truques.
— Tom Hawtin - tackline

Bem, posso estar enganado, mas os frascos podem ser incorporados dentro de outros frascos. A solução de empacotamento de um jar (TM) ibm.com/developerworks/java/library/j-onejar funciona com base nisso. Exceto, no seu caso, você não precisa das classes de carga de habilidade.
— Vineet Reynolds,

Respostas:


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... */
}

Observe que no Java 7, você pode criar um a FileSystempartir do arquivo JAR (zip) e, em seguida, usar os mecanismos de filtragem e navegação de diretório do NIO para pesquisá-lo. Isso tornaria mais fácil escrever código que lida com JARs e diretórios "explodidos".


hey obrigado ... estou procurando uma maneira de fazer isso por algumas horas agora !!
— Newtopian

9
Sim, este código funciona se quisermos listar todas as entradas dentro deste arquivo jar. Mas se eu quiser apenas listar um subdiretório dentro do jar, por exemplo, example.jar / dir1 / dir2 / , como posso listar diretamente todos os arquivos dentro desse subdiretório? Ou preciso descompactar este arquivo jar? Eu agradeço muito sua ajuda!
— Ensom Hodder

A abordagem do Java 7 mencionada está listada na resposta de @ acheron55 .
— Vadzim

@Vadzim Tem certeza de que a resposta de acheron55 é para Java 7? Não encontrei Files.walk () ou java.util.Stream no Java 7, mas no Java 8: docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
— Bruce Dom

@BruceSun, em java 7 você pode usar Files.walkFileTree (...) ao invés.
— Vadzim

80

Código que funciona para arquivos IDE e .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());
        }
    }
}

5
FileSystems.newFileSystem()recebe um Map<String, ?>, então você precisa especificar para Collections.emptyMap()que ele precisa retornar um com o tipo adequado. Isso funciona: Collections.<String, Object>emptyMap().
— Zero3

6
Fantástico!!! mas URI uri = MyClass.class.getResource ("/ resources"). toURI (); deve ter MyClass.class.getClassLoader (). getResource ("/ resources"). toURI (); ou seja, getClassLoader (). Caso contrário, não estava funcionando para mim.
— EMM de

8
Não se esqueça de fechar fileSystem!
— gmjonker

3
Esta deve ser a primeira resposta para 1.8 (o walkmétodo em Filessó está disponível em 1.8). O único problema é que o diretório de recursos aparece no Files.walk(myPath, 1), não apenas os arquivos. Eu acho que o primeiro elemento pode ser simplesmente ignorado
— toto_tico

4
myPath = fileSystem.getPath("/resources");não funciona para mim; não encontra nada. Deveria ser "imagens" no meu caso e o diretório "imagens" definitivamente está incluído no meu jar!
— phip1611

21

resposta de Erickson funcionou perfeitamente:

Aqui está o código de trabalho.

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

E acabei de modificar meu método de carregamento a partir deste:

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

Para isso:

String  [] webimages = ...

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

9

Gostaria de expandir a resposta de acheron55 , uma vez que é uma solução muito insegura , por vários motivos:

  1. Não fecha o FileSystemobjeto.
  2. Não verifica se o FileSystemobjeto já existe.
  3. Não é thread-safe.

Esta é uma solução um tanto mais segura:

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

Não há necessidade real de sincronizar sobre o nome do arquivo; pode-se simplesmente sincronizar no mesmo objeto todas as vezes (ou fazer o método synchronized), é puramente uma otimização.

Eu diria que esta ainda é uma solução problemática, uma vez que pode haver outras partes do código que usam a FileSysteminterface sobre os mesmos arquivos, e isso pode interferir neles (mesmo em um único aplicativo threaded).
Além disso, ele não verifica se há nulls (por exemplo, emgetClass().getResource() .

Essa interface Java NIO em particular é horrível, pois apresenta um recurso global / singleton não seguro para thread e sua documentação é extremamente vaga (muitas incógnitas devido a implementações específicas do provedor). Os resultados podem variar para outros FileSystemprovedores (não JAR). Talvez haja uma boa razão para ser assim; Não sei, não pesquisei as implementações.


1
A sincronização de recursos externos, como FS, não faz muito sentido em uma VM. Pode haver outros aplicativos acessando-o fora de sua VM. Além do mais, mesmo dentro do seu próprio aplicativo, seu bloqueio baseado em nomes de arquivos pode ser facilmente contornado. Com isso, é melhor confiar nos mecanismos de sincronização do sistema operacional, como bloqueio de arquivos.
— Espinosa,

@Espinosa O mecanismo de bloqueio do nome do arquivo pode ser totalmente ignorado; minha resposta também não é segura o suficiente, mas acredito que seja o máximo que você pode obter com o Java NIO com o mínimo de esforço. Depender do sistema operacional para gerenciar os bloqueios ou não ter controle de quais aplicativos acessam quais arquivos é uma má prática, IMHO, a menos que você esteja construindo um aplicativo baseado em cliente - digamos, um editor de texto. Não gerenciar bloqueios por conta própria causará o lançamento de exceções ou fará com que os threads bloqueiem o aplicativo - ambos devem ser evitados.
— Eyal Roth

8

Então acho que meu principal problema seria saber o nome do jarro onde mora minha classe principal.

Supondo que seu projeto esteja empacotado em um Jar (não necessariamente verdadeiro!), Você pode usar ClassLoader.getResource () ou findResource () com o nome da classe (seguido por .class) para obter o jar que contém uma determinada classe. Você terá que analisar o nome do jar do URL que é retornado (não é tão difícil), que deixarei como um exercício para o leitor :-)

Certifique-se de testar o caso em que a classe não faz parte de um jar.


1
huh - interessante que isso teria sido modificado sem comentários ... Usamos a técnica acima o tempo todo e funciona muito bem.
— Kevin Day

Um problema antigo, mas para mim parece um bom hack. Melhor votação para zero :)
— Tuukka Mustonen

Votos positivos, porque esta é a única solução listada aqui para o caso em que uma classe não tem um CodeSource.
— Reintegrar Monica 2331977

7

Eu transferi a resposta de acheron55 para Java 7 e fechei o FileSystemobjeto. Este código funciona em IDE, em arquivos jar e em um jar dentro de uma guerra no Tomcat 7; mas note que não funciona em um jar dentro de uma guerra no JBoss 7 (dá FileSystemNotFoundException: Provider "vfs" not installed, veja também este post ). Além disso, como o código original, não é seguro para threads, como sugerido por errr . Por essas razões, abandonei essa solução; no entanto, se você pode aceitar esses problemas, aqui está meu código pronto:

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

5

Aqui está um método que escrevi para "executar todos os JUnits em um pacote". Você deve ser capaz de adaptá-lo às suas necessidades.

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

Edit: Ah, nesse caso, você pode querer este snippet também (mesmo caso de uso :))

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
Acho que o grande problema é saber o nome do arquivo jar em primeiro lugar. É o jarro onde vive a classe principal.
— OscarRyz de

5

Aqui está um exemplo de uso da biblioteca Reflections para escanear recursivamente classpath por padrão de nome regex aumentado com algumas vantagens Guava para buscar conteúdos de recursos:

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

Isso funciona com potes e classes explodidas.


Esteja ciente de que reflexões ainda não suportam Java 9 e superior: github.com/ronmamo/reflections/issues/186 . Existem links para bibliotecas concorrentes lá.
— Vadzim

3

Um arquivo jar é apenas um arquivo zip com um manifesto estruturado. Você pode abrir o arquivo jar com as ferramentas java zip usuais e escanear o conteúdo do arquivo dessa forma, inflar streams, etc. Em seguida, use isso em uma chamada getResourceAsStream, e deve ser tudo ótimo.

EDITAR / após esclarecimento

Levei um minuto para lembrar todos os pedaços e tenho certeza de que existem maneiras mais limpas de fazer isso, mas eu queria ver se não estava louco. No meu projeto, image.jpg é um arquivo em alguma parte do arquivo jar principal. Eu pego o carregador de classe da classe principal (SomeClass é o ponto de entrada) e o uso para descobrir o recurso image.jpg. Então, um pouco de magia de fluxo para colocá-lo nessa coisa ImageInputStream e está tudo bem.

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

Este jar contém o programa principal e os recursos. Como me refiro ao jarro próprio? de dentro do arquivo jar?
— OscarRyz de

Para se referir ao arquivo JAR, basta usar "blah.JAR" como String. Você pode usar new File("blah.JAR")para criar um objeto File que representa o JAR, por exemplo. Basta substituir "blah.JAR" pelo nome do seu JAR.
— Thomas Owens,

Se for o mesmo jar que você já está esgotando, o carregador de classes deve ser capaz de ver as coisas dentro do jar ... Não entendi o que você estava tentando fazer inicialmente.
— Mikeb de

2
Bem, sim, já tenho isso, o problema é quando preciso de algo como: "... getResourceAsStream (" *. Jpg "); ..." Ou seja, listar dinamicamente os arquivos contidos.
— OscarRyz de

3

Dado um arquivo JAR real, você pode listar o conteúdo usando JarFile.entries(). No entanto, você precisará saber a localização do arquivo JAR - você não pode simplesmente pedir ao carregador de classe para listar tudo o que ele pode encontrar.

Você deve conseguir descobrir a localização do arquivo JAR com base na URL de onde retornou ThisClassName.class.getResource("ThisClassName.class"), mas pode ser um pouco complicado.


Lendo sua resposta, outra questão levantada. O que geraria a chamada: this.getClass (). GetResource ("/ my_directory"); Ele deve retornar uma URL que, por sua vez, pode ser .... usada como diretório? Nahh ... deixe-me tentar.
— OscarRyz de

Você sempre sabe a localização do JAR - está em ".". Desde que o nome do JAR seja algo conhecido, você pode usar uma constante String em algum lugar. Agora, se as pessoas mudarem o nome do JAR ...
— Thomas Owens

@Thomas: Supondo que você esteja executando o aplicativo no diretório atual. O que há de errado com "java -jar foo / bar / baz.jar"?
— Jon Skeet de

Eu acredito (e teria que verificar), que se você tivesse código em seu Jar que fosse new File("baz.jar), o objeto File representaria seu arquivo JAR.
— Thomas Owens,

@Thomas: Eu não acredito. Eu acredito que seria relativo ao diretório de trabalho atual do processo. Eu teria que verificar também :)
— Jon Skeet

3

Há algum tempo, criei uma função que obtém classes de dentro do 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;
}

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
Embora este trecho de código possa resolver o problema, ele não explica por que ou como responde à pergunta. Por favor incluir uma explicação para o seu código , como o que realmente ajuda a melhorar a qualidade do seu post. Lembre-se de que você está respondendo à pergunta para leitores no futuro e essas pessoas podem não saber os motivos de sua sugestão de código.
— Samuel Philipp

os dados estão vazios quando executamos o código de um arquivo jar.
— Aguid


1

O mecanismo mais robusto para listar todos os recursos no caminho de classe atualmente é usar esse padrão com ClassGraph , porque ele lida com a maior variedade possível de mecanismos de especificação de caminho de classe , incluindo o novo sistema de módulo JPMS. (Eu sou o autor de ClassGraph.)

Como saber o nome do arquivo JAR onde mora minha classe principal?

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

Como posso ler o conteúdo de um diretório de maneira semelhante em um arquivo JAR?

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

Existem muitas outras maneiras de lidar com recursos também.


1
Pacote muito bom, facilmente utilizável no meu projeto Scala, obrigado.
— zslim

0

Apenas uma maneira diferente de listar / ler arquivos de uma URL jar e faz isso recursivamente para jars aninhados

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

Mais um para a estrada:

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

Isso é um pouco mais flexível para combinar nomes de arquivos específicos porque usa globbing curinga.


Um estilo mais funcional:

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 );
        }
      }
    }
  }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.