Come elencare i file all'interno di un file JAR?


114

Ho questo codice che legge tutti i file da una directory.

    File textFolder = new File("text_directory");

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

Funziona benissimo. Riempie l'array con tutti i file che terminano con ".txt" dalla directory "text_directory".

Come posso leggere il contenuto di una directory in modo simile all'interno di un file JAR?

Quindi quello che voglio davvero fare è elencare tutte le immagini all'interno del mio file JAR, così posso caricarle con:

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

(Quello funziona perché "CompanyLogo" è "hardcoded" ma il numero di immagini all'interno del file JAR potrebbe essere compreso tra 10 e 200 di lunghezza variabile.)

MODIFICARE

Quindi immagino che il mio problema principale sarebbe: come conoscere il nome del file JAR in cui vive la mia classe principale?

Certo che potrei leggerlo usando java.util.Zip.

La mia struttura è così:

Sono come:

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 

In questo momento sono in grado di caricare ad esempio "images / image01.png" utilizzando:

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

Ma solo perchè conosco il nome del file, per il resto devo caricarli dinamicamente.


Solo un pensiero: perché non comprimere le immagini / jar in un file separato e leggere le voci in esso dalla tua classe in un altro jar?
Vineet Reynolds,

3
Perché avrebbe bisogno di un passaggio "extra" per la distribuzione / installazione. :( Sapete, utenti finali.
OscarRyz

Dato che hai creato il jar, potresti anche includere l'elenco dei file al suo interno piuttosto che tentare qualsiasi trucco.
Tom Hawtin - tackline

Beh, potrei sbagliarmi, ma i barattoli possono essere incorporati in altri barattoli. La soluzione di confezionamento one-jar (TM) ibm.com/developerworks/java/library/j-onejar funziona su questa base. Tranne che nel tuo caso non hai bisogno delle classi di carico abilità.
Vineet Reynolds,

Risposte:


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

Si noti che in Java 7 è possibile creare un FileSystemfile JAR (zip), quindi utilizzare i meccanismi di esplorazione e filtraggio delle directory di NIO per eseguire la ricerca all'interno. Ciò renderebbe più semplice scrivere codice che gestisce JAR e directory "esplose".


hey grazie ... ho cercato un modo per farlo da alcune ore !!
Newtopian

9
Sì, questo codice funziona se vogliamo elencare tutte le voci all'interno di questo file jar. Ma se voglio solo elencare una sottodirectory all'interno del jar, ad esempio, example.jar / dir1 / dir2 / , come posso elencare direttamente tutti i file all'interno di questa sottodirectory? O ho bisogno di decomprimere questo file jar? Apprezzo molto il tuo aiuto!
Ensom Hodder

L'approccio citato a Java 7 è elencato nella risposta di @ acheron55 .
Vadzim

@Vadzim Sei sicuro che la risposta di acheron55 sia per Java 7? Non ho trovato Files.walk () o java.util.Stream in Java 7, ma in Java 8: docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
Bruce Dom

@BruceSun, in java 7 puoi usare Files.walkFileTree (...) invece.
Vadzim

80

Codice che funziona sia per i file IDE che per i file .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()prende un Map<String, ?>, quindi è necessario specificare Collections.emptyMap()che deve restituirne uno opportunamente digitato. Questo funziona: Collections.<String, Object>emptyMap().
Zero3

6
Fantastico!!! ma URI uri = MyClass.class.getResource ("/ resources"). toURI (); dovrebbe avere MyClass.class.getClassLoader (). getResource ("/ resources"). toURI (); cioè getClassLoader (). Altrimenti non funzionava per me.
EMM

8
Non dimenticare di chiudere fileSystem!
gmjonker

3
Questa dovrebbe essere la prima risposta per 1.8 (il walkmetodo in Filesè disponibile solo in 1.8). L'unico problema è che la directory delle risorse viene visualizzata in Files.walk(myPath, 1), non solo i file. Immagino che il primo elemento possa essere semplicemente ignorato
toto_tico

4
myPath = fileSystem.getPath("/resources");non funziona per me; non trova niente. Dovrebbe essere "immagini" nel mio caso e la directory "immagini" è sicuramente inclusa nel mio jar!
phip1611

21

La risposta di Erickson ha funzionato perfettamente:

Ecco il codice funzionante.

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 ho appena modificato il mio metodo di caricamento da questo:

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

A questa:

String  [] webimages = ...

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

9

Vorrei espandere la risposta di acheron55 , poiché è una soluzione molto non sicura, per diversi motivi:

  1. Non chiude l' FileSystemoggetto.
  2. Non controlla se l' FileSystemoggetto esiste già.
  3. Non è thread-safe.

Questa è una soluzione un po 'più sicura:

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

Non c'è una reale necessità di sincronizzare il nome del file; si potrebbe semplicemente sincronizzare sullo stesso oggetto ogni volta (o creare il metodo synchronized), è puramente un'ottimizzazione.

Direi che questa è ancora una soluzione problematica, poiché potrebbero esserci altre parti nel codice che utilizzano l' FileSysteminterfaccia sugli stessi file e potrebbe interferire con esse (anche in una singola applicazione a thread).
Inoltre, non verifica la presenza di nulls (ad esempio, su getClass().getResource().

Questa particolare interfaccia Java NIO è piuttosto orribile, poiché introduce una risorsa globale / singleton non thread-safe e la sua documentazione è estremamente vaga (molte incognite a causa di implementazioni specifiche del provider). I risultati possono variare per altri FileSystemfornitori (non JAR). Forse c'è una buona ragione per cui è così; Non lo so, non ho studiato le implementazioni.


1
La sincronizzazione di risorse esterne, come FS, non ha molto senso all'interno di una VM. Possono esserci altre applicazioni che accedono al di fuori della VM. Inoltre, anche all'interno della propria applicazione, il blocco basato sui nomi dei file può essere facilmente aggirato. Con queste cose è meglio fare affidamento sui meccanismi di sincronizzazione del sistema operativo, come il blocco dei file.
Espinosa

@Espinosa Il meccanismo di blocco del nome del file potrebbe essere completamente aggirato; Anche la mia risposta non è abbastanza sicura, ma credo che sia il massimo che puoi ottenere con Java NIO con il minimo sforzo. Affidarsi al sistema operativo per gestire i blocchi o non avere il controllo di quali applicazioni accedono a quali file è una cattiva pratica IMHO, a meno che tu non stia costruendo un'app basata sul cliente, ad esempio un editor di testo. Non gestire i blocchi da soli causerà il lancio di eccezioni o il blocco dell'applicazione da parte dei thread: entrambi dovrebbero essere evitati.
Eyal Roth

8

Quindi immagino che il mio problema principale sarebbe come sapere il nome del barattolo in cui vive la mia classe principale.

Supponendo che il tuo progetto sia impacchettato in un Jar (non necessariamente vero!), Puoi usare ClassLoader.getResource () o findResource () con il nome della classe (seguito da .class) per ottenere il jar che contiene una data classe. Dovrai analizzare il nome del jar dall'URL che viene restituito (non così difficile), che lascerò come esercizio per il lettore :-)

Assicurati di testare il caso in cui la classe non fa parte di un vaso.


1
eh - interessante che questo sarebbe stato modificato senza commento ... Usiamo sempre la tecnica di cui sopra e funziona perfettamente.
Kevin Day

Un vecchio problema, ma a me sembra un buon trucco. Votato di nuovo a zero :)
Tuukka Mustonen

Votato, perché questa è l'unica soluzione qui elencata per il caso in cui una classe non ha un CodeSource.
Ripristina Monica 2331977

7

Ho portato la risposta di acheron55 a Java 7 e chiuso l' FileSystemoggetto. Questo codice funziona negli IDE, nei file jar e in un jar all'interno di una guerra su Tomcat 7; ma nota che non funziona in un jar all'interno di una guerra su JBoss 7 (dà FileSystemNotFoundException: Provider "vfs" not installed, vedi anche questo post ). Inoltre, come il codice originale, non è thread-safe, come suggerito da errr . Per questi motivi ho abbandonato questa soluzione; tuttavia, se puoi accettare questi problemi, ecco il mio codice già 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

Ecco un metodo che ho scritto per "eseguire tutte le JUnit in un pacchetto". Dovresti essere in grado di adattarlo alle tue esigenze.

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

Modifica: Ah, in tal caso, potresti volere anche questo snippet (stesso caso d'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
Immagino che il grosso problema sia conoscere il nome del file jar in primo luogo. È il barattolo in cui vive la classe principale.
OscarRyz

5

Di seguito è riportato un esempio di utilizzo della libreria Reflections per eseguire la scansione ricorsiva del percorso di classe in base al modello di nome regex aumentato con un paio di vantaggi Guava per recuperare i contenuti delle risorse:

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

Funziona sia con i vasi che con le classi esplose.


Fai attenzione che i reflections ancora non supportano Java 9 e versioni successive: github.com/ronmamo/reflections/issues/186 . Ci sono collegamenti a biblioteche concorrenti.
Vadzim

3

Un file jar è solo un file zip con un manifest strutturato. Puoi aprire il file jar con i soliti strumenti java zip e scansionare il contenuto del file in questo modo, gonfiare i flussi, ecc. Quindi usalo in una chiamata getResourceAsStream, e dovrebbe essere tutto fantastico.

EDIT / dopo chiarimenti

Mi ci è voluto un minuto per ricordare tutti i pezzi e sono sicuro che ci sono modi più puliti per farlo, ma volevo vedere che non ero pazzo. Nel mio progetto image.jpg è un file in una parte del file jar principale. Ottengo il caricatore di classi della classe principale (SomeClass è il punto di ingresso) e lo uso per scoprire la risorsa image.jpg. Quindi un po 'di magia del flusso per inserirlo in questa cosa di ImageInputStream e tutto va bene.

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

Questo vaso contiene il programma principale e le risorse. Come mi riferisco al self jar? dall'interno del file jar?
OscarRyz

Per fare riferimento al file JAR, usa semplicemente "blah.JAR" come stringa. È possibile utilizzare new File("blah.JAR")per creare un oggetto File che rappresenta il JAR, ad esempio. Basta sostituire "blah.JAR" con il nome del tuo JAR.
Thomas Owens,

Se è lo stesso jar di cui stai già esaurendo, il caricatore di classi dovrebbe essere in grado di vedere cose all'interno del jar ... Ho frainteso quello che stavi cercando di fare inizialmente.
Mikeb

2
Ebbene sì, ce l'ho già, il problema è quando ho bisogno di qualcosa come: "... getResourceAsStream (" *. Jpg "); ..." Cioè, dinamicamente, elenca i file contenuti.
OscarRyz

3

Dato un file JAR effettivo, è possibile elencare i contenuti utilizzando JarFile.entries(). Tuttavia, dovrai conoscere la posizione del file JAR: non puoi semplicemente chiedere al classloader di elencare tutto ciò a cui potrebbe arrivare.

Dovresti essere in grado di capire la posizione del file JAR in base all'URL restituito ThisClassName.class.getResource("ThisClassName.class"), ma potrebbe essere un po 'complicato.


Leggendo la tua risposta un'altra domanda sollevata. Cosa produrrebbe la chiamata: this.getClass (). GetResource ("/ my_directory"); Dovrebbe restituire un URL che potrebbe a sua volta essere .... usato come directory? Nahh ... fammi provare.
OscarRyz

Conosci sempre la posizione del JAR: è in ".". Finché il nome del JAR è noto per essere qualcosa, puoi usare una costante String da qualche parte. Ora, se le persone cambiano il nome del JAR ...
Thomas Owens,

@ Thomas: Presumendo che tu stia eseguendo l'app dalla directory corrente. Cosa c'è di sbagliato in "java -jar foo / bar / baz.jar"?
Jon Skeet,

Credo (e dovrei verificare), che se avessi del codice nel tuo Jar new File("baz.jar), l'oggetto File rappresenterebbe il tuo file JAR.
Thomas Owens,

@ Thomas: Non credo proprio. Credo che sarebbe relativo alla directory di lavoro corrente del processo. Dovrei controllare anche io però :)
Jon Skeet

3

Qualche tempo fa ho creato una funzione che ottiene classess dall'interno di 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
Sebbene questo frammento di codice possa risolvere il problema, non spiega perché o come risponde alla domanda. Si prega di includere una spiegazione per il codice , poiché ciò aiuta davvero a migliorare la qualità del tuo post. Ricorda che stai rispondendo alla domanda per i lettori in futuro e quelle persone potrebbero non conoscere i motivi del tuo suggerimento sul codice.
Samuel Philipp

i dati sono vuoti quando eseguiamo il codice da un file jar.
Aguid


1

Il meccanismo più robusto per elencare tutte le risorse nel classpath è attualmente quello di utilizzare questo modello con ClassGraph , perché gestisce la più ampia gamma possibile di meccanismi di specificazione del classpath , incluso il nuovo sistema di moduli JPMS. (Sono l'autore di ClassGraph.)

Come conoscere il nome del file JAR in cui vive la mia classe principale?

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

Come posso leggere il contenuto di una directory in modo simile all'interno di un file JAR?

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

Ci sono anche molti altri modi per gestire le risorse .


1
Pacchetto molto bello, facilmente utilizzabile nel mio progetto Scala, grazie.
zslim

0

Solo un modo diverso di elencare / leggere i file da un URL jar e lo fa in modo ricorsivo per i jar annidati

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

Uno in più per la strada:

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

Questo è un po 'più flessibile per la corrispondenza di nomi di file specifici perché utilizza il globbing dei caratteri jolly.


Uno stile più funzionale:

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 );
        }
      }
    }
  }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.