关于Java:如何在JAR文件中列出文件?

How to list the files inside a JAR file?

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

1
2
3
4
5
6
7
    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文件中的所有图像,这样我就可以加载它们:

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

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

编辑

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

可以使用java.util.Zip读取它。

我的结构是这样的:

他们就像:

1
2
3
4
5
6
7
8
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"

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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和"爆炸"目录的代码更加容易。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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());
        }
    }
}


埃里克森的答案很完美:

这是工作代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() ] );

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

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

对此:

1
2
3
String  [] webimages = ...

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

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

  • 它不会关闭FileSystem对象。
  • 它不会检查FileSystem对象是否已经存在。
  • 它不是线程安全的。
  • 这是一个较为安全的解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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接口,并且可能会干扰它们(即使在单线程应用程序中)。
    另外,它不会检查null(例如,在getClass().getResource()上)。

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


    So I guess my main problem would be, how to know the name of the jar where my main class lives.

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

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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和爆炸类。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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));
                }
            }
        }
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
        }
    }


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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;
                    }
                });
            }
        }
    }

    前段时间,我做了一个从JAR内部获取类的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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;
    }

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

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


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

    编辑/澄清后

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

    1
    2
    3
    4
    5
    6
    7
    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


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

    How to know the name of the JAR file where my main class lives?

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

    How can I read the contents of a directory in a similar fashion within a JAR file?

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

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


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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("
    "
    );
        List<String> a = Arrays.asList(s);
        ArrayList<String> m = new ArrayList<>(a);
        return m;
    }


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

  • www.inetfeedback.com/jarscan

  • jarscan.dev.java.net

  • 另请参阅以下问题:JarScan,扫描所有子文件夹中的所有JAR文件以查找特定的类


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

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

    1
    2
    3
    4
    5
    6
    7
    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
        }
    });