用java.nio.file.Path获取类路径资源

193

3
еҘҪзҡ„пјҢиө°дёҖжқЎиҫғй•ҝзҡ„и·ҜпјҲеҸҢе…іиҜӯпјүпјҢдҪ еҸҜд»ҘдҪҝз”ЁPaths.get(URI)гҖҒ然еҗҺжҳҜВҙURL.toURI()пјҢжңҖеҗҺжҳҜиҝ”еӣһдёҖдёӘURLзҡ„getResource()`гҖӮдҪ еҸҜиғҪеҸҜд»Ҙе°Ҷе®ғ们й“ҫжҺҘеңЁдёҖиө·дҪҝз”ЁпјҢдҪҶжҲ‘жІЎжңүе°қиҜ•иҝҮгҖӮ - NilsH
8个回答

260

这个对我有效:

return Path.of(ClassLoader.getSystemResource(resourceName).toURI());

10
如果资源文件在.jar文件中,可以尝试以下代码:Resource resource = new ClassPathResource("usage.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));更多信息请查看 https://dev59.com/GF8e5IYBdhLWcg3wTJGL - zhuguowei
10
@zhuguowei 这是一个特定于Spring的方法。如果没有使用Spring,它根本无法工作。 - Ryan J. McDonough
3
如果您的应用程序不依赖于系统类加载器,那么应该使用Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI() - ThrawnCA
1
这个解决方案需要 Java 11,是吗? - happy_marmoset

31

我猜你想做的是,在类路径下调用Files.lines(...)方法,可能来自于一个jar包中的资源。

因为Oracle混淆了什么时候PathPath,没有让getResource方法返回可用的路径,如果它存在于jar文件中,所以你需要像这样做:

Stream<String> stream = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream("/filename.txt"))).lines();

1
我不知道在你的情况下是否需要前面的“/”,但在我的情况下,class.getResource 需要斜杠,而 getSystemResourceAsStream 在加上斜杠前找不到文件。 - Adam

15
最通用的解决方案如下:
interface IOConsumer<T> {
    void accept(T t) throws IOException;
}
public static void processRessource(URI uri, IOConsumer<Path> action) throws IOException{
    try {
        Path p=Paths.get(uri);
        action.accept(p);
    }
    catch(FileSystemNotFoundException ex) {
        try(FileSystem fs = FileSystems.newFileSystem(
                uri, Collections.<String,Object>emptyMap())) {
            Path p = fs.provider().getPath(uri);
            action.accept(p);
        }
    }
}

主要的难点是处理两种可能性,一种是已经存在但未关闭的文件系统(例如使用file URI或Java 9的模块存储),另一种是需要自己打开并安全关闭文件系统(例如zip/jar文件)。
因此,上述解决方案将实际操作封装在一个接口中,处理这两种情况,并在第二种情况下安全关闭文件系统,适用于从Java 7到Java 18。它会探测是否已经有一个打开的文件系统,然后再打开一个新的文件系统,因此它也适用于应用程序的另一个组件已经为相同的zip/jar文件打开了文件系统的情况。
它可以在以上所有命名的Java版本中使用,例如像这样列出包(例如示例中的java.lang)的内容作为Path
processRessource(Object.class.getResource("Object.class").toURI(),new IOConsumer<Path>(){
    public void accept(Path path) throws IOException {
        try(DirectoryStream<Path> ds = Files.newDirectoryStream(path.getParent())) {
            for(Path p: ds)
                System.out.println(p);
        }
    }
});

使用Java 8或更高版本,您可以使用lambda表达式或方法引用来表示实际操作,例如:

processRessource(Object.class.getResource("Object.class").toURI(), path -> {
    try(Stream<Path> stream = Files.list(path.getParent())) {
        stream.forEach(System.out::println);
    }
});

Java 9模块系统的最终版本已经破坏了上述代码示例。从Java 9到12版本,Paths.get(Object.class.getResource("Object.class"))不一致地返回路径/java.base/java/lang/Object.class,而应该是/modules/java.base/java/lang/Object.class。如果父路径不存在,则可以通过在其前面添加缺少的/modules/来修复此问题:

processRessource(Object.class.getResource("Object.class").toURI(), path -> {
    Path p = path.getParent();
    if(!Files.exists(p))
        p = p.resolve("/modules").resolve(p.getRoot().relativize(p));
    try(Stream<Path> stream = Files.list(p)) {
        stream.forEach(System.out::println);
    }
});

然后,它将再次与所有版本和存储方法一起工作。从JDK 13开始,这种解决方法不再必要。


2
这个解决方案非常好!我可以确认它适用于目录类路径和jar类路径中的所有资源(文件、目录)。这绝对是在Java 7+中复制大量资源的正确方法。 - Mitchell Skaggs

11

原来可以通过内置的Zip文件系统提供程序来完成此操作。 但是,直接将资源URI传递给Paths.get不起作用; 相反,必须首先为不带条目名称的jar URI创建zip文件系统,然后在该文件系统中引用该条目:

static Path resourceToPath(URL resource)
throws IOException,
       URISyntaxException {

    Objects.requireNonNull(resource, "Resource URL cannot be null");
    URI uri = resource.toURI();

    String scheme = uri.getScheme();
    if (scheme.equals("file")) {
        return Paths.get(uri);
    }

    if (!scheme.equals("jar")) {
        throw new IllegalArgumentException("Cannot convert to Path: " + uri);
    }

    String s = uri.toString();
    int separator = s.indexOf("!/");
    String entryName = s.substring(separator + 2);
    URI fileURI = URI.create(s.substring(0, separator));

    FileSystem fs = FileSystems.newFileSystem(fileURI,
        Collections.<String, Object>emptyMap());
    return fs.getPath(entryName);
}

更新:

正如指出的那样,上面的代码存在资源泄漏问题,因为代码打开了一个新的FileSystem对象但从未关闭。最好的方法是传递类似于Consumer的worker对象,就像Holger的答案中所做的那样。仅在工作器需要执行与路径相关的任务时打开ZipFS FileSystem(只要工作器不尝试存储Path对象以供以后使用),然后关闭FileSystem。


11
注意新创建的文件系统(fs)。如果再次使用相同的jar文件进行第二次调用,会抛出异常,指出已存在该文件系统。最好采用try(FileSystem fs=...){return fs.getPath(entryName);}方法,或者如果您希望对其进行缓存,则可以进行更高级的处理。以当前的形式存在风险。 - raisercostin
3
除了潜在的非关闭新文件系统的问题外,方案之间关系的假设以及打开新文件系统的必要性和URI内容的限制,都限制了该解决方案的实用性。我已经提供了一个新答案,展示了一种通用方法,简化了操作,同时处理了像新的Java 9类存储这样的新方案。当应用程序中的其他人已经打开文件系统(或为同一个jar文件调用两次该方法)时,该方法也可以正常工作... - Holger
根据此解决方案的使用情况,未关闭的newFileSystem可能会导致多个资源一直处于打开状态。尽管@raisercostin的补充避免了在尝试创建已创建的文件系统时出现错误,但如果您尝试使用返回的Path,则会收到ClosedFileSystemException。 @Holger的回答对我很有效。 - José Andias
Path 对象在关闭 FileSystem 后也很难使用,因此根据代码结构的不同,它可能会变得非常丑陋。例如,您无法在实用程序方法中返回 Path 并在实用程序方法内部关闭 FileSystem,因为使用 Path 对象将会由于底层的 FileSystem 被关闭而抛出错误。 - dutoitns
请注意,奇怪的是,Files#isRegularFile(Path)不会对Jar文件中的资源返回true - dutoitns
显示剩余3条评论

5

我写了一个小的辅助方法来读取你的类资源中的Paths。它非常方便易用,只需要引用你存储资源的类以及资源本身的名称。

public static Path getResourcePath(Class<?> resourceClass, String resourceName) throws URISyntaxException {
    URL url = resourceClass.getResource(resourceName);
    return Paths.get(url.toURI());
}  

2

你无法从jar文件内部的资源创建URI。你可以将其简单地写入临时文件,然后使用它(java8):

Path path = File.createTempFile("some", "address").toPath();
Files.copy(ClassLoader.getSystemResourceAsStream("/path/to/resource"), path, StandardCopyOption.REPLACE_EXISTING);

2

使用NIO在Java8中从资源文件夹读取文件

public static String read(String fileName) {

        Path path;
        StringBuilder data = new StringBuilder();
        Stream<String> lines = null;
        try {
            path = Paths.get(Thread.currentThread().getContextClassLoader().getResource(fileName).toURI());
            lines = Files.lines(path);
        } catch (URISyntaxException | IOException e) {
            logger.error("Error in reading propertied file " + e);
            throw new RuntimeException(e);
        }

        lines.forEach(line -> data.append(line));
        lines.close();
        return data.toString();
    }

0

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