【问题标题】:How to list the files inside a JAR file?如何列出 JAR 文件中的文件?
【发布时间】:2010-11-28 14:06:25
【问题描述】:

我有这段代码可以从一个目录中读取所有文件。

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

但只是因为我知道文件名,其余的我必须动态加载它们。

【问题讨论】:

  • 只是一个想法 - 为什么不将图像压缩到一个单独的文件中,然后从另一个 jar 中的类中读取其中的条目?
  • 因为分发/安装需要“额外”步骤。 :( 你知道,最终用户。
  • 鉴于您已经创建了 jar,您不妨在其中包含文件列表,而不是尝试任何技巧。
  • 好吧,我可能弄错了,但是 jar 可以嵌入到其他 jar 中。 one-jar(TM) 包装解决方案ibm.com/developerworks/java/library/j-onejar 在此基础上运作。除非,在您的情况下,您不需要加载类的能力。

标签: java file jar java-io getresource


【解决方案1】:
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 中,您可以从 JAR (zip) 文件中创建一个FileSystem,然后使用 NIO 的目录遍历和过滤机制来搜索它。这样可以更轻松地编写处理 JAR 和“爆炸”目录的代码。

【讨论】:

  • 嘿,谢谢...几个小时以来一直在寻找一种方法!
  • 是的,如果我们想列出这个 jar 文件中的所有条目,这个代码就可以工作。但是如果我只想列出jar里面的一个子目录,比如example.jar/dir1/dir2/,那我怎么能直接列出这个子目录里面的所有文件呢?或者我需要解压这个 jar 文件?非常感谢您的帮助!
  • @acheron55's answer 中列出了提到的 Java 7 方法。
  • @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
  • @BruceSun,在 java 7 中你可以使用 Files.walkFileTree(...) 代替。
【解决方案2】:

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

【讨论】:

  • 太棒了!!!但是 URI uri = MyClass.class.getResource("/resources").toURI();应该有 MyClass.class.getClassLoader().getResource("/resources").toURI();即 getClassLoader()。否则它对我不起作用。
  • 别忘了关闭fileSystem
  • 这应该是 1.8 的第一个答案(Files 中的walk 方法仅在 1.8 中可用)。唯一的问题是资源目录出现在Files.walk(myPath, 1) 中,而不仅仅是文件。我想第一个元素可以简单地忽略
  • 这是个好主意,但也不安全。我添加了一个answer 以更安全的方法。
  • myPath = fileSystem.getPath("/resources"); 对我不起作用;它什么也没找到。在我的情况下它应该是“图像”,并且“图像”目录肯定包含在我的 jar 中!
【解决方案3】:

erickson 的 answer 完美运行:

这是工作代码。

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

【讨论】:

    【解决方案4】:

    我想扩展 acheron55 的 answer,因为它是一个非常不安全的解决方案,原因如下:

    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),结果可能会有所不同。也许有一个很好的理由。我不知道,我没有研究过实现。

    【讨论】:

    • 外部资源的同步,比如FS,在一台VM内没有太大意义。可以有其他应用程序在您的 VM 之外访问它。除了在您自己的应用程序中,您基于文件名的锁定也很容易被绕过。有了这些东西,最好依赖操作系统同步机制,比如文件锁定。
    • @Espinosa 文件名锁定机制完全可以被绕过;我的回答也不够安全,但我相信这是您可以通过 Java NIO 轻松获得的最多结果。恕我直言,依靠操作系统来管理锁,或者无法控制哪些应用程序访问哪些文件是一种不好的做法,除非您正在构建一个基于客户的应用程序——比如一个文本编辑器。不自己管理锁会导致抛出异常,或者导致线程阻塞应用程序——两者都应该避免。
    【解决方案5】:

    所以我想我的主要问题是,如何知道我的主类所在的 jar 的名称。

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

    一定要测试类不属于 jar 的情况。

    【讨论】:

    • 嗯 - 有趣的是,这会在没有评论的情况下被关闭......我们一直使用上述技术,它工作得很好。
    • 一个老问题,但对我来说这似乎是一个很好的技巧。赞成归零:)
    • 赞成,因为这是针对类没有CodeSource 的情况列出的唯一解决方案。
    【解决方案6】:

    我已将 acheron55's answer 移植到 Java 7 并关闭了 FileSystem 对象。此代码在 IDE、jar 文件和 Tomcat 7 上的战争中的 jar 中有效;但请注意,它确实在 JBoss 7 的战争中的 jar 中工作(它提供 FileSystemNotFoundException: Provider "vfs" not installed,另请参阅 this post)。此外,与原始代码一样,它不是线程安全的,正如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;
                    }
                });
            }
        }
    }
    

    【讨论】:

    • 这段代码对我不起作用,我得到NullPointerExceptionResourceWalker.class.getResource("/resources").toURI(),如果我使用“”作为参数,我得到以下错误:Starting from: rsrc:com/betalord/sgx/util/java.nio.file.FileSystemNotFoundException: Provider "rsrc" not installed
    【解决方案7】:

    下面是一个使用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 和分解的类。

    【讨论】:

    【解决方案8】:

    这是我为“在一个包下运行所有​​ 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));
                }
            }
        }
    }
    

    编辑: 啊,在那种情况下,你可能也想要这个 sn-p(相同的用例:))

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

    【讨论】:

    • 我想最大的问题是首先要知道 jar 文件名。它是 Main-Class: 所在的罐子。
    【解决方案9】:

    jar 文件只是一个带有结构化清单的 zip 文件。您可以使用通常的 java zip 工具打开 jar 文件并以这种方式扫描文件内容、膨胀流等。然后在 getResourceAsStream 调用中使用它,它应该是所有的 hunky dory。

    编辑/澄清后

    我花了一分钟来记住所有的点点滴滴,我确信有更简洁的方法可以做到这一点,但我想看看我没有疯。在我的项目中 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包含主程序和资源。我如何引用 self jar?从 jar 文件中?
    • 要引用 JAR 文件,只需使用“blah.JAR”作为字符串。例如,您可以使用new File("blah.JAR") 创建一个表示 JAR 的 File 对象。只需将“blah.JAR”替换为您的 JAR 名称即可。
    • 如果它与您已经用完的 jar 相同,则类加载器应该能够看到 jar 内的内容......我误解了您最初尝试做的事情。
    • 嗯,是的,我已经有了,问题是当我需要类似的东西时:"...getResourceAsStream("*.jpg"); ..." 也就是说,动态列出文件包含。
    【解决方案10】:

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

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

    【讨论】:

    • 阅读您的回答提出了另一个问题。什么会产生调用:this.getClass().getResource("/my_directory");它应该返回一个可以反过来被用作目录的 URL?不...让我试试看。
    • 您总是知道 JAR 的位置 - 它在“.”中。只要 JAR 的名称是已知的,您就可以在某处使用字符串常量。现在,如果人们去更改 JAR 的名称......
    • @Thomas:假设您从当前目录运行应用程序。 “java -jar foo/bar/baz.jar”有什么问题?
    • 我相信(并且必须验证),如果您的 Jar 中有 new File("baz.jar) 的代码,则 File 对象将代表您的 JAR 文件。
    • @Thomas:我不这么认为。我相信这将与进程的当前工作目录相关。不过我也得检查一下:)
    【解决方案11】:

    前段时间我做了一个从 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;
    }
    

    【讨论】:

      【解决方案12】:
      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;
      }
      

      【讨论】:

      • 虽然这段代码 sn-p 可以解决问题,但它没有解释为什么或如何回答这个问题。请include an explanation for your code,因为这确实有助于提高您的帖子质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
      • 当我们从 jar 文件中执行代码时数据为空。
      【解决方案13】:

      有两个非常有用的实用程序都称为 JarScan:

      1. www.inetfeedback.com/jarscan

      2. jarscan.dev.java.net

      另请参阅此问题:JarScan, scan all JAR files in all subfolders for specific class

      【讨论】:

        【解决方案14】:

        目前列出类路径中所有资源的最强大的机制是to use this pattern with ClassGraph,因为它处理widest possible array of classpath specification mechanisms,包括新的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();
        }
        

        还有lots of other ways to deal with resources

        【讨论】:

          【解决方案15】:

          对于匹配特定文件名更加灵活的道路,因为它使用通配符通配符,所以还有一个。在函数式风格中,这可能类似于:

          import java.io.IOException;
          import java.net.URISyntaxException;
          import java.nio.file.FileSystem;
          import java.nio.file.Files;
          import java.nio.file.Path;
          import java.nio.file.Paths;
          import java.util.function.Consumer;
          
          import static java.nio.file.FileSystems.getDefault;
          import static java.nio.file.FileSystems.newFileSystem;
          import static java.util.Collections.emptyMap;
          
          /**
           * Responsible for finding file resources.
           */
          public class ResourceWalker {
            /**
             * Globbing pattern to match font names.
             */
            public static final String GLOB_FONTS = "**.{ttf,otf}";
          
            /**
             * @param directory The root directory to scan for files matching the glob.
             * @param c         The consumer function to call for each matching path
             *                  found.
             * @throws URISyntaxException Could not convert the resource to a URI.
             * @throws IOException        Could not walk the tree.
             */
            public static void walk(
              final String directory, final String glob, final Consumer<Path> c )
              throws URISyntaxException, IOException {
              final var resource = ResourceWalker.class.getResource( directory );
              final var matcher = getDefault().getPathMatcher( "glob:" + glob );
          
              if( resource != null ) {
                final var uri = resource.toURI();
                final Path path;
                FileSystem fs = null;
          
                if( "jar".equals( uri.getScheme() ) ) {
                  fs = newFileSystem( uri, emptyMap() );
                  path = fs.getPath( directory );
                }
                else {
                  path = Paths.get( uri );
                }
          
                try( final var walk = Files.walk( path, 10 ) ) {
                  for( final var it = walk.iterator(); it.hasNext(); ) {
                    final Path p = it.next();
                    if( matcher.matches( p ) ) {
                      c.accept( p );
                    }
                  }
                } finally {
                  if( fs != null ) { fs.close(); }
                }
              }
            }
          }
          

          考虑参数化文件扩展名,留给读者练习。

          小心Files.walk。根据文档:

          此方法必须在 try-with-resources 语句或类似的控制结构中使用,以确保在流的操作完成后立即关闭流的打开目录。

          同样,newFileSystem 必须关闭,但必须在 walker 有机会访问文件系统路径之前关闭。

          【讨论】:

            【解决方案16】:

            顺便提一下,如果您已经在使用 Spring,您可以利用 PathMatchingResourcePatternResolver

            例如,从资源中的images 文件夹中获取所有 PNG 文件

            ClassLoader cl = this.getClass().getClassLoader(); 
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl);
            Resource[] resources = resolver.getResources("images/*.png");
            for (Resource r: resources){
                logger.info(r.getFilename());
                // From your example
                // ImageIO.read(cl.getResource("images/" + r.getFilename()));
            }
            

            【讨论】:

            • 简单而甜美,在码头工人的罐子里工作得很好
            【解决方案17】:

            只是一种从 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 
                }
            });
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-03-26
              • 1970-01-01
              • 2010-09-25
              • 2016-12-28
              • 1970-01-01
              • 2014-12-31
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多