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

我有这样一个代码,它可以读取目录中的所有文件。

    File textFolder = new File("text_directory");


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

它工作得很好,它将数组中所有以“ . txt”结尾的文件填充到目录“ text _ directory”中。

如何以类似的方式读取目录的内容 内心一个 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));

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

164940 次浏览

Jar 文件只是带有结构化清单的 zip 文件。您可以使用常用的 java 压缩工具打开 jar 文件,并以这种方式扫描文件内容、扩充流等等。然后在 getResourceAsStream 调用中使用它,应该会非常顺利。

编辑/澄清后

我花了一分钟才记起所有的细节,我相信有更干净的方法来做这件事,但我想看到我没有疯。在我的项目 image.jpg 中,文件位于主 jar 文件的某个部分。我获取主类的类装入器(SomClass 是入口点) ,并使用它来发现 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 文件,您可以使用 JarFile.entries()列出内容。但是,您需要知道 JAR 文件的位置-您不能仅仅要求类加载器列出它可以获得的所有内容。

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

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

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

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

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

一定要测试类不是 jar 的一部分的情况。

Erickson 的 回答运行得很好:

这是工作代码。

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

我刚刚从这里修改了 load 方法:

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

这样说:

String  [] webimages = ...


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

有两个非常有用的工具都叫做 JarScan:

  1. Www.inetfeedback.com/jarscan

  2. Jarscan.dev.java.net

也请看这个问题: JarScan,扫描所有子文件夹中特定类的所有 JAR 文件

一段时间以前,我创建了一个从 JAR 内部获取 class 的函数:

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

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

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

下面是一个使用 反光库通过正则表达式名称模式递归扫描类路径的例子,这个名称模式增加了一些 番石榴特权来获取资源内容:

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 和爆炸类。

我想扩展一下 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接口,并且它可能会干扰它们(即使在单线程应用程序中)。
此外,它不检查 null(例如,在 getClass().getResource()上)。

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

我已经将 阿克伦55号的回答移植到 Java7并关闭了 FileSystem对象。这段代码可以在 IDE 中、在 jar 文件中以及在 Tomcat 7的 war 中的 jar 中工作; 但是请注意,它在 JBoss 7的 war 中的 jar 中工作(它提供了 FileSystemNotFoundException: Provider "vfs" not installed,也参见 这篇文章)。此外,像原始代码一样,它不是线程安全的,正如 所建议的那样。由于这些原因,我放弃了这个解决方案; 但是,如果您能够接受这些问题,这里是我现成的代码:

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

列出类路径中所有资源的最健壮的机制当前是 在 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();
}

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

另外还有一个问题,它对于匹配特定的文件名来说更加灵活,因为它使用了通配符 globbing。在功能风格上,这可能类似于:

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 有机会访问文件系统路径之前关闭 newFileSystem

只是提一下,如果您已经在使用 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()));
}