Preferowany sposób ładowania zasobów w Javie

107

Chciałbym poznać najlepszy sposób ładowania zasobu w Javie:

  • this.getClass().getResource() (or getResourceAsStream()),
  • Thread.currentThread().getContextClassLoader().getResource(name),
  • System.class.getResource(name).
Doc Davluz
źródło

Odpowiedzi:

140

Opracuj rozwiązanie zgodnie z tym, co chcesz ...

Są dwie rzeczy, które getResource/ getResourceAsStream()otrzyma od klasy, do której się odwołuje ...

  1. Moduł ładujący klasy
  2. Lokacja startowa

Więc jeśli tak

this.getClass().getResource("foo.txt");

spróbuje załadować foo.txt z tego samego pakietu co klasa „this” oraz z programem ładującym klasy „this”. Jeśli umieścisz znak „/” na początku, odwołujesz się do zasobu.

this.getClass().getResource("/x/y/z/foo.txt")

załaduje zasób z programu ładującego klasy „this” iz pakietu xyz (będzie musiał znajdować się w tym samym katalogu, co klasy w tym pakiecie).

Thread.currentThread().getContextClassLoader().getResource(name)

załaduje się za pomocą programu ładującego klasy kontekstu, ale nie rozwiąże nazwy zgodnie z żadnym pakietem (musi być bezwzględnie przywoływany)

System.class.getResource(name)

Załaduje zasób za pomocą programu ładującego klasy systemowe (musiałby również być bezwzględnie odwołany, ponieważ nie będziesz w stanie umieścić niczego w pakiecie java.lang (pakiet System).

Spójrz tylko na źródło. Wskazuje również, że getResourceAsStream po prostu wywołuje „openStream” w adresie URL zwróconym przez getResource i zwraca go.

Michael Wiles
źródło
Nazwy pakietów AFAIK nie mają znaczenia, robi to ścieżka klas programu ładującego klasy.
Bart van Heukelom
@Bart Jeśli spojrzysz na kod źródłowy, zauważysz, że nazwa klasy ma znaczenie, gdy wywołujesz getResource na klasie. Pierwszą rzeczą, jaką robi to wywołanie, jest wywołanie „solutionName”, które w razie potrzeby dodaje przedrostek pakietu. Javadoc dla funkcji solutionName to „Dodaj przedrostek nazwy pakietu, jeśli nazwa nie jest bezwzględna. Usuń początkowy znak” / „jeśli nazwa jest absolutna”
Michael Wiles
10
O, rozumiem. Bezwzględne tutaj oznacza względne do ścieżki klas, a nie bezwzględne dla systemu plików.
Bart van Heukelom
1
Chcę tylko dodać, że zawsze należy sprawdzać, czy strumień zwrócony z getResourceAsStream () nie ma wartości null, ponieważ będzie tak, jeśli zasób nie znajduje się w ścieżce klas.
stenix
Warto również zauważyć, że użycie modułu ładującego klasy kontekstu umożliwia zmianę programu ładującego klasy w czasie wykonywania za pośrednictwem Thread#setContextClassLoader. Jest to przydatne, jeśli chcesz zmodyfikować ścieżkę klasy podczas wykonywania programu.
Max
14

Cóż, częściowo zależy to od tego, co chcesz, aby się wydarzyło, jeśli faktycznie jesteś w klasie pochodnej.

Na przykład załóżmy, że SuperClassznajduje się w A.jar i SubClassB.jar, a wykonujesz kod w metodzie instancji zadeklarowanej w, SuperClassale gdzie thisodnosi się do wystąpienia SubClass. Jeśli używasz this.getClass().getResource(), będzie wyglądać w stosunku do SubClass, w B.jar. Podejrzewam, że zwykle nie jest to wymagane.

Osobiście prawdopodobnie Foo.class.getResourceAsStream(name)używałbym najczęściej - jeśli znasz już nazwę zasobu, którego szukasz, i jesteś pewien, gdzie jest ona w stosunku do Foo, jest to najbardziej solidny sposób zrobienia tego IMO.

Oczywiście są chwile, kiedy nie tego też chcesz: oceniaj każdy przypadek na podstawie jego meritum. To po prostu „Wiem, że ten zasób jest dołączony do tej klasy” jest najczęstszym, na jaki się natknąłem.

Jon Skeet
źródło
skeet: wątpliwość w stwierdzeniu "wykonujesz kod w metodzie instancji klasy SuperClass, ale gdzie odnosi się to do instancji klasy SubClass" jeśli wykonujemy instrukcje wewnątrz metody instancji klasy super, wówczas "this" będzie odnosić się do nadklasy, a nie podklasa.
Dead Programmer,
1
@Suresh: Nie, nie będzie. Spróbuj! Utwórz dwie klasy, sprawiając, że jedna pochodzi od drugiej, a następnie wydrukuj w nadklasie this.getClass(). Utwórz instancję podklasy i wywołaj metodę ... wydrukuje nazwę podklasy, a nie nadklasy.
Jon Skeet,
dzięki metoda instancji podklasy wywołuje metodę nadklasy.
Dead Programmer,
1
Zastanawiam się, czy użycie this.getResourceAsStream będzie w stanie załadować zasób tylko z tego samego jar, z którego pochodzi ta klasa, a nie z innego. Według moich obliczeń to moduł ładujący klasy ładuje zasób i na pewno nie będzie ograniczony do ładowania tylko z jednego pliku jar?
Michael Wiles
10

Przeszukuję trzy miejsca, jak pokazano poniżej. Komentarze mile widziane.

public URL getResource(String resource){

    URL url ;

    //Try with the Thread Context Loader. 
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if(classLoader != null){
        url = classLoader.getResource(resource);
        if(url != null){
            return url;
        }
    }

    //Let's now try with the classloader that loaded this class.
    classLoader = Loader.class.getClassLoader();
    if(classLoader != null){
        url = classLoader.getResource(resource);
        if(url != null){
            return url;
        }
    }

    //Last ditch attempt. Get the resource from the classpath.
    return ClassLoader.getSystemResource(resource);
}
dogbane
źródło
Dzięki, to świetny pomysł. Dokładnie to, czego potrzebowałem.
devo
2
Patrzyłem na komentarze w twoim kodzie i ten ostatni brzmi interesująco. Czy wszystkie zasoby nie zostały załadowane ze ścieżki klas? A jakie przypadki obejmie ClassLoader.getSystemResource (), że powyższe nie powiodło się?
nyxz
Szczerze mówiąc, nie rozumiem, dlaczego chcesz ładować pliki z 3 różnych miejsc. Nie wiesz, gdzie są przechowywane Twoje pliki?
bvdb
3

Wiem, że jest naprawdę późno na inną odpowiedź, ale chciałem tylko podzielić się tym, co pomogło mi na końcu. Załaduje również zasoby / pliki z bezwzględnej ścieżki systemu plików (nie tylko ścieżki klas).

public class ResourceLoader {

    public static URL getResource(String resource) {
        final List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
        classLoaders.add(Thread.currentThread().getContextClassLoader());
        classLoaders.add(ResourceLoader.class.getClassLoader());

        for (ClassLoader classLoader : classLoaders) {
            final URL url = getResourceWith(classLoader, resource);
            if (url != null) {
                return url;
            }
        }

        final URL systemResource = ClassLoader.getSystemResource(resource);
        if (systemResource != null) {
            return systemResource;
        } else {
            try {
                return new File(resource).toURI().toURL();
            } catch (MalformedURLException e) {
                return null;
            }
        }
    }

    private static URL getResourceWith(ClassLoader classLoader, String resource) {
        if (classLoader != null) {
            return classLoader.getResource(resource);
        }
        return null;
    }

}
nyxz
źródło
0

Wypróbowałem wiele sposobów i funkcji, które zasugerowałem powyżej, ale nie zadziałały w moim projekcie. Tak czy inaczej znalazłem rozwiązanie i oto jest:

try {
    InputStream path = this.getClass().getClassLoader().getResourceAsStream("img/left-hand.png");
    img = ImageIO.read(path);
} catch (IOException e) {
    e.printStackTrace();
}
Vladislav
źródło
W this.getClass().getResourceAsStream()tym przypadku powinieneś lepiej użyć . Jeśli przyjrzysz się źródłu getResourceAsStreammetody, zauważysz, że robi to samo co ty, ale w sprytniejszy sposób (jeśli nie ClassLoadermożna znaleźć w klasie). Wskazuje również, że można napotkać potencjał nullna getClassLoaderw kodzie ...
Doc Davluz
@PromCompot, jak powiedziałem, this.getClass().getResourceAsStream()nie działa dla mnie, więc używam tego. Myślę, że są ludzie, którzy mogą zmierzyć się z takim problemem jak mój.
Vladislav