java.nio.file.Path pour une ressource classpath

143

Existe-t-il une API pour obtenir une ressource classpath (par exemple ce que j'obtiendrais Class.getResource(String)) en tant que java.nio.file.Path? Idéalement, j'aimerais utiliser les nouvelles PathAPI sophistiquées avec des ressources de chemin de classe.

Louis Wasserman
la source
3
Eh bien, en prenant le long chemin (jeu de mots), vous avez Paths.get(URI), alors ´URL.toURI () , and last getResource () `qui retourne un URL. Vous pourrez peut-être les enchaîner. Je n'ai pas essayé cependant.
NilsH

Réponses:

174

Celui-ci fonctionne pour moi:

return Paths.get(ClassLoader.getSystemResource(resourceName).toURI());
keyoxy
la source
7
@VGR si les ressources du fichier .jar peuvent essayer cette ressource ressource = new ClassPathResource ("usage.txt"); BufferedReader reader = new BufferedReader (new InputStreamReader (resource.getInputStream ())); `s'il vous plaît voir stackoverflow.com/questions/25869428
...
8
@zhuguowei, c'est une approche spécifique à Spring. Cela ne fonctionne pas du tout lorsque Spring n'est pas utilisé.
Ryan J. McDonough
2
Si votre application ne repose pas sur le chargeur de Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI()
classe
27

Je suppose que ce que vous voulez faire, c'est d'appeler Files.lines (...) sur une ressource qui provient du classpath - peut-être depuis un fichier jar.

Étant donné qu'Oracle a compliqué la notion de quand un chemin est un chemin en ne faisant pas que getResource renvoie un chemin utilisable s'il réside dans un fichier jar, ce que vous devez faire est quelque chose comme ceci:

Stream<String> stream = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream("/filename.txt"))).lines();
user2163960
la source
1
si le "/" précédent est nécessaire dans votre cas, je ne sais pas, mais dans mon cas class.getResourcenécessite une barre oblique mais getSystemResourceAsStreamne peut pas trouver le fichier lorsqu'il est préfixé par une barre oblique.
Adam le
11

La solution la plus générale est la suivante:

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

Le principal obstacle est de gérer les deux possibilités, soit d'avoir un système de fichiers existant que nous devrions utiliser, mais pas de fermer (comme avec les fileURI ou le stockage de modules de Java 9), soit d'avoir à ouvrir et donc fermer en toute sécurité le système de fichiers nous-mêmes (comme zip / jar).

Par conséquent, la solution ci-dessus encapsule l'action réelle dans un interface, gère les deux cas, se fermant ensuite en toute sécurité dans le second cas, et fonctionne de Java 7 à Java 10. Elle vérifie s'il existe déjà un système de fichiers ouvert avant d'en ouvrir un nouveau, donc il fonctionne également dans le cas où un autre composant de votre application a déjà ouvert un système de fichiers pour le même fichier zip / jar.

Il peut être utilisé dans toutes les versions de Java nommées ci-dessus, par exemple pour lister le contenu d'un paquet ( java.langdans l'exemple) en tant que Paths, comme ceci:

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

Avec Java 8 ou plus récent, vous pouvez utiliser des expressions lambda ou des références de méthode pour représenter l'action réelle, par exemple

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

Faire la même chose.


La version finale du système de modules de Java 9 a cassé l'exemple de code ci-dessus. Le JRE renvoie de manière incohérente le chemin /java.base/java/lang/Object.classpour Object.class.getResource("Object.class")alors qu'il devrait l'être /modules/java.base/java/lang/Object.class. Cela peut être résolu en ajoutant les éléments manquants /modules/lorsque le chemin parent est signalé comme inexistant:

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

Ensuite, il fonctionnera à nouveau avec toutes les versions et méthodes de stockage.

Holger
la source
1
Cette solution fonctionne très bien! Je peux confirmer que cela fonctionne avec toutes les ressources (fichiers, répertoires) dans les chemins de classe de répertoire et les chemins de classe jar. C'est certainement ainsi que la copie de nombreuses ressources doit être effectuée dans Java 7+.
Mitchell Skaggs le
10

Il s'avère que vous pouvez le faire, avec l'aide du fournisseur de système de fichiers Zip intégré . Cependant, passer directement un URI de ressource à Paths.getne fonctionnera pas; au lieu de cela, il faut d'abord créer un système de fichiers zip pour l'URI jar sans le nom d'entrée, puis se référer à l'entrée dans ce système de fichiers:

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

Mettre à jour:

Il a été souligné à juste titre que le code ci-dessus contient une fuite de ressources, car le code ouvre un nouvel objet FileSystem mais ne le ferme jamais. La meilleure approche consiste à transmettre un objet worker de type Consumer, tout comme la réponse de Holger le fait. Ouvrez le système de fichiers ZipFS juste assez longtemps pour que le travailleur fasse tout ce qu'il doit faire avec le chemin (tant que le travailleur n'essaye pas de stocker l'objet Chemin pour une utilisation ultérieure), puis fermez le système de fichiers.

VGR
la source
11
Faites attention aux fs nouvellement créés. Un deuxième appel utilisant le même fichier jar lèvera une exception se plaignant d'un système de fichiers déjà existant. Il vaudra mieux essayer (FileSystem fs = ...) {return fs.getPath (entryName);} ou si vous voulez que ce cache soit mis en cache, faites une gestion plus avancée. Dans la forme actuelle, c'est risqué.
raisercostin
3
Outre le problème du nouveau système de fichiers potentiellement non fermé, les hypothèses sur la relation entre les schémas et la nécessité d'ouvrir un nouveau système de fichiers et la confusion avec le contenu de l'URI limitent l'utilité de la solution. J'ai mis en place une nouvelle réponse qui montre une approche générale qui simplifie l'opération et gère de nouveaux schémas comme le nouveau stockage de classe Java 9 en même temps. Cela fonctionne également lorsque quelqu'un d'autre dans l'application a déjà ouvert le système de fichiers (ou que la méthode est appelée deux fois pour le même pot)…
Holger
En fonction de l'utilisation de cette solution, le non fermé newFileSystempeut conduire à plusieurs ressources qui restent ouvertes pour toujours. Bien que l'addendum @raisercostin évite l'erreur lorsque vous essayez de créer un système de fichiers déjà créé, si vous essayez d'utiliser le fichier renvoyé, Pathvous obtiendrez un fichier ClosedFileSystemException. La réponse @Holger fonctionne bien pour moi.
José Andias
Je ne fermerais pas le FileSystem. Si vous chargez une ressource à partir d'un Jar et que vous créez ensuite le fichier requis FileSystem, FileSystemcela vous permettra également de charger d'autres ressources à partir du même Jar. De plus, une fois que vous avez créé le nouveau, FileSystemvous pouvez simplement essayer de charger à nouveau la ressource en utilisant Paths.get(Path)et l'implémentation utilisera automatiquement le nouveau FileSystem.
NS du Toit
Ie vous n'avez pas à utiliser la #getPath(String)méthode sur l' FileSystemobjet.
NS du Toit le
5

J'ai écrit une petite méthode d'aide à lire à Pathspartir de vos ressources de classe. Il est assez pratique à utiliser car il ne nécessite qu'une référence de la classe dans laquelle vous avez stocké vos ressources ainsi que le nom de la ressource elle-même.

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

Vous ne pouvez pas créer d'URI à partir des ressources à l'intérieur du fichier jar. Vous pouvez simplement l'écrire dans le fichier temporaire, puis l'utiliser (java8):

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

Lire un fichier à partir du dossier de ressources à l'aide de NIO, dans 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();
    }
Codeur réel
la source
0

Vous devez définir le système de fichiers pour lire la ressource à partir du fichier jar comme indiqué dans https://docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/zipfilesystemprovider.html . J'ai réussi à lire la ressource à partir du fichier jar avec les codes ci-dessous:

Map<String, Object> env = new HashMap<>();
try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {

        Path path = fs.getPath("/path/myResource");

        try (Stream<String> lines = Files.lines(path)) {
            ....
        }
    }
user3050291
la source