如何列出JAR文件中的文件?


114

我有这段代码,它从目录中读取所有文件。

    File textFolder = new File("text_directory");

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

效果很好。它使用目录“ text_directory”中所有以“ .txt”结尾的文件填充数组。

如何 JAR文件中以类似方式读取目录的内容?

所以我真正想做的是列出我的JAR文件中的所有图像,这样我就可以加载它们:

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

(之所以可行,是因为“ CompanyLogo”是“硬编码的”,但是JAR文件中的图像数量可能是10到200个可变长度。)

编辑

所以我想我的主要问题是:如何知道我的主类所在的JAR文件名称

当然,我可以使用阅读java.util.Zip

我的结构是这样的:

他们就像:

my.jar!/Main.class
my.jar!/Aux.class
my.jar!/Other.class
my.jar!/images/image01.png
my.jar!/images/image02a.png
my.jar!/images/imwge034.png
my.jar!/images/imagAe01q.png
my.jar!/META-INF/manifest 

现在,我可以使用以下示例加载“ images / image01.png”:

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

但是仅由于我知道文件名,其余的我必须动态加载它们。


只是想一想-为什么不将zip / jar图像压缩到一个单独的文件中,然后从您的类中的另一个jar中读取其中的条目?
Vineet Reynolds,

3
因为这将需要一个“额外”步骤来进行分发/安装。:(您知道,最终用户
。– OscarRyz

既然您已经创建了jar,那么不妨尝试其中的所有文件,而不要尝试任何技巧。
汤姆·哈特芬

好吧,我可能会弄错,但是罐子可以嵌入其他罐子中。one-jar(TM)打包解决方案ibm.com/developerworks/java/library/j-onejar在此基础上工作。除了这种情况,您不需要能力加载类。
Vineet Reynolds

Answers:


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

请注意,在Java 7中,您可以FileSystem从JAR(zip)文件创建一个,然后使用NIO的目录遍历和过滤机制在其中进行搜索。这将使编写处理JAR和“爆炸”目录的代码更加容易。


嘿,谢谢。。。几个小时以来一直在寻找一种方法!
Newtopian

9
是的,如果我们要列出此jar文件中的所有条目,则此代码有效。但是,如果我只想在jar中列出一个子目录,例如example.jar / dir1 / dir2 /,我如何直接列出该子目录中的所有文件?还是我需要解压缩该jar文件?非常感谢您的帮助!
Ensom Hodder

提到的Java 7方法在@ acheron55的答案中列出。
Vadzim 2015年

@Vadzim您确定acheron55的答案是针对Java 7的吗?我没有在Java 7中找到Files.walk()或java.util.Stream,但在Java 8中:docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
Bruce 2016年

@BruceSun,在Java 7中,您可以改用Files.walkFileTree(...)
Vadzim '16

79

适用于IDE和.jar文件的代码:

import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class ResourceWalker {
    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        Path myPath;
        if (uri.getScheme().equals("jar")) {
            FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap());
            myPath = fileSystem.getPath("/resources");
        } else {
            myPath = Paths.get(uri);
        }
        Stream<Path> walk = Files.walk(myPath, 1);
        for (Iterator<Path> it = walk.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}

5
FileSystems.newFileSystem()需要一个Map<String, ?>,因此您需要指定Collections.emptyMap()它需要返回一个适当类型的值。这工作:Collections.<String, Object>emptyMap()
Zero3

6
太棒了!!! 但是URI uri = MyClass.class.getResource(“ / resources”)。toURI(); 应该具有MyClass.class.getClassLoader()。getResource(“ / resources”)。toURI(); 即,getClassLoader()。否则,它对我不起作用。
EMM 2015年

8
不要忘记关闭fileSystem
gmjonker '16

3
这应该是1.8的第一个答案(walk方法Files仅在1.8中可用)。唯一的问题是资源目录显示在中Files.walk(myPath, 1),而不仅仅是文件。我猜第一个元素可以简单地忽略
toto_tico 16-4-19

4
myPath = fileSystem.getPath("/resources");对我不起作用;它什么也没找到。就我而言,它应该是“ images”,而“ images”目录肯定包含在我的jar中!
phip1611 '18

21

埃里克森的答案 非常有效:

这是工作代码。

CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();
List<String> list = new ArrayList<String>();

if( src != null ) {
    URL jar = src.getLocation();
    ZipInputStream zip = new ZipInputStream( jar.openStream());
    ZipEntry ze = null;

    while( ( ze = zip.getNextEntry() ) != null ) {
        String entryName = ze.getName();
        if( entryName.startsWith("images") &&  entryName.endsWith(".png") ) {
            list.add( entryName  );
        }
    }

 }
 webimages = list.toArray( new String[ list.size() ] );

我刚刚从中修改了我的加载方法:

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

对此:

String  [] webimages = ...

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

9

我想扩展acheron55的答案,因为它是一个非常不安全的解决方案,其原因有以下几种:

  1. 它不会关闭FileSystem对象。
  2. 它不会检查FileSystem对象是否已经存在。
  3. 它不是线程安全的。

这是一个较为安全的解决方案:

private static ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();

public void walk(String path) throws Exception {

    URI uri = getClass().getResource(path).toURI();
    if ("jar".equals(uri.getScheme()) {
        safeWalkJar(path, uri);
    } else {
        Files.walk(Paths.get(path));
    }
}

private void safeWalkJar(String path, URI uri) throws Exception {

    synchronized (getLock(uri)) {    
        // this'll close the FileSystem object at the end
        try (FileSystem fs = getFileSystem(uri)) {
            Files.walk(fs.getPath(path));
        }
    }
}

private Object getLock(URI uri) {

    String fileName = parseFileName(uri);  
    locks.computeIfAbsent(fileName, s -> new Object());
    return locks.get(fileName);
}

private String parseFileName(URI uri) {

    String schemeSpecificPart = uri.getSchemeSpecificPart();
    return schemeSpecificPart.substring(0, schemeSpecificPart.indexOf("!"));
}

private FileSystem getFileSystem(URI uri) throws IOException {

    try {
        return FileSystems.getFileSystem(uri);
    } catch (FileSystemNotFoundException e) {
        return FileSystems.newFileSystem(uri, Collections.<String, String>emptyMap());
    }
}   

真正不需要在文件名上进行同步。每次都可以在同一个对象上简单地同步(或进行方法synchronized),这纯粹是一种优化。

我会说这仍然是一个有问题的解决方案,因为代码中可能还有其他部分在FileSystem同一文件上使用该接口,并且可能会干扰它们(即使在单线程应用程序中)。
此外,它不会检查nulls(例如,在上)getClass().getResource()

这个特殊的Java NIO接口很可怕,因为它引入了全局/单个非线程安全资源,并且其文档非常模糊(由于提供程序特定的实现,很多未知数)。对于其他FileSystem提供程序(不是JAR),结果可能有所不同。这样做是有充分的理由的。我不知道,我还没有研究实现。


1
像FS这样的外部资源的同步在一个VM中没有多大意义。可能有其他应用程序在VM外部访问它。除了在您自己的应用程序内部之外,还可以轻松绕过基于文件名的锁定。有了这些,最好依靠操作系统同步机制,例如文件锁定。
Espinosa

@Espinosa可以完全绕过文件名锁定机制。我的回答还不够安全,但是我相信这是Java NIO所能付出的最大努力。恕我直言,除非您要构建基于客户的应用程序(例如,文本编辑器),否则依靠操作系统来管理锁,或者无法控制哪些应用程序访问哪些文件是恕我直言的做法。不自行管理锁将导致引发异常或导致线程阻塞应用程序-应避免两者。
艾尔·罗斯

8

所以我想我的主要问题是,如何知道我的主要学生居住的罐子的名字。

假设您的项目打包在一个Jar中(不一定是true!),则可以将ClassLoader.getResource()或findResource()与类名一起使用(后跟.class),以获取包含给定类的jar。您必须从返回的URL解析jar名称(不是那么难),我将留给读者练习:-)

确保测试类不是jar的一部分的情况。


1
呵呵-有趣的是,如果不加注释,它会被关闭...我们一直在使用上述技术,并且效果很好。
凯文·戴斯2009年

一个老问题,但是对我来说,这似乎是个不错的选择。提升为零:)
Tuukka Mustonen 2010年

推荐使用,因为这是这里列出的唯一解决方案,适用于类没有的情况CodeSource
恢复莫妮卡2331977

7

我已经将acheron55的答案移植到Java 7并关闭了该FileSystem对象。这段代码可在IDE,jar文件和Tomcat 7战争中的jar中工作;但是请注意,在JBoss 7的战争中,它不能在jar中使用(它给出了FileSystemNotFoundException: Provider "vfs" not installed,另请参见本帖子)。此外,就像原始代码一样,它也不是线程安全的,如errr所建议。由于这些原因,我放弃了该解决方案。但是,如果您可以接受这些问题,这是我现成的代码:

import java.io.IOException;
import java.net.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;

public class ResourceWalker {

    public static void main(String[] args) throws URISyntaxException, IOException {
        URI uri = ResourceWalker.class.getResource("/resources").toURI();
        System.out.println("Starting from: " + uri);
        try (FileSystem fileSystem = (uri.getScheme().equals("jar") ? FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap()) : null)) {
            Path myPath = Paths.get(uri);
            Files.walkFileTree(myPath, new SimpleFileVisitor<Path>() { 
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println(file);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }
}

5

这是我为“在一个程序包下运行所有​​JUnit”编写的一种方法。您应该能够使其适应您的需求。

private static void findClassesInJar(List<String> classFiles, String path) throws IOException {
    final String[] parts = path.split("\\Q.jar\\\\E");
    if (parts.length == 2) {
        String jarFilename = parts[0] + ".jar";
        String relativePath = parts[1].replace(File.separatorChar, '/');
        JarFile jarFile = new JarFile(jarFilename);
        final Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            final String entryName = entry.getName();
            if (entryName.startsWith(relativePath)) {
                classFiles.add(entryName.replace('/', File.separatorChar));
            }
        }
    }
}

编辑:啊,在这种情况下,您可能也需要此代码段(相同的用例:))

private static File findClassesDir(Class<?> clazz) {
    try {
        String path = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
        final String codeSourcePath = URLDecoder.decode(path, "UTF-8");
        final String thisClassPath = new File(codeSourcePath, clazz.getPackage().getName().repalce('.', File.separatorChar));
    } catch (UnsupportedEncodingException e) {
        throw new AssertionError("impossible", e);
    }
}

1
我猜最大的问题是首先要知道jar文件的名称。它是主要阶级居住的罐子。
OscarRyz

5

这是一个使用Reflections库通过正则表达式名称模式以几个Guava特权增强的递归方式扫描类路径的示例,以获取资源内容:

Reflections reflections = new Reflections("com.example.package", new ResourcesScanner());
Set<String> paths = reflections.getResources(Pattern.compile(".*\\.template$"));

Map<String, String> templates = new LinkedHashMap<>();
for (String path : paths) {
    log.info("Found " + path);
    String templateName = Files.getNameWithoutExtension(path);
    URL resource = getClass().getClassLoader().getResource(path);
    String text = Resources.toString(resource, StandardCharsets.UTF_8);
    templates.put(templateName, text);
}

这适用于jar和爆炸类。


请注意,反射仍然不支持Java 9及更高版本:github.com/ronmamo/reflections/issues/186。有链接到那里的竞争图书馆。
Vadzim

3

jar文件只是具有结构化清单的zip文件。您可以使用常规的Java zip工具打开jar文件,然后以这种方式扫描文件内容,对流进行充气等。然后在getResourceAsStream调用中使用该文件,它应该是很笨拙的。

编辑/澄清后

我花了一分钟的时间记住所有的点点滴滴,我敢肯定有更干净的方法可以做到这一点,但我想知道自己并不疯狂。在我的项目中,image.jpg是主jar文件中某些部分的文件。我得到了主类的类加载器(SomeClass是入口点),并使用它来发现image.jpg资源。然后使用一些流魔术将其放入此ImageInputStream中,一切都很好。

InputStream inputStream = SomeClass.class.getClassLoader().getResourceAsStream("image.jpg");
JPEGImageReaderSpi imageReaderSpi = new JPEGImageReaderSpi();
ImageReader ir = imageReaderSpi.createReaderInstance();
ImageInputStream iis = new MemoryCacheImageInputStream(inputStream);
ir.setInput(iis);
....
ir.read(0); //will hand us a buffered image

这个jar包含主程序和资源。我如何指代自我罐子?从jar文件中?
OscarRyz

要引用JAR文件,只需使用“ blah.JAR”作为字符串。例如,您可以new File("blah.JAR")用来创建代表JAR的File对象。只需用您的JAR名称替换“ blah.JAR”。
Thomas Owens

如果它已经用完了相同的jar,则类加载器应该能够看到jar中的东西……我误解了您最初尝试执行的操作。
Mikeb

2
好吧,是的,我已经知道了,问题是当我需要类似的东西:“ ... getResourceAsStream(” *。jpg“); ...”也就是说,动态地列出包含的文件。
OscarRyz

3

给定一个实际的JAR文件,您可以使用列出内容JarFile.entries()。但是,您将需要知道JAR文件的位置-您不能只要求类加载器列出它可以得到的所有内容。

您应该能够根据从返回的URL来计算JAR文件的位置ThisClassName.class.getResource("ThisClassName.class"),但这可能有点麻烦。


阅读您的答案提出了另一个问题。什么会产生呼叫:this.getClass()。getResource(“ / my_directory”); 它应该返回一个URL,该URL可以反过来用作目录。不,让我尝试一下。
OscarRyz

您始终知道JAR的位置-位于“。”中。只要知道JAR的名称是什么,就可以在某个地方使用String常量。现在,如果人们去更改JAR的名称...
Thomas Owens

@Thomas:假设您正在从当前目录运行该应用程序。“ java -jar foo / bar / baz.jar”有什么问题?
乔恩·斯基特

我相信(并且必须进行验证),如果Jar中有代码new File("baz.jar),则File对象将代表您的JAR文件。
Thomas Owens

@托马斯:我不这么认为。我相信这将是相对于该进程的当前工作目录。我也必须检查一下:)
乔恩·斯凯特

3

前段时间,我做了一个从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
尽管此代码段可以解决问题,但并未说明原因或答案。请提供代码说明,因为这确实有助于提高您的帖子质量。请记住,您将来会为读者回答这个问题,而这些人可能不知道您提出代码建议的原因。
塞缪尔·菲利普

当我们从jar文件执行代码时,数据为空。
Aguid


1

当前,列出类路径中所有资源的最可靠的机制是将这种模式与ClassGraph一起使用,因为它可以处理最广泛的类路径规范机制,包括新的JPMS模块系统。(我是ClassGraph的作者。)

如何知道我的主要班级居住的JAR文件的名称?

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

如何在JAR文件中以类似方式读取目录的内容?

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

还有许多其他方法来处理资源


1
非常好的软件包,可以在我的Scala项目中轻松使用,谢谢。
zslim

0

只是从jar URL列出/读取文件的一种不同方法,它对嵌套jar进行递归处理

https://gist.github.com/trung/2cd90faab7f75b3bcbaa

URL urlResource = Thead.currentThread().getContextClassLoader().getResource("foo");
JarReader.read(urlResource, new InputStreamCallback() {
    @Override
    public void onFile(String name, InputStream is) throws IOException {
        // got file name and content stream 
    }
});

0

一路走来:

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

这对于匹配特定的文件名更加灵活,因为它使用通配符。


更实用的样式:

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 );
        }
      }
    }
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.