java.nio.file.Path dla zasobu ścieżki klas

143

Czy istnieje interfejs API, aby uzyskać zasób ścieżki klas (np. Z tego, z czego otrzymam Class.getResource(String)) jako plik java.nio.file.Path? Idealnie, chciałbym używać nowych, fantazyjnych PathAPI z zasobami classpath.

Louis Wasserman
źródło
3
Cóż, biorąc długą ścieżkę (zamierzona gra słów), otrzymujesz Paths.get(URI)´URL.toURI () , and last getResource () `, która zwraca plik URL. Możesz połączyć je w łańcuch. Jednak nie próbowałem.
NilsH

Odpowiedzi:

174

Ten działa dla mnie:

return Paths.get(ClassLoader.getSystemResource(resourceName).toURI());
keyoxy
źródło
7
@VGR, jeśli zasoby w pliku .jar mogą wypróbować to `Resource resource = new ClassPathResource (" usage.txt "); BufferedReader reader = new BufferedReader (new InputStreamReader (resource.getInputStream ())); `zobacz stackoverflow.com/questions/25869428/…
zhuguowei
8
@zhuguowei to podejście specyficzne dla wiosny. W ogóle nie działa, gdy Spring nie jest używany.
Ryan J. McDonough
2
Jeśli twoja aplikacja nie opiera się na systemowym Thread.currentThread().getContextClassLoader().getResource(resourceName).toURI()
module ładującym
27

Domyślam się, że to, co chcesz zrobić, to wywołanie Files.lines (...) na zasobie pochodzącym ze ścieżki klas - prawdopodobnie z słoika.

Ponieważ Oracle zagmatwało pojęcie, kiedy ścieżka jest ścieżką, nie zmuszając getResource do zwracania użytecznej ścieżki, jeśli znajduje się ona w pliku jar, musisz zrobić coś takiego:

Stream<String> stream = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream("/filename.txt"))).lines();
user2163960
źródło
1
czy w Twoim przypadku potrzebny jest poprzedni class.getResourceznak „/”, nie wiem, ale w moim przypadku wymaga ukośnika, ale getSystemResourceAsStreamnie mogę znaleźć pliku poprzedzonego ukośnikiem.
Adam
11

Najbardziej ogólne rozwiązanie jest następujące:

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

Główną przeszkodą jest poradzenie sobie z dwiema możliwościami, albo posiadanie istniejącego systemu plików, którego powinniśmy używać, ale nie zamykania (jak w przypadku fileidentyfikatorów URI lub pamięci modułów Java 9), albo konieczność samodzielnego otwierania, a tym samym bezpiecznego zamykania systemu plików (np. zip / jar).

Dlatego powyższe rozwiązanie hermetyzuje rzeczywistą akcję w pliku interface, obsługuje oba przypadki, bezpiecznie zamykając później w drugim przypadku i działa od Java 7 do Java 10. Sprawdza, czy istnieje już otwarty system plików przed otwarciem nowego, więc działa również w przypadku, gdy inny składnik Twojej aplikacji już otworzył system plików dla tego samego pliku zip / jar.

Można go używać we wszystkich wymienionych powyżej wersjach Java, np. Do wypisywania zawartości pakietu ( java.langw przykładzie) jako Paths, na przykład:

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

W Javie 8 lub nowszym można używać wyrażeń lambda lub odwołań do metod, aby przedstawić rzeczywistą akcję, np

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

zrobić to samo.


Ostateczna wersja systemu modułów Java 9 zepsuła powyższy przykład kodu. JRE niekonsekwentnie zwraca ścieżkę /java.base/java/lang/Object.classdla Object.class.getResource("Object.class")podczas gdy powinno być /modules/java.base/java/lang/Object.class. Można to naprawić, dodając brakującą wartość, /modules/gdy ścieżka nadrzędna jest zgłaszana jako nieistniejąca:

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

Następnie będzie ponownie działać ze wszystkimi wersjami i metodami przechowywania.

Holger
źródło
1
To rozwiązanie działa świetnie! Mogę potwierdzić, że działa to ze wszystkimi zasobami (plikami, katalogami) zarówno w ścieżkach klas katalogów, jak i ścieżkach klas jar. Tak właśnie powinno się kopiować wiele zasobów w Javie 7+.
Mitchell Skaggs
10

Okazuje się, że możesz to zrobić z pomocą wbudowanego dostawcy systemu plików Zip . Jednak przekazanie identyfikatora URI zasobu bezpośrednio do Paths.getnie zadziała; zamiast tego należy najpierw utworzyć system plików zip dla identyfikatora URI jar bez nazwy wpisu, a następnie odnieść się do wpisu w tym systemie plików:

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

Aktualizacja:

Słusznie wskazano, że powyższy kod zawiera wyciek zasobów, ponieważ kod otwiera nowy obiekt FileSystem, ale nigdy go nie zamyka. Najlepszym podejściem jest przekazanie obiektu roboczego podobnego do Konsumenta, podobnie jak robi to odpowiedź Holgera. Otwórz system plików ZipFS na tyle długo, aby pracownik mógł zrobić wszystko, co musi zrobić ze ścieżką (o ile pracownik nie będzie próbował przechowywać obiektu ścieżki do późniejszego użycia), a następnie zamknij system plików.

VGR
źródło
11
Uważaj na nowo utworzone pliki fs. Drugie wywołanie przy użyciu tego samego jar spowoduje zgłoszenie wyjątku narzekającego na już istniejący system plików. Lepiej będzie spróbować (FileSystem fs = ...) {return fs.getPath (entryName);} lub jeśli chcesz mieć to buforowane, wykonaj bardziej zaawansowaną obsługę. W obecnej formie jest ryzykowne.
raisercostin
3
Poza kwestią potencjalnie niezamkniętego nowego systemu plików, założenia dotyczące relacji między schematami i konieczności otwierania nowego systemu plików oraz zagadki z zawartością URI ograniczają użyteczność rozwiązania. Skonfigurowałem nową odpowiedź, która pokazuje ogólne podejście, które upraszcza działanie i obsługuje jednocześnie nowe schematy, takie jak nowa pamięć masowa klasy Java 9. Działa również, gdy ktoś inny w aplikacji już otworzył system plików (lub metoda jest wywoływana dwukrotnie dla tego samego pliku jar)…
Holger
W zależności od zastosowania tego rozwiązania, niezamknięte newFileSystemmoże prowadzić do tego, że wiele zasobów będzie pozostawać otwartych na zawsze. Chociaż dodatek @raisercostin pozwala uniknąć błędu podczas próby utworzenia już utworzonego systemu plików, jeśli spróbujesz użyć zwróconego Path, otrzymasz plik ClosedFileSystemException. Odpowiedź @Holger działa dobrze dla mnie.
José Andias
Nie zamykałbym FileSystem. Jeśli załadujesz zasób z słoika, a następnie utworzysz wymagany FileSystem- FileSystempozwoli ci to również załadować inne zasoby z tego samego słoika. Po utworzeniu nowego FileSystemzasobu możesz po prostu spróbować ponownie załadować zasób za pomocą, Paths.get(Path)a implementacja automatycznie użyje nowego FileSystem.
NS du Toit
Oznacza to, że nie musisz używać #getPath(String)metody na FileSystemobiekcie.
NS du Toit
5

Napisałem małą metodę pomocniczą do odczytu Pathsz zasobów Twojej klasy. Jest to bardzo wygodne w użyciu, ponieważ wymaga jedynie odniesienia do klasy, w której przechowujesz zasoby, oraz nazwy samego zasobu.

public static Path getResourcePath(Class<?> resourceClass, String resourceName) throws URISyntaxException {
    URL url = resourceClass.getResource(resourceName);
    return Paths.get(url.toURI());
}  
Michael Stauffer
źródło
1

Nie można utworzyć identyfikatora URI z zasobów znajdujących się w pliku jar. Możesz po prostu zapisać go w pliku tymczasowym, a następnie użyć (java8):

Path path = File.createTempFile("some", "address").toPath();
Files.copy(ClassLoader.getSystemResourceAsStream("/path/to/resource"), path, StandardCopyOption.REPLACE_EXISTING);
user1079877
źródło
1

Odczytaj plik z folderu zasobów za pomocą NIO, w 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();
    }
Real Coder
źródło
0

Musisz zdefiniować system plików, aby odczytywać zasoby z pliku jar, jak wspomniano w https://docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/zipfilesystemprovider.html . Udało mi się odczytać zasób z pliku jar z poniższymi kodami:

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
źródło