从jar包中读取资源文件

348

我想从我的jar文件中读取资源,方法如下:

File file;
file = new File(getClass().getResource("/file.txt").toURI());
BufferedReader reader = new BufferedReader(new FileReader(file));

//Read the file

在Eclipse中运行代码时工作正常,但是如果将其导出为jar文件然后运行,会出现IllegalArgumentException异常:

Exception in thread "Thread-2"
java.lang.IllegalArgumentException: URI is not hierarchical

而且我真的不知道为什么,但是通过一些测试我发现如果我改变

file = new File(getClass().getResource("/file.txt").toURI());

file = new File(getClass().getResource("/folder/file.txt").toURI());

然后情况就反过来了(在 jar 中可以运行,但在 Eclipse 中不行)。

我正在使用 Eclipse,并且包含我的文件的文件夹位于一个类文件夹中。


如果您想从包含任意数量文件的jar目录中读取文件,请参见Stackoverflow-Link - Michael Hegner
1
我不确定原始问题是否涉及Spring。上一个评论中的链接是来自另一个问题的Spring特定答案。我认为getResourceAsStream仍然是解决问题更简单、更可移植的方法。 - Drew MacInnis
15个回答

526

不要试图将资源视为File,而是通过getResourceAsStream请求ClassLoader返回资源的InputStream

try (InputStream in = getClass().getResourceAsStream("/file.txt");
    BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
    // Use resource
}

只要类路径下存在file.txt资源,无论file.txt资源是在classes/目录中还是在jar包内部,这种方法都可以正常工作。
因为在jar文件中的资源的URI看起来像这样:file:/example.jar!/file.txt,所以会出现"URI is not hierarchical"的错误。你不能像操作普通File一样读取一个jar(即zip文件)内的条目。
这个问题的答案已经被很好地解释了:

3
谢谢,这很有帮助并且代码完美运行,但我有一个问题,我需要确定InputStream是否存在(类似于File.exists()),以便我的游戏可以判断是否使用默认文件。谢谢。 - Koi
1
哦,顺便说一下,getClass().getResource("**/folder**/file.txt") 之所以能够正常工作,是因为我把那个文件夹放在与我的 JAR 文件相同的目录中。 :) - Koi
10
如果资源不存在,getResourceAsStream 返回空值,因此这可以作为您的“存在”测试。 - Drew MacInnis
2
当然,不要忘记关闭inputStream和BufferedReader。 - Noremac
5
你无法像读取普通文件那样读取一个 jar 文件(即 zip 文件)中的条目。 这很糟糕,因为有大量的库函数期望以文件路径作为输入。 - ptkvsk
显示剩余12条评论

49
要访问 jar 文件中的文件,您有两个选项:
  • 将文件放在与包名称匹配的目录结构中(在提取 .jar 文件后,它应该与 .class 文件在同一个目录中),然后使用 getClass().getResourceAsStream("file.txt") 访问它。

  • 将文件放在根目录下(在提取 .jar 文件后,它应该在根目录下),然后使用 Thread.currentThread().getContextClassLoader().getResourceAsStream("file.txt") 访问它。

当 jar 用作插件时,第一种选项可能无法正常工作。


非常感谢您提供的这个好答案... 第二种选择的另一个好处是它也可以从主方法中工作,因为getClass()仅从实例而不是静态方法中工作。 - Dan Ortega

12

我之前遇到过这个问题,所以我设置了备用的加载方式。基本上第一种方式适用于.jar文件中,而第二种方式适用于eclipse或其他IDE。

public class MyClass {

    public static InputStream accessFile() {
        String resource = "my-file-located-in-resources.txt";

        // this is the path within the jar file
        InputStream input = MyClass.class.getResourceAsStream("/resources/" + resource);
        if (input == null) {
            // this is how we load file within editor (eg eclipse)
            input = MyClass.class.getClassLoader().getResourceAsStream(resource);
        }

        return input;
    }
}

对我不起作用....我在jar资源中有.wsdl文件...但仍然无法解析wsdl。 - divyanayan awasthi

8

到目前为止(2017年12月),这是我找到的唯一一个在IDE内外都能正常运行的解决方案。

使用PathMatchingResourcePatternResolver

注意:它也适用于spring-boot。

在此示例中,我正在读取位于src/main/resources/my_folder中的某些文件:

try {
    // Get all the files under this inner resource folder: my_folder
    String scannedPackage = "my_folder/*";
    PathMatchingResourcePatternResolver scanner = new PathMatchingResourcePatternResolver();
    Resource[] resources = scanner.getResources(scannedPackage);

    if (resources == null || resources.length == 0)
        log.warn("Warning: could not find any resources in this scanned package: " + scannedPackage);
    else {
        for (Resource resource : resources) {
            log.info(resource.getFilename());
            // Read the file content (I used BufferedReader, but there are other solutions for that):
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                // ...
                // ...                      
            }
            bufferedReader.close();
        }
    }
} catch (Exception e) {
    throw new Exception("Failed to read the resources folder: " + e.getMessage(), e);
}

7

问题在于某些第三方库需要文件路径而不是输入流。大多数答案都没有解决这个问题。

在这种情况下,一种解决方法是将资源内容复制到临时文件中。以下示例使用 jUnit 的 TemporaryFolder

    private List<String> decomposePath(String path){
        List<String> reversed = Lists.newArrayList();
        File currFile = new File(path);
        while(currFile != null){
            reversed.add(currFile.getName());
            currFile = currFile.getParentFile();
        }
        return Lists.reverse(reversed);
    }

    private String writeResourceToFile(String resourceName) throws IOException {
        ClassLoader loader = getClass().getClassLoader();
        InputStream configStream = loader.getResourceAsStream(resourceName);
        List<String> pathComponents = decomposePath(resourceName);
        folder.newFolder(pathComponents.subList(0, pathComponents.size() - 1).toArray(new String[0]));
        File tmpFile = folder.newFile(resourceName);
        Files.copy(configStream, tmpFile.toPath(), REPLACE_EXISTING);
        return tmpFile.getAbsolutePath();
    }

这个经常被忽略。感谢您的记录。我很好奇在没有JUnit的情况下还有哪些选项存在。 - Alex Moore-Niemi
3
由于resourceName应始终使用/作为分隔符,而不像decomposePath内部的File操作使用特定于系统的文件分隔符,因此该方法与path.split("/")相比不仅过于复杂,甚至是错误的。此外,在您不使用其结果时,不清楚为什么要调用.toArray(new String [0]) - Holger
1
“folder” 是什么, “Lists” 属于哪个库, “REPLACE_EXISTING” 是什么意思…? 请在您的帖子中提供完整的信息。 - ElectRocnic

4

在我的情况下,我最终通过以下方式实现了目标:

import java.lang.Thread;
import java.io.BufferedReader;
import java.io.InputStreamReader;

final BufferedReader in = new BufferedReader(new InputStreamReader(
      Thread.currentThread().getContextClassLoader().getResourceAsStream("file.txt"))
); // no initial slash in file.txt

1
很奇怪的是,在我的嵌套资源中,这也是解决方案。不过我想知道为什么其他发布的答案中带斜杠似乎也能工作。 - ElectRocnic
1
@ElectRocnic 我认为这是由于路径解释的方式不同。"/file.txt" 表示切换到当前目录并打开 file.txt,而 "file.txt" 表示从当前位置打开 file.txt。它们都指向相同的位置。对我来说,在 Windows 系统中两者都可以工作。 - Srishti Sharma
啊哈,这在Docker+Linux环境下对我不起作用...感谢您的陈述。 - ElectRocnic
对我来说,简单地使用 getClass().getClassLoader().getResourceAsStream() 就可以了。 - Yaniv K.

1
我找到了一个解决方法。
BufferedReader br = new BufferedReader(new InputStreamReader(Main.class.getResourceAsStream(path)));

将“Main”替换为您编写的Java类。将“path”替换为jar文件中的路径。例如,如果您将State1.txt放在com.issac.state包中,则在Linux或Mac上将路径输入为“/com/issac/state/State1”,在Windows上输入路径为“\com\issac\state\State1”。除非出现“File not found exception”异常,否则不要添加.txt后缀名。

1

确保使用正确的分隔符。我用 File.separator 替换了相对路径中的所有 /。这在IDE中运行良好,但在构建JAR时无效。


0

最终我解决了错误:

String input_path = "resources\\file.txt";
        
        input_path = input_path.replace("\\", "/");  // doesn't work with back slash
        
        URL file_url = getClass().getClassLoader().getResource(input_path);
        String file_path = new URI(file_url.toString().replace(" ","%20")).getSchemeSpecificPart();
        InputStream file_inputStream = file_url.openStream();

请考虑添加更多关于您的解决方案以及为什么它有效的信息。 - Jaco-Ben Vosloo

0

这段代码可以在Eclipse和导出的可运行JAR文件中都正常工作。

private String writeResourceToFile(String resourceName) throws IOException {
    File outFile = new File(certPath + File.separator + resourceName);

    if (outFile.isFile())
        return outFile.getAbsolutePath();
    
    InputStream resourceStream = null;
    
    // Java: In caso di JAR dentro il JAR applicativo 
    URLClassLoader urlClassLoader = (URLClassLoader)Cypher.class.getClassLoader();
    URL url = urlClassLoader.findResource(resourceName);
    if (url != null) {
        URLConnection conn = url.openConnection();
        if (conn != null) {
            resourceStream = conn.getInputStream();
        }
    }
    
    if (resourceStream != null) {
        Files.copy(resourceStream, outFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
        return outFile.getAbsolutePath();
    } else {
        System.out.println("Embedded Resource " + resourceName + " not found.");
    }
    
    return "";
}   

2
这里的 certPath 是什么? - Tom Taylor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接