¿Cómo enumerar los archivos dentro de un archivo JAR?

114

Tengo este código que lee todos los archivos de un directorio.

    File textFolder = new File("text_directory");

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

Funciona muy bien. Llena la matriz con todos los archivos que terminan con ".txt" del directorio "text_directory".

¿Cómo puedo leer el contenido de un directorio de manera similar dentro de un archivo JAR?

Entonces, lo que realmente quiero hacer es enumerar todas las imágenes dentro de mi archivo JAR, para poder cargarlas con:

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

(Ese funciona porque el "CompanyLogo" está "codificado", pero la cantidad de imágenes dentro del archivo JAR podría ser de 10 a 200 de longitud variable).

EDITAR

Entonces supongo que mi principal problema sería: ¿Cómo saber el nombre del archivo JAR donde vive mi clase principal?

De acuerdo, podría leerlo usando java.util.Zip.

Mi estructura es así:

Ellos son 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 

Ahora mismo puedo cargar, por ejemplo, "images / image01.png" usando:

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

Pero solo porque sé el nombre del archivo, para el resto tengo que cargarlos dinámicamente.

OscarRyz
fuente
Solo un pensamiento: ¿por qué no comprimir imágenes / jar en un archivo separado y leer las entradas de su clase en otro jar?
Vineet Reynolds
3
Porque necesitaría un paso "extra" para la distribución / instalación. :( Ya saben, usuarios finales.
OscarRyz
Dado que ha creado el jar, también puede incluir la lista de archivos dentro de él en lugar de intentar trucos.
Tom Hawtin - tackline
Bueno, podría estar equivocado, pero los frascos se pueden incrustar dentro de otros frascos. La solución de empaquetado one-jar (TM) ibm.com/developerworks/java/library/j-onejar funciona sobre esta base. Excepto, en su caso, no requiere las clases de carga de habilidad.
Vineet Reynolds

Respuestas:

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

Tenga en cuenta que en Java 7, puede crear un FileSystemarchivo desde el archivo JAR (zip) y luego usar los mecanismos de filtrado y desplazamiento del directorio de NIO para buscarlo. Esto facilitaría la escritura de código que maneje archivos JAR y directorios "explotados".

erickson
fuente
hey gracias ... he estado buscando una manera de hacer esto por unas horas !!
Newtopian
9
Sí, este código funciona si queremos enumerar todas las entradas dentro de este archivo jar. Pero si solo quiero enumerar un subdirectorio dentro del jar, por ejemplo, example.jar / dir1 / dir2 / , ¿cómo puedo enumerar directamente todos los archivos dentro de este subdirectorio? ¿O necesito descomprimir este archivo jar? ¡Aprecio mucho tu ayuda!
Ensom Hodder
El enfoque de Java 7 mencionado se enumera en la respuesta de @ acheron55 .
Vadzim
@Vadzim ¿Estás seguro de que la respuesta de acheron55 es para Java 7? No encontré Files.walk () o java.util.Stream en Java 7, pero en Java 8: docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
Bruce Dom
@BruceSun, en java 7 puedes usar Files.walkFileTree (...) en su lugar.
Vadzim
80

Código que funciona para archivos IDE y .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
fuente
5
FileSystems.newFileSystem()toma un Map<String, ?>, por lo que debe especificar Collections.emptyMap()que debe devolver uno adecuadamente escrito. Esto funciona: Collections.<String, Object>emptyMap().
Zero3
6
¡¡¡Fantástico!!! pero URI uri = MyClass.class.getResource ("/ recursos"). toURI (); debe tener MyClass.class.getClassLoader (). getResource ("/ recursos"). toURI (); es decir, getClassLoader (). De lo contrario, no me estaba funcionando.
EMM
8
¡No olvide cerrar fileSystem!
gmjonker
3
Esta debería ser la primera respuesta para 1.8 (el walkmétodo Filessolo está disponible en 1.8). El único problema es que el directorio de recursos aparece en el Files.walk(myPath, 1), no solo en los archivos. Supongo que el primer elemento puede simplemente ignorarse
toto_tico
4
myPath = fileSystem.getPath("/resources");no funciona para mí; no encuentra nada. ¡Deberían ser "imágenes" en mi caso y el directorio de "imágenes" definitivamente está incluido en mi jar!
phip1611
21

La respuesta de Erickson funcionó perfectamente:

Aquí está el código de trabajo.

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

Y acabo de modificar mi método de carga a partir de esto:

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

A esto:

String  [] webimages = ...

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

Me gustaría ampliar la respuesta de acheron55 , ya que es una solución muy poco segura, por varias razones:

  1. No cierra el FileSystemobjeto.
  2. No comprueba si el FileSystemobjeto ya existe.
  3. No es seguro para subprocesos.

Esta es una solución algo más 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());
    }
}   

No hay ninguna necesidad real de sincronizar sobre el nombre del archivo; uno podría simplemente sincronizar en el mismo objeto cada vez (o hacer el método synchronized), es puramente una optimización.

Diría que esta sigue siendo una solución problemática, ya que podría haber otras partes en el código que usen la FileSysteminterfaz sobre los mismos archivos y podría interferir con ellos (incluso en una aplicación de un solo subproceso).
Además, no busca nulls (por ejemplo, on getClass().getResource().

Esta interfaz Java NIO en particular es algo horrible, ya que introduce un recurso global / singleton no seguro para subprocesos, y su documentación es extremadamente vaga (muchas incógnitas debido a implementaciones específicas del proveedor). Los resultados pueden variar para otros FileSystemproveedores (no para JAR). Quizás haya una buena razón para que sea así; No lo sé, no he investigado las implementaciones.

Eyal Roth
fuente
1
La sincronización de recursos externos, como FS, no tiene mucho sentido dentro de una VM. Puede haber otras aplicaciones que accedan a él fuera de su VM. Además, incluso dentro de su propia aplicación, su bloqueo basado en nombres de archivo se puede omitir fácilmente. Con estas cosas, es mejor confiar en los mecanismos de sincronización del sistema operativo, como el bloqueo de archivos.
Espinosa
@Espinosa El mecanismo de bloqueo de nombre de archivo podría omitirse por completo; Mi respuesta tampoco es lo suficientemente segura, pero creo que es lo máximo que puede obtener con Java NIO con un mínimo esfuerzo. Depender del sistema operativo para administrar los bloqueos, o no tener el control de qué aplicaciones acceden a qué archivos es una mala práctica en mi humilde opinión, a menos que esté creando una aplicación basada en el cliente, por ejemplo, un editor de texto. No administrar los bloqueos por su cuenta provocará que se produzcan excepciones o que los subprocesos bloqueen la aplicación; ambos deben evitarse.
Eyal Roth
8

Así que supongo que mi principal problema sería cómo saber el nombre del frasco donde vive mi clase principal.

Suponiendo que su proyecto está empaquetado en un Jar (¡no necesariamente cierto!), Puede usar ClassLoader.getResource () o findResource () con el nombre de la clase (seguido de .class) para obtener el jar que contiene una clase determinada. Tendrá que analizar el nombre del jar de la URL que se devuelve (no es tan difícil), que dejaré como ejercicio para el lector :-)

Asegúrese de probar el caso en el que la clase no sea parte de un frasco.

Kevin Day
fuente
1
eh - es interesante que esto se hubiera modificado sin comentarios ... Usamos la técnica anterior todo el tiempo y funciona bien.
Kevin Day
Un problema antiguo, pero a mí me parece un buen truco. Votación a favor de nuevo a cero :)
Tuukka Mustonen
Voto a favor, porque esta es la única solución que se enumera aquí para el caso en que una clase no tiene un CodeSource.
Reincorporación a Monica 2331977
7

Me he portado respuesta de acheron55 a Java 7 y cerré el FileSystemobjeto. Este código funciona en IDE, en archivos jar y en un jar dentro de una guerra en Tomcat 7; pero tenga en cuenta que no funciona en un frasco dentro de una guerra en JBoss 7 (da FileSystemNotFoundException: Provider "vfs" not installed, vea también esta publicación ). Además, como el código original, no es seguro para subprocesos, como sugiere errr . Por estas razones he abandonado esta solución; sin embargo, si puede aceptar estos problemas, aquí está mi código listo para usar:

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
fuente
5

Aquí hay un método que escribí para "ejecutar todos los JUnits en un paquete". Debería poder adaptarlo a sus necesidades.

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

Editar: Ah, en ese caso, es posible que también desee este fragmento (mismo 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
fuente
1
Supongo que el gran problema es conocer el nombre del archivo jar en primer lugar. Es el frasco donde vive la Clase Principal.
OscarRyz
5

Aquí hay un ejemplo del uso de la biblioteca Reflections para escanear de forma recursiva classpath por patrón de nombre de expresión regular aumentado con un par de ventajas de Guava para recuperar el contenido de los 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);
}

Esto funciona tanto con jarras como con clases explosionadas.

Vadzim
fuente
Tenga en cuenta que las reflexiones aún no son compatibles con Java 9 y versiones posteriores: github.com/ronmamo/reflections/issues/186 . Allí hay enlaces a bibliotecas de la competencia.
Vadzim
3

Un archivo jar es solo un archivo zip con un manifiesto estructurado. Puede abrir el archivo jar con las herramientas habituales de java zip y escanear el contenido del archivo de esa manera, inflar secuencias, etc. Luego úselo en una llamada getResourceAsStream, y debería ser todo perfecto.

EDITAR / después de la aclaración

Me tomó un minuto recordar todos los detalles y estoy seguro de que hay formas más limpias de hacerlo, pero quería ver que no estaba loco. En mi proyecto, image.jpg es un archivo en alguna parte del archivo jar principal. Obtengo el cargador de clases de la clase principal (SomeClass es el punto de entrada) y lo uso para descubrir el recurso image.jpg. Luego, algo de magia de flujo para meterlo en este elemento ImageInputStream y todo está bien.

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
fuente
Este frasco contiene el programa principal y los recursos. ¿Cómo me refiero al tarro de uno mismo? desde dentro del archivo jar?
OscarRyz
Para hacer referencia al archivo JAR, simplemente use "blah.JAR" como String. Puede utilizarlo new File("blah.JAR")para crear un objeto Archivo que represente el JAR, por ejemplo. Simplemente reemplace "blah.JAR" con el nombre de su JAR.
Thomas Owens
Si es el mismo jar del que ya se está quedando, el cargador de clases debería poder ver cosas dentro del jar ... No entendí lo que estaba tratando de hacer inicialmente.
Mikeb
2
Bueno, sí, ya lo tengo, el problema es cuando necesito algo como: "... getResourceAsStream (" *. Jpg "); ..." Es decir, de forma dinámica, enumerar los archivos que contiene.
OscarRyz
3

Dado un archivo JAR real, puede enumerar el contenido usando JarFile.entries(). Sin embargo, necesitará saber la ubicación del archivo JAR; no puede simplemente pedirle al cargador de clases que enumere todo lo que puede encontrar.

Debería poder calcular la ubicación del archivo JAR en función de la URL devuelta ThisClassName.class.getResource("ThisClassName.class"), pero puede ser un poco complicado.

Jon Skeet
fuente
Al leer su respuesta surgió otra pregunta. Qué produciría la llamada: this.getClass (). GetResource ("/ my_directory"); Debería devolver una URL que, a su vez, podría ... ¿usarse como directorio? Nahh ... déjame intentarlo.
OscarRyz
Siempre sabes la ubicación del JAR, está en ".". Siempre que se sepa que el nombre del JAR es algo, puede usar una constante String en alguna parte. Ahora, si la gente cambia el nombre del JAR ...
Thomas Owens
@Thomas: Eso es asumiendo que está ejecutando la aplicación desde el directorio actual. ¿Qué pasa con "java -jar foo / bar / baz.jar"?
Jon Skeet
Creo (y tendría que verificar) que si tuviera código en su Jar new File("baz.jar), el objeto File representaría su archivo JAR.
Thomas Owens
@Thomas: No lo creo. Creo que sería relativo al directorio de trabajo actual del proceso. Sin embargo, también tendría que verificar :)
Jon Skeet
3

Hace algún tiempo hice una función que obtiene clases desde dentro de 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
fuente
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.
fuente
3
Si bien este fragmento de código puede resolver el problema, no explica por qué ni cómo responde a la pregunta. Por favor, incluya una explicación de su código , como que realmente ayuda a mejorar la calidad de su puesto. Recuerde que está respondiendo a la pregunta para los lectores en el futuro, y es posible que esas personas no conozcan los motivos de su sugerencia de código.
Samuel Philipp
los datos están vacíos cuando ejecutamos el código desde un archivo jar.
Aguid
1

El mecanismo más robusto para enumerar todos los recursos en la ruta de clases es actualmente usar este patrón con ClassGraph , porque maneja la más amplia gama posible de mecanismos de especificación de rutas de clases , incluido el nuevo sistema de módulos JPMS. (Soy el autor de ClassGraph.)

¿Cómo saber el nombre del archivo JAR donde vive mi clase principal?

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

¿Cómo puedo leer el contenido de un directorio de manera similar dentro de un archivo JAR?

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

También hay muchas otras formas de gestionar los recursos .

Luke Hutchison
fuente
1
Muy buen paquete, fácilmente utilizable en mi proyecto Scala, gracias.
zslim
0

Solo una forma diferente de enumerar / leer archivos de una URL de jar y lo hace de forma recursiva para jarras anidadas

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
fuente
0

Uno más para el camino:

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

Esto es un poco más flexible para hacer coincidir nombres de archivos específicos porque usa comodines globbing.


Un estilo más 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
fuente