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.

OscarRyz
fonte
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".

Erickson
fonte
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());
        }
    }
}
acheron55
fonte
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]));
OscarRyz
fonte
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.

Eyal Roth
fonte
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.

Kevin Day
fonte
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;
                }
            });
        }
    }
}
Pino
fonte
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);
    }
}
Ran Biron
fonte
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.

Vadzim
fonte
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
Mikeb
fonte
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.

Jon Skeet
fonte
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;
}
Berni
fonte
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;
}
Felix G.
fonte
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.

Luke Hutchison
fonte
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 
    }
});
trung
fonte
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 );
        }
      }
    }
  }
}
Dave Jarvis
fonte